<?xml version="1.0" encoding="utf-8" standalone="yes"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/">
  <channel>
    <title>Nirav Raval</title>
    <link>https://niravraval.com/</link>
    <description>Recent content on Nirav Raval</description>
    <image>
      <title>Nirav Raval</title>
      <url>https://niravraval.com/Images/Nirav_about.jpg</url>
      <link>https://niravraval.com/Images/Nirav_about.jpg</link>
    </image>
    <generator>Hugo -- 0.147.3</generator>
    <language>en-us</language>
    <lastBuildDate>Sun, 03 May 2026 23:34:56 -0400</lastBuildDate>
    <atom:link href="https://niravraval.com/index.xml" rel="self" type="application/rss+xml" />
    <item>
      <title>How to build a High-Performance MCP Server on Azure Functions</title>
      <link>https://niravraval.com/blog/2026/may/how-to-build-a-high-performance-mcp-server-on-azure-functions/</link>
      <pubDate>Mon, 04 May 2026 00:00:00 +0000</pubDate>
      <guid>https://niravraval.com/blog/2026/may/how-to-build-a-high-performance-mcp-server-on-azure-functions/</guid>
      <description>&lt;p&gt;In my recent project, I integrated the &lt;strong&gt;Model Context Protocol (MCP)&lt;/strong&gt; into an existing &lt;strong&gt;Azure Functions&lt;/strong&gt; API and I wanted to share how it works and why it matters. MCP (Model Context Protocol) provides a standardized interface for connecting Large Language Models (LLMs), such as Microsoft 365 Copilot, to private data and specialized tools. While many implementations focus on local setups, deploying an MCP server in a cloud-native, serverless environment like Azure Functions offers the scalability, security, and flexibility needed for enterprise-grade applications.&lt;/p&gt;</description>
      <content:encoded><![CDATA[<p>In my recent project, I integrated the <strong>Model Context Protocol (MCP)</strong> into an existing <strong>Azure Functions</strong> API and I wanted to share how it works and why it matters. MCP (Model Context Protocol) provides a standardized interface for connecting Large Language Models (LLMs), such as Microsoft 365 Copilot, to private data and specialized tools. While many implementations focus on local setups, deploying an MCP server in a cloud-native, serverless environment like Azure Functions offers the scalability, security, and flexibility needed for enterprise-grade applications.</p>
<h2 id="the-gateway-pattern">The Gateway Pattern</h2>
<p>The Gateway Pattern is the optimal architecture for implementing MCP in Azure Functions. Rather than mapping every tool to a distinct function endpoint, a single <strong>McpGateway</strong> function serves as a protocol adapter. This function exposes all your API endpoints through a unified entry point.</p>
<h3 id="request-flow">Request Flow</h3>
<p>The interaction follows a structured flow where the client sends a <a href="https://www.jsonrpc.org/specification"><strong>JSON-RPC</strong></a> request to the <code>/api/mcp</code> endpoint. The gateway then validates this request, identifies the user context, and parses the specific method requested. Next, the execution engine uses reflection and dependency injection to find and run the necessary tool. Finally, the system packages the result into a JSON-RPC response and sends it back to the client.</p>
<figure class="align-center ">
    <img loading="lazy" src="/Images/blog_images/2026/May/MCPAZ1.png#center"
         alt="Alt text"/> <figcaption>
            <p>(Generated using <a href="https://www.eraser.io/">eraser.io</a>)</p>
        </figcaption>
</figure>

<h2 id="the-json-rpc-router">The JSON-RPC Router</h2>
<p>MCP operations utilize <a href="https://www.jsonrpc.org/specification"><strong>JSON-RPC 2.0</strong></a> (a stateless protocol). The gateway must process three core method types: <code>initialize</code>, <code>tools/list</code>, and <code>tools/call</code>.</p>
<h3 id="1-the-initialize-handshake">1. The Initialize Handshake</h3>
<p>Before calling any tools, the client &ldquo;handshakes&rdquo; with your server to understand its capabilities. This code below defines an Azure Function named McpGateway that acts as an HTTP endpoint for handling JSON-RPC requests. It begins by asynchronously reading the incoming request body and parsing it into a JSON object to extract the method and id fields.</p>
<p>The logic specifically checks if the client is calling the initialize method, which is the standard handshake for the Model Context Protocol (MCP). If it matches, the function returns a successful response containing the protocol version, the server’s capabilities (specifically tool listing), and metadata about the server. This confirms the connection between the client and the server.
 </p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-csharp" data-lang="csharp"><span style="display:flex;"><span><span style="color:#a6e22e">[Function(&#34;McpGateway&#34;)]</span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">public</span> <span style="color:#66d9ef">async</span> Task&lt;HttpResponseData&gt; Run([HttpTrigger(AuthorizationLevel.Anonymous, <span style="color:#e6db74">&#34;post&#34;</span>)] HttpRequestData req)
</span></span><span style="display:flex;"><span>{
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">var</span> requestBody = <span style="color:#66d9ef">await</span> req.ReadAsStringAsync();
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">var</span> jsonRequest = JsonConvert.DeserializeObject&lt;JObject&gt;(requestBody);
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">var</span> method = jsonRequest[<span style="color:#e6db74">&#34;method&#34;</span>]?.ToString();
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">var</span> id = jsonRequest[<span style="color:#e6db74">&#34;id&#34;</span>];
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">if</span> (method == <span style="color:#e6db74">&#34;initialize&#34;</span>) {
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">return</span> <span style="color:#66d9ef">await</span> CreateJsonRpcResponse(req, id, <span style="color:#66d9ef">new</span> {
</span></span><span style="display:flex;"><span>            protocolVersion = <span style="color:#e6db74">&#34;2025-11-25&#34;</span>,
</span></span><span style="display:flex;"><span>            capabilities = <span style="color:#66d9ef">new</span> { tools = <span style="color:#66d9ef">new</span> { listChanged = <span style="color:#66d9ef">true</span> } },
</span></span><span style="display:flex;"><span>            serverInfo = <span style="color:#66d9ef">new</span> { name = <span style="color:#e6db74">&#34;EnterpriseMcpServer&#34;</span>, version = <span style="color:#e6db74">&#34;1.0.0&#34;</span> }
</span></span><span style="display:flex;"><span>        });
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p><strong>Example Input:</strong></p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-json" data-lang="json"><span style="display:flex;"><span>{
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">&#34;jsonrpc&#34;</span>: <span style="color:#e6db74">&#34;2.0&#34;</span>,
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">&#34;id&#34;</span>: <span style="color:#ae81ff">1</span>,
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">&#34;method&#34;</span>: <span style="color:#e6db74">&#34;initialize&#34;</span>,
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">&#34;params&#34;</span>: {
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">&#34;protocolVersion&#34;</span>: <span style="color:#e6db74">&#34;2025-11-25&#34;</span>,
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">&#34;capabilities&#34;</span>: {}
</span></span><span style="display:flex;"><span>  }
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p><strong>Example Output:</strong></p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-json" data-lang="json"><span style="display:flex;"><span>{
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">&#34;jsonrpc&#34;</span>: <span style="color:#e6db74">&#34;2.0&#34;</span>,
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">&#34;id&#34;</span>: <span style="color:#ae81ff">1</span>,
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">&#34;result&#34;</span>: {
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">&#34;protocolVersion&#34;</span>: <span style="color:#e6db74">&#34;2025-11-25&#34;</span>,
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">&#34;capabilities&#34;</span>: { <span style="color:#f92672">&#34;tools&#34;</span>: { <span style="color:#f92672">&#34;listChanged&#34;</span>: <span style="color:#66d9ef">true</span> } },
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">&#34;serverInfo&#34;</span>: { <span style="color:#f92672">&#34;name&#34;</span>: <span style="color:#e6db74">&#34;McpServer&#34;</span>, <span style="color:#f92672">&#34;version&#34;</span>: <span style="color:#e6db74">&#34;1.0.0&#34;</span> }
</span></span><span style="display:flex;"><span>  }
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><h2 id="2-dynamic-tool-discovery-toolslist">2. Dynamic Tool Discovery (<code>tools/list</code>)</h2>
<p>Dynamic tool discovery lets your server automatically find and expose available tools without hard-coding them. Instead of maintaining a manual list, you simply mark the methods you want to expose, and the system discovers them at runtime.</p>
<p>You do this using attributes (decorators) to clearly define which classes and methods act as tools:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-csharp" data-lang="csharp"><span style="display:flex;"><span><span style="color:#a6e22e">[AttributeUsage(AttributeTargets.Class)]</span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">public</span> <span style="color:#66d9ef">class</span> <span style="color:#a6e22e">McpToolProviderAttribute</span> : Attribute { }
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">[AttributeUsage(AttributeTargets.Method)]</span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">public</span> <span style="color:#66d9ef">class</span> <span style="color:#a6e22e">McpToolAttribute</span> : Attribute { 
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">public</span> <span style="color:#66d9ef">string</span> Name { <span style="color:#66d9ef">get</span>; <span style="color:#66d9ef">set</span>; }
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">public</span> <span style="color:#66d9ef">string</span> Description { <span style="color:#66d9ef">get</span>; <span style="color:#66d9ef">set</span>; }
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>The server then uses reflection to scan for these attributes and build a tool list dynamically:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-csharp" data-lang="csharp"><span style="display:flex;"><span><span style="color:#66d9ef">var</span> toolList = Assembly.GetExecutingAssembly()
</span></span><span style="display:flex;"><span>    .GetTypes()
</span></span><span style="display:flex;"><span>    .Where(t =&gt; t.GetCustomAttribute&lt;McpToolProviderAttribute&gt;() != <span style="color:#66d9ef">null</span>)
</span></span><span style="display:flex;"><span>    .SelectMany(t =&gt; t.GetMethods())
</span></span><span style="display:flex;"><span>    .Where(m =&gt; m.GetCustomAttribute&lt;McpToolAttribute&gt;() != <span style="color:#66d9ef">null</span>)
</span></span><span style="display:flex;"><span>    .Select(m =&gt; <span style="color:#66d9ef">new</span> {
</span></span><span style="display:flex;"><span>        name = m.GetCustomAttribute&lt;McpToolAttribute&gt;().Name,
</span></span><span style="display:flex;"><span>        description = m.GetCustomAttribute&lt;McpToolAttribute&gt;().Description,
</span></span><span style="display:flex;"><span>        inputSchema = <span style="color:#66d9ef">new</span> {
</span></span><span style="display:flex;"><span>            type = <span style="color:#e6db74">&#34;object&#34;</span>,
</span></span><span style="display:flex;"><span>            properties = m.GetParameters().ToDictionary(
</span></span><span style="display:flex;"><span>                p =&gt; p.Name, 
</span></span><span style="display:flex;"><span>                p =&gt; <span style="color:#66d9ef">new</span> { type = GetJsonType(p.ParameterType), description = GetDescription(p) }
</span></span><span style="display:flex;"><span>            ),
</span></span><span style="display:flex;"><span>            required = m.GetParameters().Where(p =&gt; !p.IsOptional).Select(p =&gt; p.Name)
</span></span><span style="display:flex;"><span>        }
</span></span><span style="display:flex;"><span>    });
</span></span></code></pre></div><p><strong>Tool Definition Example:</strong></p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-csharp" data-lang="csharp"><span style="display:flex;"><span><span style="color:#a6e22e">[McpToolProvider]</span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">public</span> <span style="color:#66d9ef">class</span> <span style="color:#a6e22e">SharePointTools</span> {
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">    [McpTool(Name = &#34;search_documents&#34;, Description = &#34;Search for files in a document library&#34;)]</span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">public</span> <span style="color:#66d9ef">string</span> SearchDocs(<span style="color:#66d9ef">string</span> query, <span style="color:#66d9ef">int</span> limit = <span style="color:#ae81ff">5</span>) =&gt; <span style="color:#e6db74">$&#34;Results for {query}&#34;</span>;
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>If you define multiple tools (for example, 5 methods with <code>[McpTool]</code>), the server will automatically return all 5 in the response:</p>
<p><strong>Output to LLM:</strong></p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-json" data-lang="json"><span style="display:flex;"><span>{
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">&#34;tools&#34;</span>: [
</span></span><span style="display:flex;"><span>    { <span style="color:#f92672">&#34;name&#34;</span>: <span style="color:#e6db74">&#34;search_documents&#34;</span> },
</span></span><span style="display:flex;"><span>    { <span style="color:#f92672">&#34;name&#34;</span>: <span style="color:#e6db74">&#34;get_document&#34;</span> },
</span></span><span style="display:flex;"><span>    { <span style="color:#f92672">&#34;name&#34;</span>: <span style="color:#e6db74">&#34;create_document&#34;</span> },
</span></span><span style="display:flex;"><span>    { <span style="color:#f92672">&#34;name&#34;</span>: <span style="color:#e6db74">&#34;update_document&#34;</span> },
</span></span><span style="display:flex;"><span>    { <span style="color:#f92672">&#34;name&#34;</span>: <span style="color:#e6db74">&#34;delete_document&#34;</span> }
</span></span><span style="display:flex;"><span>  ]
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><h2 id="3-the-execution-toolscall">3. The Execution (<code>tools/call</code>)</h2>
<p>The final component is the execution engine. Once the AI knows what tools exist, it needs a way to trigger them. The tools/call method maps incoming JSON arguments to C# types and invokes the corresponding method using Dependency Injection (DI). This allows your tools can access databases, logging, or other enterprise services safely.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-csharp" data-lang="csharp"><span style="display:flex;"><span><span style="color:#66d9ef">private</span> <span style="color:#66d9ef">async</span> Task&lt;<span style="color:#66d9ef">string</span>&gt; InvokeToolAsync(<span style="color:#66d9ef">string</span> toolName, JObject arguments)
</span></span><span style="display:flex;"><span>{
</span></span><span style="display:flex;"><span>    <span style="color:#75715e">// Locate the tool metadata in the registry</span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">var</span> toolInfo = _toolRegistry[toolName];
</span></span><span style="display:flex;"><span>    
</span></span><span style="display:flex;"><span>    <span style="color:#75715e">// Resolve the class instance via DI (Dependency Injection)</span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">var</span> toolInstance = _serviceProvider.GetRequiredService(toolInfo.Type);
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">var</span> method = toolInfo.Method;
</span></span><span style="display:flex;"><span>    
</span></span><span style="display:flex;"><span>    <span style="color:#75715e">// Prepare the arguments for the C# method</span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">var</span> parameters = method.GetParameters();
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">object</span>[] methodArgs = <span style="color:#66d9ef">new</span> <span style="color:#66d9ef">object</span>[parameters.Length];
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">for</span> (<span style="color:#66d9ef">int</span> i = <span style="color:#ae81ff">0</span>; i &lt; parameters.Length; i++) {
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">var</span> p = parameters[i];
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">if</span> (arguments.TryGetValue(p.Name, <span style="color:#66d9ef">out</span> <span style="color:#66d9ef">var</span> token)) {
</span></span><span style="display:flex;"><span>            <span style="color:#75715e">// Convert JSON types to C# types</span>
</span></span><span style="display:flex;"><span>            methodArgs[i] = token.ToObject(p.ParameterType);
</span></span><span style="display:flex;"><span>        } <span style="color:#66d9ef">else</span> <span style="color:#66d9ef">if</span> (p.IsOptional) {
</span></span><span style="display:flex;"><span>            methodArgs[i] = p.DefaultValue;
</span></span><span style="display:flex;"><span>        }
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#75715e">// Execute the logic and handle Async/Sync results</span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">var</span> result = method.Invoke(toolInstance, methodArgs);
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">return</span> result <span style="color:#66d9ef">is</span> Task&lt;<span style="color:#66d9ef">string</span>&gt; t ? <span style="color:#66d9ef">await</span> t : result?.ToString();
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>This execution engine is the bridge between the AI and your application logic. It finds the correct tool, prepares the inputs, runs the method, and returns the result. Everything in a structured way.</p>
<p><strong>Example Input (<code>tools/call</code>):</strong> Sends the function name along with the necessary arguments.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-json" data-lang="json"><span style="display:flex;"><span>{
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">&#34;method&#34;</span>: <span style="color:#e6db74">&#34;tools/call&#34;</span>,
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">&#34;params&#34;</span>: {
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">&#34;name&#34;</span>: <span style="color:#e6db74">&#34;Get_record&#34;</span>,
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">&#34;arguments&#34;</span>: {
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">&#34;recordId&#34;</span>: <span style="color:#e6db74">&#34;REC-99&#34;</span>,
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">&#34;status&#34;</span>: <span style="color:#e6db74">&#34;InProgress&#34;</span>
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>  }
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p><strong>Example Output:</strong> The server executes the C# method and wraps the return value into a standardized MCP response format for the AI to process.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-json" data-lang="json"><span style="display:flex;"><span>{
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">&#34;jsonrpc&#34;</span>: <span style="color:#e6db74">&#34;2.0&#34;</span>,
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">&#34;id&#34;</span>: <span style="color:#e6db74">&#34;call-456&#34;</span>,
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">&#34;result&#34;</span>: {
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">&#34;content&#34;</span>: [
</span></span><span style="display:flex;"><span>      {
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">&#34;type&#34;</span>: <span style="color:#e6db74">&#34;text&#34;</span>,
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">&#34;text&#34;</span>: <span style="color:#e6db74">&#34;{ recordId: 12, description: \&#34;...\&#34; }&#34;</span>
</span></span><span style="display:flex;"><span>      }
</span></span><span style="display:flex;"><span>    ]
</span></span><span style="display:flex;"><span>  }
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><h2 id="best-practices">Best Practices</h2>
<ul>
<li>Extract JWT tokens from the <code>Authorization</code> header to expose and connect to the server.</li>
<li>Implement all tools as <code>Task&lt;string&gt;</code>. This aligns with the asynchronous nature of Azure Functions and external Graph API calls.</li>
</ul>
<h2 id="conclusion">Conclusion</h2>
<p>It can feel time consuming and a bit tedious to set everything up, but once it’s in place, MCP delivers real value. In my opinion, building an MCP server with the gateway pattern isn’t about adding complexity, it’s about creating a clean, secure, and scalable bridge between LLMs and your enterprise systems. With a platform like Azure Functions, this approach improves reliability and security while making it much easier to grow and adapt your capabilities as LLM-powered applications evolve.</p>
]]></content:encoded>
    </item>
    <item>
      <title>Power Apps Cards Retirement and Road Ahead</title>
      <link>https://niravraval.com/blog/2025/august/power-apps-cards-retirement-and-road-ahead/</link>
      <pubDate>Fri, 15 Aug 2025 00:00:00 +0000</pubDate>
      <guid>https://niravraval.com/blog/2025/august/power-apps-cards-retirement-and-road-ahead/</guid>
      <description>&lt;p&gt;If you’ve been using Cards for Power Apps in Teams, kindly note that they will be &lt;a href=&#34;https://learn.microsoft.com/en-us/power-platform/important-changes-coming#deprecation-of-cards-for-power-apps&#34;&gt;deprecated&lt;/a&gt; on &lt;strong&gt;August 29, 2025&lt;/strong&gt;. However, there are few alternatives available, and in this blog post, I will guide you through those.&lt;/p&gt;
&lt;h2 id=&#34;whats-happening&#34;&gt;What&amp;rsquo;s Happening?&lt;/h2&gt;
&lt;p&gt;After August 29, 2025, three major things happen:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Existing PowerApps cards in Teams will stop working.&lt;/li&gt;
&lt;li&gt;You can’t create new ones.&lt;/li&gt;
&lt;li&gt;Any flows or automations that rely on them will break.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Microsoft is moving everything to Adaptive Cards, which are now seen as the standard format across Teams, Outlook, and your favorite Copilot.&lt;/p&gt;</description>
      <content:encoded><![CDATA[<p>If you’ve been using Cards for Power Apps in Teams, kindly note that they will be <a href="https://learn.microsoft.com/en-us/power-platform/important-changes-coming#deprecation-of-cards-for-power-apps">deprecated</a> on <strong>August 29, 2025</strong>. However, there are few alternatives available, and in this blog post, I will guide you through those.</p>
<h2 id="whats-happening">What&rsquo;s Happening?</h2>
<p>After August 29, 2025, three major things happen:</p>
<ul>
<li>Existing PowerApps cards in Teams will stop working.</li>
<li>You can’t create new ones.</li>
<li>Any flows or automations that rely on them will break.</li>
</ul>
<p>Microsoft is moving everything to Adaptive Cards, which are now seen as the standard format across Teams, Outlook, and your favorite Copilot.</p>
<h2 id="what-is-adaptive-cards-">What is Adaptive Cards ?</h2>
<p>If you’re not familiar, <a href="https://adaptivecards.io/">Adaptive Cards</a> are basically rich, interactive UI snippets described in JSON. You can use them to collect inputs, display dynamic information, trigger actions, or even handle approvals. For example, a help desk ticket card can display fields such as &ldquo;Title&rdquo; and &ldquo;Description,&rdquo; along with a submit button, allowing users to send this information directly to Power Automate without leaving Teams or Outlook.</p>
<p><img alt="PowerApps Cards Deprication Image 1" loading="lazy" src="/Images/blog_images/2025/August/AC_1.png"></p>
<p>Few advantages that I can think for Adaptive cards&hellip;</p>
<ul>
<li>Adapt to the app they’re in (Teams, Outlook, Windows, etc.).</li>
<li>Support text, images, buttons, and input fields.</li>
<li>Look native in light/dark mode.</li>
<li>Work in multiple places without rebuilding.</li>
</ul>
<p>In short, it&rsquo;s piece of a card with one design, but can work on many platforms.</p>
<h2 id="which-option-should-you-select">Which option should you select?</h2>
<p>Below are three recommendations you can choose from based on your goals.</p>
<ol>
<li><strong>Adaptive cards in Teams</strong> - You can deliver these without coding using <strong>Power Automate</strong>. Create the card in JSON, post it to a chat or channel, and collect responses directly in Teams. Perfect for quick updates, collecting feedback, or handling approvals. Bots can also send these cards for more dynamic scenarios.</li>
</ol>
<p><img alt="PowerApps Cards Deprication Image 2" loading="lazy" src="/Images/blog_images/2025/August/AC_2.png"></p>
<ol start="2">
<li><strong>Adaptive cards in Copilot Studio</strong> - If you’re building in Copilot Studio, you can present Adaptive Cards right in a conversation. For example, when a user says &ldquo;Log a help desk ticket&rdquo;, Copilot can show a card asking for details, then trigger your automation once submitted — all inside the chat.</li>
</ol>
<p><img alt="PowerApps Cards Deprication Image 3" loading="lazy" src="/Images/blog_images/2025/August/AC_3.png"></p>
<ol start="3">
<li><strong>Outlook Actionable Messages</strong> - These are Adaptive Cards embedded in Outlook emails. The recipient can approve, reject, or submit information without leaving their inbox.</li>
</ol>
<p><img alt="PowerApps Cards Deprication Image 4" loading="lazy" src="/Images/blog_images/2025/August/AC_4.png"></p>
<h2 id="my-view">My View</h2>
<p>Accept it and move on! You don’t have to stop using Power Apps. But make sure instead of putting the card inside Power Apps, you switch it in Teams, Outlook, or Copilot, then send the responses back to your Power Apps app or Dataverse tables. This way, your backend stays the same, but the front-end experience becomes more modern and works across more places.</p>
<p>I know it’s a tedious task and will take some effort now, but once you’ve moved to Adaptive Cards, your solutions will look cleaner, and with Microsoft backing it you can trust it’s here to stay. You know what I mean! 😉</p>
<p><strong>Useful Resources</strong></p>
<ul>
<li><a href="https://learn.microsoft.com/en-us/power-apps/cards/overview">Power Apps cards overview</a></li>
<li><a href="https://learn.microsoft.com/en-us/power-automate/overview-adaptive-cards">Adaptive cards in Power Automate</a></li>
<li><a href="https://learn.microsoft.com/en-us/microsoft-copilot-studio/authoring-ask-with-adaptive-card">&ldquo;Ask with Adaptive Card&rdquo; in Microsoft Copilot Studio</a></li>
<li><a href="https://learn.microsoft.com/en-us/outlook/actionable-messages/">Actionable messages in Outlook</a></li>
</ul>
]]></content:encoded>
    </item>
    <item>
      <title>Productivity Tool - PowerToys Workspaces</title>
      <link>https://niravraval.com/blog/2025/july/productivity-tool---powertoys-workspaces/</link>
      <pubDate>Wed, 02 Jul 2025 00:00:00 +0000</pubDate>
      <guid>https://niravraval.com/blog/2025/july/productivity-tool---powertoys-workspaces/</guid>
      <description>&lt;p&gt;I’ve been using this handy tool called &lt;a href=&#34;https://learn.microsoft.com/en-us/windows/powertoys/&#34;&gt;PowerToys&lt;/a&gt;. That contains a bunch of utilities that make working on Windows easier and faster. One of my favorite features in PowerToys is &lt;strong&gt;Workspaces&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;I truly hate restarting my machine. The pain is, all your apps and windows disappear, and it takes forever to get everything back the way it was.&lt;/p&gt;
&lt;p&gt;That’s where PowerToys Workspaces is a helpful tool. It’s a desktop manager that lets you save your current desktop setup (all apps and window positions), and later launch everything back in place with just one click.&lt;/p&gt;</description>
      <content:encoded><![CDATA[<p>I’ve been using this handy tool called <a href="https://learn.microsoft.com/en-us/windows/powertoys/">PowerToys</a>. That contains a bunch of utilities that make working on Windows easier and faster. One of my favorite features in PowerToys is <strong>Workspaces</strong>.</p>
<p>I truly hate restarting my machine. The pain is, all your apps and windows disappear, and it takes forever to get everything back the way it was.</p>
<p>That’s where PowerToys Workspaces is a helpful tool. It’s a desktop manager that lets you save your current desktop setup (all apps and window positions), and later launch everything back in place with just one click.</p>
<p>Here&rsquo;s the simple guide that you can follow and use the tool:</p>
<h2 id="install-powertoys-and-enable-workspaces">Install PowerToys and Enable WorkSpaces</h2>
<p>Download and install PowerToys from the official link:</p>
<p><a href="https://learn.microsoft.com/en-us/windows/powertoys/install">https://learn.microsoft.com/en-us/windows/powertoys/install</a></p>
<p>Open, and turn on <strong>Workspaces</strong> toggle if it&rsquo;s turned off.</p>
<p>You can open the editor using by selecting &ldquo;Launch editor&rdquo; from PowerToys Workspaces settings or by using the shortcut Win+Ctrl+ `</p>
<p><img alt="Productivitytip Image 1" loading="lazy" src="/Images/blog_images/2025/July/Productivityimage1.png"></p>
<h2 id="open-your-desired-setup">Open Your Desired Setup</h2>
<p>Now open all the apps and windows you want to save. I have 3 screens with VS Code, browser tabs, Outlook, Teams, and more.</p>
<p>Once everything is ready, click on <strong>&ldquo;Create workspace&rdquo;</strong></p>
<p><img alt="Productivitytip Image 2" loading="lazy" src="/Images/blog_images/2025/July/Productivityimage2.png"></p>
<h2 id="capture-the-workspace">Capture the Workspace</h2>
<p>Click the <strong>&ldquo;Capture”</strong> button and PowerToys will take a snapshot of your current layout.</p>
<p><img alt="Productivitytip Image 3" loading="lazy" src="/Images/blog_images/2025/July/Productivityimage3.png"></p>
<p>You’ll see an editor where you can:</p>
<ul>
<li>Give your workspace a name</li>
<li>Remove windows you don’t want to include</li>
<li>Create a desktop shortcut</li>
</ul>
<p>After editing, click on <strong>&ldquo;Save workspace&rdquo;</strong></p>
<p><img alt="Productivitytip Image 4" loading="lazy" src="/Images/blog_images/2025/July/Productivityimage4.png"></p>
<h2 id="open-workspace">Open Workspace</h2>
<p>And that&rsquo;s it. Your saved workspace will now appear in the PowerToys dashboard.</p>
<p>Even after restarting machine, you can launch it anytime and all your apps will reopen exactly where you had them before.</p>
<p><img alt="Productivitytip Image 5" loading="lazy" src="/Images/blog_images/2025/July/Productivityimage5.png"></p>
<p>Try it out! Create a workspace and let me know how it goes for you.</p>
<h2 id="powertoys-utilities">Powertoys utilities:</h2>
<p>If you are curious about other utilities that tool offer, check out below list.</p>
<table>
  <thead>
      <tr>
          <th></th>
          <th>Current utilities</th>
          <th></th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><a href="https://aka.ms/PowerToysOverview_AdvancedPaste">Advanced Paste</a></td>
          <td><a href="https://aka.ms/PowerToysOverview_AoT">Always on Top</a></td>
          <td><a href="https://aka.ms/PowerToysOverview_Awake">PowerToys Awake</a></td>
      </tr>
      <tr>
          <td><a href="https://aka.ms/PowerToysOverview_ColorPicker">Color Picker</a></td>
          <td><a href="https://aka.ms/PowerToysOverview_CmdNotFound">Command Not Found</a></td>
          <td><a href="https://aka.ms/PowerToysOverview_CmdPal">Command Palette</a></td>
      </tr>
      <tr>
          <td><a href="https://aka.ms/PowerToysOverview_CropAndLock">Crop And Lock</a></td>
          <td><a href="https://aka.ms/PowerToysOverview_EnvironmentVariables">Environment Variables</a></td>
          <td><a href="https://aka.ms/PowerToysOverview_FancyZones">FancyZones</a></td>
      </tr>
      <tr>
          <td><a href="https://aka.ms/PowerToysOverview_FileExplorerAddOns">File Explorer Add-ons</a></td>
          <td><a href="https://aka.ms/PowerToysOverview_FileLocksmith">File Locksmith</a></td>
          <td><a href="https://aka.ms/PowerToysOverview_HostsFileEditor">Hosts File Editor</a></td>
      </tr>
      <tr>
          <td><a href="https://aka.ms/PowerToysOverview_ImageResizer">Image Resizer</a></td>
          <td><a href="https://aka.ms/PowerToysOverview_KeyboardManager">Keyboard Manager</a></td>
          <td><a href="https://aka.ms/PowerToysOverview_MouseUtilities">Mouse utilities</a></td>
      </tr>
      <tr>
          <td><a href="https://aka.ms/PowerToysOverview_MouseWithoutBorders">Mouse Without Borders</a></td>
          <td><a href="https://aka.ms/PowerToysOverview_NewPlus">New+</a></td>
          <td><a href="https://aka.ms/PowerToysOverview_PastePlain">Paste as Plain Text</a></td>
      </tr>
      <tr>
          <td><a href="https://aka.ms/PowerToysOverview_Peek">Peek</a></td>
          <td><a href="https://aka.ms/PowerToysOverview_PowerRename">PowerRename</a></td>
          <td><a href="https://aka.ms/PowerToysOverview_PowerToysRun">PowerToys Run</a></td>
      </tr>
      <tr>
          <td><a href="https://aka.ms/PowerToysOverview_QuickAccent">Quick Accent</a></td>
          <td><a href="https://aka.ms/PowerToysOverview_RegistryPreview">Registry Preview</a></td>
          <td><a href="https://aka.ms/PowerToysOverview_ScreenRuler">Screen Ruler</a></td>
      </tr>
      <tr>
          <td><a href="https://aka.ms/PowerToysOverview_ShortcutGuide">Shortcut Guide</a></td>
          <td><a href="https://aka.ms/PowerToysOverview_TextExtractor">Text Extractor</a></td>
          <td><a href="https://aka.ms/PowerToysOverview_Workspaces">Workspaces</a></td>
      </tr>
      <tr>
          <td><a href="https://aka.ms/PowerToysOverview_ZoomIt">ZoomIt</a></td>
          <td></td>
          <td></td>
      </tr>
  </tbody>
</table>
<p><code>If I had to pick from this list, I would start with Color Picker, Text Extractor, and ZoomIt (which is underrated)</code></p>
<p>If you&rsquo;re already using PowerToys or planning to start using it now, let me know your favorite utility in the comment box below.</p>
]]></content:encoded>
    </item>
    <item>
      <title>How to Use Send HTTPS Request in Copilot Studio</title>
      <link>https://niravraval.com/blog/2025/may/how-to-use-send-https-request-in-copilot-studio/</link>
      <pubDate>Thu, 08 May 2025 00:00:00 +0000</pubDate>
      <guid>https://niravraval.com/blog/2025/may/how-to-use-send-https-request-in-copilot-studio/</guid>
      <description>&lt;p&gt;Microsoft &lt;a href=&#34;https://learn.microsoft.com/en-us/microsoft-copilot-studio/fundamentals-what-is-copilot-studio&#34;&gt;Copilot Studio&lt;/a&gt; is getting better every day! One of the coolest features is the ability to build your own custom agents with knowledge bases, topics, tools (previously Actions), and channels.&lt;/p&gt;
&lt;p&gt;In this blog, I’ll show you how to use the &lt;a href=&#34;https://learn.microsoft.com/en-us/microsoft-copilot-studio/authoring-http-node&#34;&gt;&lt;strong&gt;Send HTTPS Request&lt;/strong&gt;&lt;/a&gt; feature effectively. We’ll create a simple agent that fetches random quotes from the &lt;a href=&#34;https://breakingbadquotes.xyz/&#34;&gt;&lt;em&gt;Breaking Bad&lt;/em&gt; API&lt;/a&gt; (I just love using this API to show examples). The API response will return a quote along with the author’s name (character name) from the series.&lt;/p&gt;</description>
      <content:encoded><![CDATA[<p>Microsoft <a href="https://learn.microsoft.com/en-us/microsoft-copilot-studio/fundamentals-what-is-copilot-studio">Copilot Studio</a> is getting better every day! One of the coolest features is the ability to build your own custom agents with knowledge bases, topics, tools (previously Actions), and channels.</p>
<p>In this blog, I’ll show you how to use the <a href="https://learn.microsoft.com/en-us/microsoft-copilot-studio/authoring-http-node"><strong>Send HTTPS Request</strong></a> feature effectively. We’ll create a simple agent that fetches random quotes from the <a href="https://breakingbadquotes.xyz/"><em>Breaking Bad</em> API</a> (I just love using this API to show examples). The API response will return a quote along with the author’s name (character name) from the series.</p>
<p>Follow the steps below.</p>
<h2 id="step-1-create-a-new-agent">Step 1: Create a New Agent</h2>
<p>Go to <a href="https://copilotstudio.microsoft.com">Copilot Studio</a> and click on the <strong>New agent</strong> button. For this example, we’ll create the agent <strong>from scratch</strong>, without any use of AI.</p>
<p><img alt="Send Https Request Image 1" loading="lazy" src="/Images/blog_images/2025/May/CS1.png"></p>
<p>Click <strong>Create</strong>.</p>
<p><img alt="Send Https Request Image 2" loading="lazy" src="/Images/blog_images/2025/May/CS2.png"></p>
<h2 id="step-2-add-a-topic">Step 2: Add a Topic</h2>
<p>You’re inside your brand-new agent! Let&rsquo;s add a new topic. This is where we’ll place our HTTP request logic.</p>
<blockquote>
<p>Topics are like conversation flows. They define how your agent responds in different scenarios.</p></blockquote>
<p>Create a <strong>manual topic (From blank)</strong> for this demo.</p>
<p><img alt="Send Https Request Image 3" loading="lazy" src="/Images/blog_images/2025/May/CS3.png"></p>
<p>By default, you’ll see some system generated and custom topics.</p>
<h2 id="step-3-add-trigger-phrases">Step 3: Add Trigger Phrases</h2>
<p>Now, you’ll see a <strong>trigger</strong> created. This is where you define phrases or description that will start the topic.</p>
<p>Enter the following two phrases:</p>
<ul>
<li>
<p>Give me a breaking bad quote</p>
</li>
<li>
<p>New quote</p>
</li>
</ul>
<p><img alt="Send Https Request Image 4" loading="lazy" src="/Images/blog_images/2025/May/CS4.png"></p>
<h2 id="step-4-send-an-https-request">Step 4: Send an HTTPS Request</h2>
<p>Click the <strong>+ icon</strong>, choose <strong>Advanced</strong>, and then select <strong>Send an HTTPS Request</strong>.</p>
<p><img alt="Send Https Request Image 5" loading="lazy" src="/Images/blog_images/2025/May/CS5.png">
We’re going to call an open-source API to get quotes from <em>Breaking Bad</em>.</p>
<p>Enter this URL:
<code>https://api.breakingbadquotes.xyz/v1/quotes</code></p>
<p>Set the <strong>method</strong> to <code>GET</code>.</p>
<p>Also:</p>
<ul>
<li>
<p>Set the <strong>response type</strong> to <code>any</code></p>
</li>
<li>
<p>Create a variable (I called mine <code>Quote</code>) to store the response</p>
</li>
</ul>
<p>This API returns a quote and author, we’ll grab and display only <strong>quote</strong>.</p>
<p><img alt="Send Https Request Image 6" loading="lazy" src="/Images/blog_images/2025/May/CS6.png"></p>
<h2 id="step-5-send-the-quote-to-the-user">Step 5: Send the Quote to the User</h2>
<p>Let’s show the result to the user. Add a <strong>Send a message</strong> node.</p>
<p><img alt="Send Https Request Image 7" loading="lazy" src="/Images/blog_images/2025/May/CS7.png">
In the message box, use the following <code>Power Fx</code> formula to get the quote text:</p>
<p><code>First(Topic.Quote).quote</code></p>
<p>This pulls the first item from the response and shows the quote.</p>
<p><img alt="Send Https Request Image 8" loading="lazy" src="/Images/blog_images/2025/May/CS8.png"></p>
<h2 id="final-step-name-save--test">Final Step: Name, Save &amp; Test</h2>
<p>Give your topic a name like <strong>Get Quote</strong>, then click <strong>Save</strong>. Your topic flow should look something like this:</p>
<p><img alt="Send Https Request Image 9" loading="lazy" src="/Images/blog_images/2025/May/CS9.png">
Now, click <strong>Test</strong>, type <strong>&ldquo;Give me a breaking bad quote&rdquo;</strong>, and your agent will fetch a quote from the API and show it!</p>
<p><img alt="Send Https Request Image 10" loading="lazy" src="/Images/blog_images/2025/May/CS10.png">
We just used the <strong>GET</strong> method here, but Copilot Studio also supports <strong>POST</strong>, <strong>PUT</strong>, and <strong>DELETE</strong>.</p>
<p>Share your thoughts below!</p>
<p>Happy building!🐱</p>
]]></content:encoded>
    </item>
    <item>
      <title>Conditional Mapping in ShareGate Using PnP PowerShell</title>
      <link>https://niravraval.com/blog/2025/april/conditional-mapping-in-sharegate-using-pnp-powershell/</link>
      <pubDate>Fri, 11 Apr 2025 00:00:00 +0000</pubDate>
      <guid>https://niravraval.com/blog/2025/april/conditional-mapping-in-sharegate-using-pnp-powershell/</guid>
      <description>&lt;p&gt;In my last &lt;a href=&#34;https://niravraval.com/blog/2025/april/how-to-achieve-conditional-mapping-with-sharegate/#option-2-use-pnp-powershell-after-migration&#34;&gt;blog post&lt;/a&gt;, I explained two ways to tackle conditional mapping in ShareGate. In this post, I&amp;rsquo;ll show you how to use the &lt;a href=&#34;(https://pnp.github.io/powershell/index.html)&#34;&gt;PnP PowerShell module&lt;/a&gt; to handle conditional mapping more efficiently.&lt;/p&gt;
&lt;p&gt;As I mentioned earlier, it&amp;rsquo;s not practical to rely on the ShareGate user interface for each site. That’s where I recommend a better approach -&amp;gt; using PnP PowerShell module.&lt;/p&gt;
&lt;h2 id=&#34;pnp-powershell-script&#34;&gt;PnP PowerShell Script:&lt;/h2&gt;
&lt;p&gt;No big deal! Here&amp;rsquo;s the scenario I mentioned earlier: you want to run a script that checks each item’s &lt;strong&gt;Status&lt;/strong&gt; (a choice column in your SharePoint list), and if it’s set to &lt;strong&gt;“Pending”&lt;/strong&gt;, the script should update it to &lt;strong&gt;“Approved”&lt;/strong&gt;.&lt;/p&gt;</description>
      <content:encoded><![CDATA[<p>In my last <a href="https://niravraval.com/blog/2025/april/how-to-achieve-conditional-mapping-with-sharegate/#option-2-use-pnp-powershell-after-migration">blog post</a>, I explained two ways to tackle conditional mapping in ShareGate. In this post, I&rsquo;ll show you how to use the <a href="(https://pnp.github.io/powershell/index.html)">PnP PowerShell module</a> to handle conditional mapping more efficiently.</p>
<p>As I mentioned earlier, it&rsquo;s not practical to rely on the ShareGate user interface for each site. That’s where I recommend a better approach -&gt; using PnP PowerShell module.</p>
<h2 id="pnp-powershell-script">PnP PowerShell Script:</h2>
<p>No big deal! Here&rsquo;s the scenario I mentioned earlier: you want to run a script that checks each item’s <strong>Status</strong> (a choice column in your SharePoint list), and if it’s set to <strong>“Pending”</strong>, the script should update it to <strong>“Approved”</strong>.</p>
<blockquote>
<p><strong>Tip:</strong> By default, ShareGate uses PowerShell <strong>v5.0</strong>, but <strong>PnP requires PowerShell v7</strong> to run any scripts. So, if you want to run the PnP module from the same shell, use the <code>pwsh</code> command to launch the script.</p></blockquote>
<p>As of now, this kind of conditional check from source and update it to destination isn’t possible using ShareGate alone.</p>
<script src="https://gist.github.com/nirav-raval/d0ceaec2e5292b12d3939f63d36c3adf.js"></script>
<blockquote>
<p>Don’t know your client ID?, As of September 9th, 2024, this has become mandatory step. This<a href="https://pnp.github.io/powershell/articles/registerapplication.html"> article</a> will guide you through how to do so.</p></blockquote>
<p>This was just a simple use case. Yours might be more advanced. All the best tackling it!</p>
<p>Feel free to drop your thoughts or questions in the comments section below, I’d love to help if you&rsquo;re trying something similar.</p>
<p>Thanks again for reading. I hope you&rsquo;re finding these posts helpful.</p>
]]></content:encoded>
    </item>
    <item>
      <title>Conditional Mapping in ShareGate – Is It Even Possible?</title>
      <link>https://niravraval.com/blog/2025/april/how-to-achieve-conditional-mapping-with-sharegate/</link>
      <pubDate>Fri, 04 Apr 2025 00:00:00 +0000</pubDate>
      <guid>https://niravraval.com/blog/2025/april/how-to-achieve-conditional-mapping-with-sharegate/</guid>
      <description>&lt;p&gt;During a recent migration project, I worked with a customer who had a pretty unique requirement: they wanted to apply &lt;code&gt;conditional mappings&lt;/code&gt; using ShareGate.&lt;/p&gt;
&lt;p&gt;At first, I thought, &lt;em&gt;“Wait, what?”&lt;/em&gt; Because what they were asking for just isn&amp;rsquo;t something ShareGate supports out of the box—neither through the UI nor the ShareGate PowerShell module. But after a bit of head-scratching (and lots of coffee ☕), I came up with a workaround to get the job done.&lt;/p&gt;</description>
      <content:encoded><![CDATA[<p>During a recent migration project, I worked with a customer who had a pretty unique requirement: they wanted to apply <code>conditional mappings</code> using ShareGate.</p>
<p>At first, I thought, <em>“Wait, what?”</em> Because what they were asking for just isn&rsquo;t something ShareGate supports out of the box—neither through the UI nor the ShareGate PowerShell module. But after a bit of head-scratching (and lots of coffee ☕), I came up with a workaround to get the job done.</p>
<p>Before I get into the <em>how</em>, let’s back up a bit.</p>
<h2 id="what-is-conditional-mapping">What Is Conditional Mapping?</h2>
<p>If you’ve ever used ShareGate, you’ve probably worked with mapping configurations—where you can map metadata from the source to the destination. But what’s different about <em>conditional mapping</em>?</p>
<p>Conditional mapping means applying logic to the mapping process. In other words, you want to compare the value from a source column and based on a certain condition, modify it before it gets migrated to the destination.</p>
<blockquote>
<p><strong>Example:</strong> Imagine you’re migrating a list where the <strong>Status</strong> column sometimes contains the value <code>&quot;Pending&quot;</code>. During migration, you want to check for that and replace it with <code>&quot;Approved&quot;</code> only when that condition is met. That’s conditional mapping.</p></blockquote>
<p>Sounds simple enough, right?</p>
<h2 id="but-can-sharegate-do-that">But Can ShareGate Do That?</h2>
<p>If you go into ShareGate’s UI and try to modify metadata mappings, here are the options you’ll find:</p>
<ul>
<li>
<p>Set mapped value</p>
</li>
<li>
<p>Set a custom value</p>
</li>
<li>
<p>Ask Value (Manual mode only)</p>
</li>
<li>
<p>Set mapped value or default</p>
</li>
<li>
<p>Ignore</p>
</li>
</ul>
<p>Nowhere in that list do you see an option like <em>&ldquo;If Title contains X, replace with Y.&rdquo;</em> So no, <code>ShareGate doesn’t support conditional logic in metadata mappings</code>.</p>
<p><img alt="ShareGate MetaData Options" loading="lazy" src="/Images/blog_images/2025/April/SG1.png"></p>
<h2 id="what-about-modifying-the-sgt-template">What About Modifying the .sgt Template?</h2>
<blockquote>
<p><em>“Can I edit the ShareGate migration template (.sgt) file with PowerShell and add some conditions there?”</em></p></blockquote>
<p>Good question. I tried that too—and unfortunately, it didn’t work. If you try to forcefully apply a modified template with custom conditions, ShareGate throws a <strong>template read error</strong>. So that door’s closed as well.</p>
<h2 id="whats-the-workaround">What’s the Workaround?</h2>
<p>There are two practical ways to achieve conditional mapping:</p>
<h3 id="option-1-export--modify-metadata">Option 1: Export &amp; Modify Metadata</h3>
<ul>
<li>
<p>Export the source list metadata to an Excel file (screenshot below).</p>
</li>
<li>
<p>Manually update the data in Excel to reflect your desired changes.</p>
</li>
<li>
<p>Use <strong>Import &amp; Copy</strong> in ShareGate to push that modified data to the destination.</p>
</li>
</ul>
<p>This works fine if you&rsquo;re only dealing with a few sites or lists. It gives you full control over the values that will be written. But let’s be honest, are you really going to do that for <em>dozens</em> of sites? Probably not. It’s time-consuming and error-prone at scale.</p>
<p><img alt="ShareGate MetaData Options" loading="lazy" src="/Images/blog_images/2025/April/SG2.png"></p>
<h3 id="option-2-use-pnp-powershell-after-migration">Option 2: Use PnP PowerShell After Migration</h3>
<p>This is the smarter, scalable approach.</p>
<p>Migrate the content as-is using ShareGate. Then, use the <strong>PnP.PowerShell</strong> module to run a script that goes through the migrated items and applies your conditional logic.</p>
<p>For example, you can run a script that checks each item’s <strong>Status</strong> column, and if it contains <strong>&ldquo;Pending&rdquo;</strong>, it updates it to <strong>&ldquo;Approved&rdquo;</strong>. Simple, powerful, and works across sites.</p>
<h2 id="coming-up-next">Coming Up Next&hellip;</h2>
<p>In my next blog post, I’ll walk you through how to write a PnP PowerShell script—and how to execute it directly from the ShareGate migration PowerShell terminal.</p>
<p>So if you’ve ever faced this limitation and wanted a way around it, <strong>stay tuned!</strong></p>
]]></content:encoded>
    </item>
    <item>
      <title>Create a Custom Declarative Agent with open-source API</title>
      <link>https://niravraval.com/blog/2025/january/create-a-custom-declarative-agent-with-open-source-api/</link>
      <pubDate>Mon, 06 Jan 2025 00:00:00 +0000</pubDate>
      <guid>https://niravraval.com/blog/2025/january/create-a-custom-declarative-agent-with-open-source-api/</guid>
      <description>&lt;p&gt;Back in November 2024, Microsoft introduced new &lt;a href=&#34;https://techcommunity.microsoft.com/blog/microsoft365copilotblog/introducing-new-agents-in-microsoft-365/4296918?utm_source=chatgpt.com&#34;&gt;AI-powered agents&lt;/a&gt; in Microsoft 365 Copilot.  &lt;/p&gt;
&lt;p&gt;&lt;a href=&#34;https://learn.microsoft.com/en-us/microsoft-365-copilot/extensibility/overview-declarative-agent&#34;&gt;Declarative agents&lt;/a&gt; in Microsoft 365 Copilot are a type of AI assistant that helps businesses automate tasks and workflows by following predefined instructions and using enterprise data. They work within tools like Teams, Word, and PowerPoint to boost productivity. For example, a customer service agent can provide real-time order updates using an API plugin, to improve support quality. &lt;/p&gt;</description>
      <content:encoded><![CDATA[<p>Back in November 2024, Microsoft introduced new <a href="https://techcommunity.microsoft.com/blog/microsoft365copilotblog/introducing-new-agents-in-microsoft-365/4296918?utm_source=chatgpt.com">AI-powered agents</a> in Microsoft 365 Copilot.  </p>
<p><a href="https://learn.microsoft.com/en-us/microsoft-365-copilot/extensibility/overview-declarative-agent">Declarative agents</a> in Microsoft 365 Copilot are a type of AI assistant that helps businesses automate tasks and workflows by following predefined instructions and using enterprise data. They work within tools like Teams, Word, and PowerPoint to boost productivity. For example, a customer service agent can provide real-time order updates using an API plugin, to improve support quality. </p>
<p>Today, we will create a <strong>custom declarative agent</strong> using an open-source API to demonstrate how agents can interact with external data sources.</p>
<p><strong>Open Source API Overview:</strong></p>
<p>We&rsquo;ll be using the <a href="https://breakingbadquotes.xyz/">Breaking Bad API</a>, an open-source API that provides quotes from the popular TV series Breaking Bad. The API response will return a quote along with the author&rsquo;s name (character name) from the series.</p>
<p>👇<strong>Sample Response from API</strong> 👇</p>
<p><img alt="Custom Agent- CA1" loading="lazy" src="/Images/blog_images/2025/January/CA-1.png"></p>
<p>Let’s get started!</p>
<h2 id="step-1-creating-a-custom-declarative-agent-using-microsoft-toolkit-extension">Step 1: Creating a Custom Declarative Agent Using Microsoft Toolkit Extension</h2>
<p>Let&rsquo;s begin by creating a custom declarative agent using the <a href="https://learn.microsoft.com/en-us/microsoftteams/platform/toolkit/install-teams-toolkit?tabs=vscode">Microsoft Toolkit extension</a>. Follow these step-by-step instructions to set up your agent: </p>
<ol>
<li><strong>Launch the Microsoft Toolkit Extension</strong> and click on <em><code>Create a New App</code></em> under <strong>Create a Project or Explore Samples</strong></li>
<li>Choose <strong>Agent</strong> as your app type</li>
<li>Select <strong>Declarative Agent</strong> to build an agent powered by Microsoft 365 Copilot</li>
<li>Click <strong>Add Plugin</strong> to connect external APIs to your agent</li>
<li>Choose <strong>Start with a New API</strong> to set up a fresh connection for your agent</li>
<li>Pick <strong>None</strong> for authentication since the Breaking Bad API doesn’t require it</li>
<li>Opt for <strong>TypeScript</strong> as programming language for a smoother experience</li>
<li>Select a folder to store project files (pro tip: avoid saving it in the C drive for better organization)</li>
<li>Finally, give your agent a name like <strong>BreakingBad Agent</strong> and hit the setup button</li>
</ol>
<p>Wohooo! 🎉 You&rsquo;ve Successfully Created Your Declarative Agent with an API Plugin! After completing the setup, a new window will pop up in VS Code with your project structure ready. But wait—there&rsquo;s more! </p>
<p>By default, the project folder includes a demo API plugin. However, this plugin uses a local json file as its data source. Since we want to fetch real-time data from an actual API, we will need to remove the existing demo setup.</p>
<p><strong>Clean Up the Default Files</strong></p>
<p>Now that your declarative agent is set up, it&rsquo;s time to remove the default files that use local data. Since we will be working with a live API instead, delete the following files from your project directory: </p>
<ul>
<li>
<p>appPackage &gt; apiSpecificationfile &gt; <code>repair.yml</code> </p>
</li>
<li>
<p>appPackage &gt; <code>repairDeclarativeAgent.json</code> </p>
</li>
<li>
<p>src &gt; <code>RepairsData.json</code> </p>
</li>
<li>
<p>src &gt; functions &gt; <code>repairs.ts</code></p>
</li>
</ul>
<p><img alt="Custom Agent- CA2" loading="lazy" src="/Images/blog_images/2025/January/CA-2.png"></p>
<blockquote>
<p>Don’t worry about the files we just deleted. We’re going to create the essential files needed for our Breaking Bad Agent from scratch. Up next, we’ll fetch real-time data directly from the API and configure the agent to use it.</p></blockquote>
<h2 id="step-2-create-a-new-function-file-for-the-breaking-bad-api-integration">Step 2: Create a New Function File for the Breaking Bad API Integration</h2>
<p>Next, we need to create a new file where our agent will interact with the Breaking Bad API. </p>
<ol>
<li>Navigate to: src &gt; functions</li>
<li>Create a New File: Name it <code>quotes.ts</code></li>
</ol>
<p>Now, copy and paste the following code into <code>quotes.ts</code>. This function will act as an Azure Function responsible for sending requests and receiving quotes from the live Breaking Bad API.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-ts" data-lang="ts"><span style="display:flex;"><span><span style="color:#66d9ef">import</span> { <span style="color:#a6e22e">app</span>, <span style="color:#a6e22e">HttpResponseInit</span> } <span style="color:#66d9ef">from</span> <span style="color:#e6db74">&#34;@azure/functions&#34;</span>; 
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">export</span> <span style="color:#66d9ef">async</span> <span style="color:#66d9ef">function</span> <span style="color:#a6e22e">quotes</span>()<span style="color:#f92672">:</span> <span style="color:#a6e22e">Promise</span>&lt;<span style="color:#f92672">HttpResponseInit</span>&gt; { 
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>  <span style="color:#66d9ef">const</span> <span style="color:#a6e22e">breakingbadResponse</span> <span style="color:#f92672">=</span> <span style="color:#66d9ef">await</span> <span style="color:#a6e22e">fetch</span>(<span style="color:#e6db74">&#39;https://api.breakingbadquotes.xyz/v1/quotes&#39;</span>); 
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>  <span style="color:#66d9ef">const</span> <span style="color:#a6e22e">breakingbadData</span> <span style="color:#f92672">=</span> <span style="color:#66d9ef">await</span> <span style="color:#a6e22e">breakingbadResponse</span>.<span style="color:#a6e22e">json</span>(); 
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>  <span style="color:#66d9ef">return</span> { 
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">status</span>: <span style="color:#66d9ef">200</span>, 
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">jsonBody</span><span style="color:#f92672">:</span> { 
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>      <span style="color:#a6e22e">quotes</span>: <span style="color:#66d9ef">breakingbadData</span>, 
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>  }, 
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>  }; 
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>} 
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">app</span>.<span style="color:#a6e22e">http</span>(<span style="color:#e6db74">&#34;quotes&#34;</span>, { 
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>  <span style="color:#a6e22e">methods</span><span style="color:#f92672">:</span> [<span style="color:#e6db74">&#34;GET&#34;</span>], 
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>  <span style="color:#a6e22e">authLevel</span><span style="color:#f92672">:</span> <span style="color:#e6db74">&#34;anonymous&#34;</span>, 
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>  <span style="color:#a6e22e">handler</span>: <span style="color:#66d9ef">quotes</span>, 
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>});
</span></span></code></pre></div><blockquote>
<p>If you encounter the error <code>Cannot find module '@azure/functions' or its corresponding type</code>, don’t worry! This is a common issue, and the required modules will be automatically installed when you run the agent for the first time.</p></blockquote>
<h2 id="step-3-create-the-api-specification-file">Step 3: Create the API Specification File</h2>
<p>Now, let&rsquo;s create the API specification file that will define how the declarative agent interacts with the Breaking Bad API.</p>
<ol>
<li>Navigate to: appPackage &gt; apiSpecificationfile</li>
<li>Create a New File: Name it <code>quotes.yml</code></li>
</ol>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-yml" data-lang="yml"><span style="display:flex;"><span><span style="color:#ae81ff">openapi: 3.0.0 </span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#ae81ff">info: </span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>  <span style="color:#ae81ff">title: Breaking bad Quote API </span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>  <span style="color:#ae81ff">description: Random Quote api from the Breaking Bad TV series with author </span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>  <span style="color:#ae81ff">version: 1.0.0 </span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#ae81ff">servers: </span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>  - <span style="color:#ae81ff">url: ${{OPENAPI_SERVER_URL}}/api </span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#ae81ff">description: The quotes api server </span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#ae81ff">paths: </span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>  <span style="color:#ae81ff">/quotes: </span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#ae81ff">get: </span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>      <span style="color:#ae81ff">operationId: getQuotes </span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>      <span style="color:#ae81ff">summary: get all breaking bad quotes with author </span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>      <span style="color:#ae81ff">description: Returns a single random quote from the Breaking Bad TV series with author </span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>      <span style="color:#ae81ff">responses: </span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>        <span style="color:#e6db74">&#39;200&#39;</span>: 
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>          <span style="color:#ae81ff">description: A random quote from the Breaking Bad TV series with author </span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>          <span style="color:#ae81ff">content: </span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>            <span style="color:#ae81ff">application/json: </span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>              <span style="color:#ae81ff">schema: </span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>                <span style="color:#ae81ff">type: object </span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>                <span style="color:#ae81ff">properties: </span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>                  <span style="color:#ae81ff">results: </span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>                    <span style="color:#ae81ff">type: array </span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>                    <span style="color:#ae81ff">items: </span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>                      <span style="color:#ae81ff">type: object </span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>                      <span style="color:#ae81ff">properties: </span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>                        <span style="color:#ae81ff">quote: </span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>                          <span style="color:#ae81ff">type: string </span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>                          <span style="color:#ae81ff">description: Breaking bad quote with author </span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>                        <span style="color:#ae81ff">author: </span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>                          <span style="color:#ae81ff">type: string </span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>                          <span style="color:#ae81ff">description: The author name of the quote</span>
</span></span></code></pre></div><h2 id="step-4-update-theai-pluginjsonfile">Step 4: Update the ai-plugin.json File</h2>
<p>Now, let&rsquo;s update the ai-plugin.json file so that the declarative agent is properly linked with the Breaking Bad API and the Azure Function. </p>
<ol>
<li>Navigate to: appPackage. </li>
<li>Open the File: Open <code>ai-plugin.json</code></li>
<li>Remove: The <code>&quot;Static_template&quot;</code> entry</li>
<li>Update: The runtimes URL and the function name as shown below:</li>
</ol>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-json" data-lang="json"><span style="display:flex;"><span>{ 
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">&#34;$schema&#34;</span>: <span style="color:#e6db74">&#34;[https://developer.microsoft.com/json-schemas/copilot/plugin/v2.2/schema.json](https://developer.microsoft.com/json-schemas/copilot/plugin/v2.2/schema.json)&#34;</span>, 
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">&#34;schema_version&#34;</span>: <span style="color:#e6db74">&#34;v2.2&#34;</span>, 
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">&#34;namespace&#34;</span>: <span style="color:#e6db74">&#34;Breakingbadquotes&#34;</span>, 
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">&#34;name_for_human&#34;</span>: <span style="color:#e6db74">&#34;Breakingbadquotes Plugin&#34;</span>, 
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">&#34;description_for_human&#34;</span>: <span style="color:#e6db74">&#34;Generate random quotes from the Breaking Bad TV series.&#34;</span>, 
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">&#34;description_for_model&#34;</span>: <span style="color:#e6db74">&#34;Plugin for generating random quotes from the Breaking Bad TV series.&#34;</span>, 
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">&#34;functions&#34;</span>: [ 
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    { 
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">&#34;name&#34;</span>: <span style="color:#e6db74">&#34;getQuotes&#34;</span>, 
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">&#34;description&#34;</span>: <span style="color:#e6db74">&#34;Returns a random quote with author from the Breaking Bad TV series.&#34;</span>, 
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">&#34;capabilities&#34;</span>: { 
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">&#34;response_semantics&#34;</span>: { 
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>          <span style="color:#f92672">&#34;data_path&#34;</span>: <span style="color:#e6db74">&#34;$.quotes&#34;</span>, 
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>          <span style="color:#f92672">&#34;properties&#34;</span>: { 
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>            <span style="color:#f92672">&#34;title&#34;</span>: <span style="color:#e6db74">&#34;$.quote&#34;</span>, 
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>            <span style="color:#f92672">&#34;subtitle&#34;</span>: <span style="color:#e6db74">&#34;$.author&#34;</span> 
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>          }  
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>        } 
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>      }  
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    }     
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>  ], 
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">&#34;runtimes&#34;</span>: [ 
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    { 
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">&#34;type&#34;</span>: <span style="color:#e6db74">&#34;OpenApi&#34;</span>, 
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">&#34;auth&#34;</span>: { 
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">&#34;type&#34;</span>: <span style="color:#e6db74">&#34;None&#34;</span> 
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>      }, 
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">&#34;spec&#34;</span>: { 
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">&#34;url&#34;</span>: <span style="color:#e6db74">&#34;apiSpecificationFile/quotes.yml&#34;</span>, 
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">&#34;progress_style&#34;</span>: <span style="color:#e6db74">&#34;ShowUsageWithInputAndOutput&#34;</span> 
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>      }, 
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">&#34;run_for_functions&#34;</span>: [<span style="color:#e6db74">&#34;getQuotes&#34;</span>] 
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    } 
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>  ], 
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">&#34;capabilities&#34;</span>: { 
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">&#34;conversation_starters&#34;</span>: [] 
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>  } 
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><h2 id="step-5-update-the-manifestjson-file">Step 5: Update the manifest.json File</h2>
<p>The manifest.json file serves as the app manifest for your declarative agent. It is specifying the agent’s identity and the declarative agent configuration files. We&rsquo;ll update it to reflect the Breaking Bad agent setup.</p>
<ol>
<li>Navigate to: appPackage</li>
<li>Open the File: open <code>manifest.json</code></li>
<li>Update: The <code>&quot;copilotAgents&quot;</code> section as shown below:</li>
</ol>
<blockquote>
<p>You can also update the icon of your application in the same file. First, download any image and make sure it&rsquo;s <strong>192x192 pixels</strong>. Replace the existing icon with your new image by saving it as <strong>color.png</strong> in your project folder. If you choose to change the name of the image, make sure to update it in the application <strong>manifest.json</strong> file.</p></blockquote>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-json" data-lang="json"><span style="display:flex;"><span>{ 
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">&#34;copilotAgents&#34;</span>: { 
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">&#34;declarativeAgents&#34;</span>: [ 
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>      { 
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">&#34;id&#34;</span>: <span style="color:#e6db74">&#34;breakingBadDeclarativeAgent&#34;</span>, 
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">&#34;file&#34;</span>: <span style="color:#e6db74">&#34;breakingBadDeclarativeAgent.json&#34;</span> 
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>      } 
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    ] 
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>  } 
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><h2 id="step-6-create-the-quotesdeclarativeagentjson-file">Step 6: Create the quotesDeclarativeAgent.json File</h2>
<p>We need to create the agent manifest file which defines how the declarative agent behaves and interacts with the Breaking Bad API.</p>
<ol>
<li>Navigate to: appPackage</li>
<li>Create a New File: Name it <code>quotesDeclarativeAgent.json</code></li>
</ol>
<p><strong>Add the Following Content:</strong></p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-json" data-lang="json"><span style="display:flex;"><span>{ 
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">&#34;$schema&#34;</span>: <span style="color:#e6db74">&#34;[https://developer.microsoft.com/json-schemas/copilot/declarative-agent/v1.2/schema.json](https://developer.microsoft.com/json-schemas/copilot/declarative-agent/v1.2/schema.json)&#34;</span>, 
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">&#34;version&#34;</span>: <span style="color:#e6db74">&#34;v1.2&#34;</span>, 
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">&#34;name&#34;</span>: <span style="color:#e6db74">&#34;Breaking badquotes&#34;</span>, 
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">&#34;description&#34;</span>: <span style="color:#e6db74">&#34;This declarative agent helps you get a random quote from the Breaking Bad TV series.&#34;</span>, 
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">&#34;instructions&#34;</span>: <span style="color:#e6db74">&#34;$[file(&#39;instruction.txt&#39;)]&#34;</span>, 
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">&#34;conversation_starters&#34;</span>: [ 
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>          { 
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>            <span style="color:#f92672">&#34;text&#34;</span>: <span style="color:#e6db74">&#34;Give me a random quote from Breaking Bad with author name?&#34;</span> 
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>        }    
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    ], 
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">&#34;actions&#34;</span>: [ 
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>        { 
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>            <span style="color:#f92672">&#34;id&#34;</span>: <span style="color:#e6db74">&#34;quoteplugin&#34;</span>, 
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>            <span style="color:#f92672">&#34;file&#34;</span>: <span style="color:#e6db74">&#34;ai-plugin.json&#34;</span> 
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>        } 
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    ] 
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><h2 id="step-7-update-the-instructionstxt-file">Step 7: Update the instructions.txt File</h2>
<p>The <code>instructions.txt</code> file plays a crucial role in declarative agent setup. It defines how the agent should behave, respond to user queries, and interact with the Breaking Bad API. This make sures that the agent stays focused on its purpose and maintains a consistent user experience.</p>
<p><strong>Update it with the Following Content:</strong></p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-txt" data-lang="txt"><span style="display:flex;"><span>You are an assistant specialized in retrieving quotes from the provided API, ensuring a smooth and delightful user experience. Your responses should include the quote and its author. 
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>Guidelines: 
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>- Exclusive API Use: Do not perform web searches; rely solely on the provided API for data retrieval. Do not proceess any other request. This is solely for breaking bad quotes nothing else, keep in mind! 
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>- Response Format: Present the quote followed by the author&#39;s name.  
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>- Error Handling: If the API does not return a quote, respond with: &#34;I&#39;m sorry, I couldn&#39;t retrieve a quote at this moment.&#34; 
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>- User Interaction: Maintain a friendly and engaging tone to ensure a delightful user experience.
</span></span></code></pre></div><h2 id="showcase-the-final-outcome-of-all-efforts">Showcase: The Final Outcome of All Efforts</h2>
<p>To run and debug your declarative agent, click on the <code>Run and Debug</code> icon in VS Code, which can be found on the left sidebar. The process will take approximately <strong>~10-15 minutes</strong> to initialize. During this time, a sign-in prompt will appear where you’ll need to log in with your Microsoft 365 account credentials.</p>
<p><img alt="Custom Agent- CA3" loading="lazy" src="/Images/blog_images/2025/January/CA-3.png"></p>
<blockquote>
<p>Once signed in, if you are not automatically redirected, you can manually visit <a href="https://office.com/chat?auth=2">office.com/chat?auth=2</a> to access the Copilot interface.</p></blockquote>
<p>Select the prompt we defined earlier, and it will prompt you to process the query. Click on <code>Always Allow</code> to proceed.</p>
<p><img alt="Custom Agent- CA4" loading="lazy" src="/Images/blog_images/2025/January/CA-4.png"></p>
<p><strong>Final Output:</strong></p>
<p><img alt="Custom Agent- CA5" loading="lazy" src="/Images/blog_images/2025/January/CA-5.png"></p>
<p><strong>How to Verify if It&rsquo;s Working and Fetching Data from the API</strong></p>
<ul>
<li><strong>Verify via Localhost:</strong> Open the terminal in <strong>VS Code</strong> and click on <strong>Start Backend</strong>. Once it&rsquo;s running, you&rsquo;ll see the <strong>local host URL</strong>, which should begin with <code>localhost:7071/api/quotes</code>. Open this URL in your browser, and it will return a JSON response with the quote data.</li>
<li><strong>Verify via Agent Request:</strong> Another way to check is by sending a request from the agent. This will invoke the <code>getQuote</code> function, which generates a unique ID each time. The response will include text indicating that the function was programmatically called.</li>
</ul>
<h2 id="bonus-how-does-the-breaking-bad-quotes-agent-work">Bonus: How does the breaking bad quotes agent work?</h2>
<p>When a user requests a Breaking Bad quote in Microsoft 365 Copilot, the agent works in a few simple steps: </p>
<ol>
<li>Manifest Check: The agent starts by checking its task list in the manifest file</li>
<li>API Connection: It then refers to the API specification to understand how to connect to the Breaking Bad API</li>
<li>Data Fetch: The agent sends a request to an Azure Function, which fetches a random quote and the author’s name from the API</li>
<li>Data Return: The quote and author are returned in a structured JSON format</li>
<li>User Display: Finally, the agent displays the quote clearly in the Copilot interface</li>
</ol>
<h2 id="conclusion">Conclusion</h2>
<p>It’s been a bit of a ride, hasn’t it? But hey, we’ve finally done it! We have successfully created a declarative agent using the Breaking Bad API, but the process can be applied to any <strong>API</strong>, including those requiring <strong>authentication</strong> with API key or OAuth.</p>
<p>if you hit any bumps along the way, don’t hesitate to drop a comment below. If you found this helpful, give it a thumbs up or click on your favorite emoji button below. Good luck, and happy coding!</p>
<p><strong>Appendix</strong></p>
<p>This post is inspired by the following resources:</p>
<ul>
<li><a href="https://techcommunity.microsoft.com/blog/microsoft365copilotblog/introducing-new-agents-in-microsoft-365/4296918?utm_source=chatgpt.com">Introducing New Agents in Microsoft 365</a></li>
<li><a href="https://microsoft.github.io/TechExcel-Extending-Copilot-for-Microsoft365/">Extending Copilot for Microsoft 365</a></li>
<li><a href="https://www.voitanos.io/workshop-microsoft-365-copilot-build-declarative-agents/">Workshop on Building Declarative Agents for Microsoft 365 Copilot</a></li>
<li><a href="https://breakingbadquotes.xyz/">Breaking Bad Quotes API</a></li>
<li><a href="https://icons8.com/icon/21732/breaking-bad/">Breaking Bad Icon by Icons8</a></li>
</ul>
]]></content:encoded>
    </item>
    <item>
      <title>How to Use PowerShell Start-Job for Background Tasks</title>
      <link>https://niravraval.com/blog/2024/december/how-to-use-powershell-start-job-for-background-tasks/</link>
      <pubDate>Wed, 18 Dec 2024 00:00:00 +0000</pubDate>
      <guid>https://niravraval.com/blog/2024/december/how-to-use-powershell-start-job-for-background-tasks/</guid>
      <description>&lt;p&gt;PowerShell is one of my favorite tool when it comes to automation. It has lot of functionalities and one of its cool features is &lt;code&gt;Start-Job&lt;/code&gt;. This will let you run tasks in the background while you keep using PowerShell for other things.&lt;/p&gt;
&lt;h2 id=&#34;start-job-is-ideal-to-use-when-&#34;&gt;Start-Job is ideal to use when &amp;hellip;&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;Tasks take a long time to complete&lt;/li&gt;
&lt;li&gt;You want to avoid module conflicts by isolating them in separate jobs&lt;/li&gt;
&lt;li&gt;You need to resolve assembly conflicts by running jobs in isolated processes&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&#34;how-to-use-start-job&#34;&gt;How to Use &lt;code&gt;Start-Job&lt;/code&gt;&lt;/h2&gt;
&lt;p&gt;I will try to explain this with a simple example below.&lt;/p&gt;</description>
      <content:encoded><![CDATA[<p>PowerShell is one of my favorite tool when it comes to automation. It has lot of functionalities and one of its cool features is <code>Start-Job</code>. This will let you run tasks in the background while you keep using PowerShell for other things.</p>
<h2 id="start-job-is-ideal-to-use-when-">Start-Job is ideal to use when &hellip;</h2>
<ul>
<li>Tasks take a long time to complete</li>
<li>You want to avoid module conflicts by isolating them in separate jobs</li>
<li>You need to resolve assembly conflicts by running jobs in isolated processes</li>
</ul>
<h2 id="how-to-use-start-job">How to Use <code>Start-Job</code></h2>
<p>I will try to explain this with a simple example below.</p>
<h3 id="example--displaying-the-date-and-time-using-get-date">Example : Displaying the Date and Time using Get-Date</h3>
<p>Below example shows how to run a background job in PowerShell to get the current date and time. It uses <code>Start-Job</code> to begin the job, waits for it with <code>Wait-Job</code>, gets the result using <code>Receive-Job</code>, and cleans up with <code>Remove-Job</code>.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-ps" data-lang="ps"><span style="display:flex;"><span><span style="color:#a6e22e">#</span> <span style="color:#a6e22e">Start</span> <span style="color:#a6e22e">a</span> <span style="color:#a6e22e">background</span> <span style="color:#a6e22e">job</span> <span style="color:#a6e22e">to</span> <span style="color:#a6e22e">display</span> <span style="color:#a6e22e">the</span> <span style="color:#a6e22e">current</span> <span style="color:#a6e22e">date</span> <span style="color:#a6e22e">and</span> <span style="color:#a6e22e">time</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">$job</span> <span style="color:#a6e22e">=</span> <span style="color:#a6e22e">Start-Job</span> <span style="color:#a6e22e">-ScriptBlock</span> {
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">Get-Date</span> 
</span></span><span style="display:flex;"><span>}
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">#</span> <span style="color:#a6e22e">Check</span> <span style="color:#a6e22e">if</span> <span style="color:#a6e22e">the</span> <span style="color:#a6e22e">job</span> <span style="color:#a6e22e">is</span> <span style="color:#a6e22e">complete</span>
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">Write-Output</span> <span style="color:#a6e22e">&#34;Job</span> <span style="color:#a6e22e">ID:</span> <span style="color:#a6e22e">$</span><span style="color:#e6db74">($job.Id)</span> <span style="color:#a6e22e">started.&#34;</span>
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">Wait-Job</span> <span style="color:#a6e22e">-Job</span> <span style="color:#a6e22e">$job</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">#</span> <span style="color:#a6e22e">Get</span> <span style="color:#a6e22e">the</span> <span style="color:#a6e22e">result</span> <span style="color:#a6e22e">of</span> <span style="color:#a6e22e">the</span> <span style="color:#a6e22e">job</span>
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">$jobResult</span> <span style="color:#a6e22e">=</span> <span style="color:#a6e22e">Receive-Job</span> <span style="color:#a6e22e">-Job</span> <span style="color:#a6e22e">$job</span>
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">Write-Output</span> <span style="color:#a6e22e">&#34;The</span> <span style="color:#a6e22e">current</span> <span style="color:#a6e22e">date</span> <span style="color:#a6e22e">and</span> <span style="color:#a6e22e">time</span> <span style="color:#a6e22e">is:</span> <span style="color:#a6e22e">$jobResult&#34;</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">#</span> <span style="color:#a6e22e">Clean</span> <span style="color:#a6e22e">up</span>
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">Remove-Job</span> <span style="color:#a6e22e">-Job</span> <span style="color:#a6e22e">$job</span>
</span></span></code></pre></div><h2 id="output">Output</h2>
<p><img alt="PS_Start_Job" loading="lazy" src="/Images/blog_images/PS_Start_Job.jpg"></p>
<p>It’s quite simple, isn’t it? Just make sure to use <code>Remove-Job</code> to clean up after your job is done. If you skip this step, it can lead to duplication or conflicts with other jobs.</p>
<h3 id="why-i-recommend-to-use-jobs">Why I recommend to use Jobs?</h3>
<p>Numerous reasons but, the major reason is that some PowerShell modules can <a href="https://github.com/pnp/powershell/issues/3637">conflict</a> when loaded together in the same session. For example, Microsoft Graph and PnP PowerShell might have overlapping dependencies. Running these modules or assemblies in separate jobs make sures that they operate independently without issues.</p>
<p>Try it out and let me know how you use it in your own scripts!</p>
<hr>
]]></content:encoded>
    </item>
    <item>
      <title>forEach vs for Loops for Async Operations in JavaScript</title>
      <link>https://niravraval.com/blog/2024/november/foreach-vs-for-loops-for-async-operations-in-javascript/</link>
      <pubDate>Fri, 29 Nov 2024 00:00:00 +0000</pubDate>
      <guid>https://niravraval.com/blog/2024/november/foreach-vs-for-loops-for-async-operations-in-javascript/</guid>
      <description>&lt;p&gt;Well, today I learnt an important lesson. 🤔&lt;/p&gt;
&lt;h1 id=&#34;the-problem&#34;&gt;The Problem&lt;/h1&gt;
&lt;p&gt;In a recent coding effort, I found myself faced with the classic choice between JavaScript&amp;rsquo;s forEach and for&amp;hellip; loops. As I was iterating through an array of data, I realized that my code required asynchronous operations.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Async/Await in forEach&lt;/strong&gt;: Initially, I opted for the simplicity of forEach, but soon hit a roadblock. Despite my attempts to use async/await within the loop, I encountered unexpected behaviour. The asynchronous nature of forEach proved to be a block which led to unhandled promises and unpredictable results. Here&amp;rsquo;s what I was using:&lt;/p&gt;</description>
      <content:encoded><![CDATA[<p>Well, today I learnt an important lesson. 🤔</p>
<h1 id="the-problem">The Problem</h1>
<p>In a recent coding effort, I found myself faced with the classic choice between JavaScript&rsquo;s forEach and for&hellip; loops. As I was iterating through an array of data, I realized that my code required asynchronous operations.</p>
<p><strong>Async/Await in forEach</strong>: Initially, I opted for the simplicity of forEach, but soon hit a roadblock. Despite my attempts to use async/await within the loop, I encountered unexpected behaviour. The asynchronous nature of forEach proved to be a block which led to unhandled promises and unpredictable results. Here&rsquo;s what I was using:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-js" data-lang="js"><span style="display:flex;"><span><span style="color:#75715e">// Using forEach (causes issues with async/await)
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span><span style="color:#a6e22e">dataArray</span>.<span style="color:#a6e22e">forEach</span>(<span style="color:#66d9ef">async</span> (<span style="color:#a6e22e">item</span>) =&gt; {
</span></span><span style="display:flex;"><span>  <span style="color:#66d9ef">await</span> <span style="color:#a6e22e">processItem</span>(<span style="color:#a6e22e">item</span>); <span style="color:#75715e">// This won&#39;t behave as expected
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>});
</span></span></code></pre></div><h1 id="the-solution">The solution</h1>
<p>The solution is simple. Use for loop not <strong>foreach</strong> (of course when doing asynchronous operations).</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-js" data-lang="js"><span style="display:flex;"><span><span style="color:#75715e">// Using for loop (structured and reliable)
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span><span style="color:#66d9ef">for</span> (<span style="color:#66d9ef">const</span> <span style="color:#a6e22e">item</span> <span style="color:#66d9ef">of</span> <span style="color:#a6e22e">dataArray</span>) {
</span></span><span style="display:flex;"><span>  <span style="color:#66d9ef">await</span> <span style="color:#a6e22e">processItem</span>(<span style="color:#a6e22e">item</span>); <span style="color:#75715e">// Awaits completion before moving to the next item
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>}
</span></span></code></pre></div><p>No rocket science right? Now, why did I choose for loop? 🎬 the answer is simple, I found relief in its structured approach to asynchronous iteration. With async/await, each loop iteration awaited the completion of asynchronous tasks.</p>
]]></content:encoded>
    </item>
    <item>
      <title>About me</title>
      <link>https://niravraval.com/about/</link>
      <pubDate>Mon, 01 Jan 0001 00:00:00 +0000</pubDate>
      <guid>https://niravraval.com/about/</guid>
      <description>&lt;img src=&#34;https://niravraval.com/Images/About_me/Nirav_about.jpg&#34; alt=&#34; &#34; style=&#34;float: left; margin-right: 20px; width: 50%;&#34; 
/&gt;

&lt;p&gt;Hi, I’m Nirav. I’m a Developer focused on the Microsoft 365 domain, and I work at &lt;a href=&#34;https://www.creospark.com&#34;&gt;&lt;strong&gt;Creospark&lt;/strong&gt;&lt;/a&gt; Consulting firm located in Toronto, Canada. In addition to working on development and consultation projects, I’m an active contributor to the &lt;a href=&#34;https://pnp.github.io/&#34;&gt;Microsoft 365 and Power Platform community&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;I’ve been working with Microsoft technologies for around 4 years. I’ve started adopting multiple technologies throughout my career. My areas of expertise include &lt;strong&gt;SharePoint Framework (SPFx)&lt;/strong&gt;, &lt;strong&gt;React&lt;/strong&gt;, &lt;strong&gt;PowerShell&lt;/strong&gt;, &lt;strong&gt;Azure DevOps&lt;/strong&gt;, &lt;strong&gt;PowerApps&lt;/strong&gt; and &lt;strong&gt;Robotic Process Automation&lt;/strong&gt; with Power Platform.&lt;/p&gt;</description>
      <content:encoded><![CDATA[<img src="/Images/About_me/Nirav_about.jpg" alt=" " style="float: left; margin-right: 20px; width: 50%;" 
/>

<p>Hi, I’m Nirav. I’m a Developer focused on the Microsoft 365 domain, and I work at <a href="https://www.creospark.com"><strong>Creospark</strong></a> Consulting firm located in Toronto, Canada. In addition to working on development and consultation projects, I’m an active contributor to the <a href="https://pnp.github.io/">Microsoft 365 and Power Platform community</a>.</p>
<p>I’ve been working with Microsoft technologies for around 4 years. I’ve started adopting multiple technologies throughout my career. My areas of expertise include <strong>SharePoint Framework (SPFx)</strong>, <strong>React</strong>, <strong>PowerShell</strong>, <strong>Azure DevOps</strong>, <strong>PowerApps</strong> and <strong>Robotic Process Automation</strong> with Power Platform.</p>
<p>I hold the following certifications:</p>
<ul>
<li><a href="https://learn.microsoft.com/en-us/users/nrvrvl/credentials/5b602095cbfba715?ref=https%3A%2F%2Fwww.linkedin.com%2F">Microsoft Certified: Power Platform Functional Consultant Associate</a></li>
<li><a href="https://learn.microsoft.com/en-us/users/nrvrvl/credentials/66B6FA2C51333F2E">Microsoft Certified: Power Platform App Maker Associate</a></li>
<li><a href="https://learn.microsoft.com/api/credentials/share/en-us/nrvrvl/E489BAC41388B8C?sharingId">Microsoft Certified : Create and Manage Canvas Apps with Power Apps</a></li>
<li><a href="https://learn.microsoft.com/api/credentials/share/en-us/nrvrvl/5ab8d05d4018b40d?sharingId">Microsoft Certified : Implement AI models with Microsoft Power Platform AI Builder</a></li>
<li><a href="https://learn.microsoft.com/api/credentials/share/en-us/nrvrvl/BDC078CFACACCD79?sharingId">Microsoft 365 Certified: Teams Administrator Associate</a></li>
<li><a href="https://learn.microsoft.com/api/credentials/share/en-us/nrvrvl/241C9876DD963464">Microsoft 365 Certified: Fundamentals</a></li>
<li><a href="https://www.credly.com/badges/173b861e-c889-40ae-a3ff-eae59701c303?source=linked_in_profile">Microsoft Certified: Azure Fundamentals</a></li>
<li><a href="https://learn.microsoft.com/api/credentials/share/en-ca/nrvrvl/B023BD34DDE9A11F?sharingId=F70DBDD87F942163">Microsoft Certified: Azure AI Fundamentals</a></li>
<li><a href="https://www.youracclaim.com/badges/79b37252-59b1-4a1d-8854-3d19dce16362?source=linked_in_profile">Microsoft Certified : Power Platform Fundamentals</a></li>
<li><a href="https://learn.microsoft.com/api/credentials/share/en-us/nrvrvl/F28485F2A0F4BF76?sharingId">Microsoft Certified : Create and manage automated processes by using Power Automate</a></li>
</ul>
<p><img src="/Images/About_me/Badge_M365Contributor2026.png" alt="Badge_M365Contributor2026" style="display: inline-block;
width: 25%; " />

<style>
  :root {
    --theme: #f5f5f5;
  }
</style>

<img src="/Images/About_me/Badge_M365Contributor2025.png" alt="Badge_M365Contributor2025" style="display: inline-block;
width: 25%; " />

<style>
  :root {
    --theme: #f5f5f5;
  }
</style>

<img src="/Images/About_me/Badge_Associate.png" alt="Associate Badge" style="display: inline-block;
width: 25%; " />

<style>
  :root {
    --theme: #f5f5f5;
  }
</style>

<img src="/Images/About_me/Badge_Fundamental.png" alt="Badge_Fundamental" style="display: inline-block;
width: 25%; " />

<style>
  :root {
    --theme: #f5f5f5;
  }
</style>
</p>
<p><img src="/Images/About_me/Badge_SPFxContributor.png" alt="Badge_SPFxContributor" style="display: inline-block;
width: 25%; " />

<style>
  :root {
    --theme: #f5f5f5;
  }
</style>

<img src="/Images/About_me/Badge_Hacktoberfest.png" alt="Badge_Hacktoberfest" style="display: inline-block;
width: 25%; " />

<style>
  :root {
    --theme: #f5f5f5;
  }
</style>

<img src="/Images/About_me/Badge_spfxToolkit.png" alt="Badge_spfxToolkit" style="display: inline-block;
width: 25%; " />

<style>
  :root {
    --theme: #f5f5f5;
  }
</style>

<img src="/Images/About_me/Badge_Copilot_Recruit.png" alt="Badge_Copilot_Recruit" style="display: inline-block;
width: 25%; " />

<style>
  :root {
    --theme: #f5f5f5;
  }
</style>
</p>
<p>For a long time, I’ve had the thought of creating a blog to share knowledge and help others. I’m building this space to discuss the problems I solve. While it’s primarily focused on the Microsoft ecosystem, I’ll also share tips and tricks that I find helpful across other areas.</p>
<p>Enjoy! 😺</p>
<p>Nirav</p>
]]></content:encoded>
    </item>
    <item>
      <title>Contributions</title>
      <link>https://niravraval.com/contributions/</link>
      <pubDate>Mon, 01 Jan 0001 00:00:00 +0000</pubDate>
      <guid>https://niravraval.com/contributions/</guid>
      <description>&lt;div class=&#34;table-container&#34;&gt;
  &lt;div class=&#34;search-filter-wrapper&#34;&gt;
    &lt;input
      type=&#34;text&#34;
      id=&#34;search-input&#34;
      placeholder=&#34;Search...&#34;
      class=&#34;search-input&#34;
    /&gt;

    &lt;div
      class=&#34;toggle-group&#34;
      role=&#34;radiogroup&#34;
      aria-label=&#34;Filter Pull Requests&#34;
    &gt;
      &lt;button
        type=&#34;button&#34;
        id=&#34;closed-filter&#34;
        class=&#34;toggle-btn active&#34;
        aria-pressed=&#34;true&#34;
      &gt;
        Closed
      &lt;/button&gt;
      &lt;button
        type=&#34;button&#34;
        id=&#34;open-filter&#34;
        class=&#34;toggle-btn&#34;
        aria-pressed=&#34;false&#34;
      &gt;
        Open
      &lt;/button&gt;
    &lt;/div&gt;
  &lt;/div&gt;

  &lt;table
    id=&#34;contrib-table&#34;
    aria-live=&#34;polite&#34;
    aria-relevant=&#34;all&#34;
    aria-label=&#34;GitHub Pull Requests&#34;
  &gt;
    &lt;thead&gt;
      &lt;tr&gt;
        &lt;th style=&#34;width: 40%&#34;&gt;Repository Name&lt;/th&gt;
        &lt;th style=&#34;width: 30%&#34;&gt;Issue Title&lt;/th&gt;
        &lt;th style=&#34;width: 20%&#34;&gt;Date&lt;/th&gt;
        &lt;th style=&#34;width: 10%&#34;&gt;&lt;/th&gt;
      &lt;/tr&gt;
    &lt;/thead&gt;
    &lt;tbody&gt;&lt;/tbody&gt;
  &lt;/table&gt;

  &lt;div
    id=&#34;loading-indicator&#34;
    class=&#34;loading-indicator&#34;
    aria-live=&#34;assertive&#34;
    aria-atomic=&#34;true&#34;
    hidden
  &gt;
    Loading PRs...
  &lt;/div&gt;

  &lt;div class=&#34;github-source-info&#34; style=&#34;text-align: center; margin-top: 20px&#34;&gt;
    (Live from
    &lt;a
      href=&#34;https://github.com/nirav-raval&#34;
      target=&#34;_blank&#34;
      rel=&#34;noopener noreferrer&#34;
      &gt;GitHub&lt;/a
    &gt;
    using
    &lt;a
      href=&#34;https://docs.github.com/en/rest/about-the-rest-api/about-the-rest-api?apiVersion=2022-11-28&#34;
      target=&#34;_blank&#34;
      rel=&#34;noopener noreferrer&#34;
      &gt;GitHub API&lt;/a
    &gt;)
  &lt;/div&gt;
  &lt;div class=&#34;pagination&#34;&gt;
    &lt;button id=&#34;prev-btn&#34; class=&#34;pagination-button&#34; aria-label=&#34;Previous page&#34;&gt;
      Previous
    &lt;/button&gt;
    &lt;span id=&#34;page-info&#34; class=&#34;page-info&#34; aria-live=&#34;polite&#34; aria-atomic=&#34;true&#34;
      &gt;Page 1&lt;/span
    &gt;
    &lt;button id=&#34;next-btn&#34; class=&#34;pagination-button&#34; aria-label=&#34;Next page&#34;&gt;
      Next
    &lt;/button&gt;
  &lt;/div&gt;
&lt;/div&gt;

&lt;style&gt;
  .table-container {
    --c-primary: var(--primary);
    --c-primary-hover: var(--secondary);
    --c-theme: var(--theme);
    --c-border: var(--border);
    --c-code-bg: var(--tertiary);
    --c-accent: var(--primary);
    --c-accent-light: var(--entry);
    
    margin: 40px 10px 10px 10px;
    color: var(--c-primary);
  }

   
  .search-filter-wrapper {
    display: flex;
    flex-wrap: wrap;
    gap: 15px;
    align-items: center;
    margin-bottom: 20px;
  }

  .search-input {
    flex-grow: 1;
    min-width: 200px;
    padding: 10px 14px;
    border: 1.5px solid var(--c-border);
    border-radius: 6px;
    font-size: 1rem;
    background-color: var(--c-theme);
    color: var(--c-primary);
    transition: border-color 0.3s ease;
  }

  .search-input:focus {
    outline: none;
    border-color: var(--c-accent);
    box-shadow: 0 0 6px var(--c-accent-light);
  }

  .toggle-group {
    display: flex;
    border: 1.5px solid var(--c-border);
    border-radius: 25px;
    background-color: var(--c-theme);
    overflow: hidden;
    user-select: none;
  }

  .toggle-btn {
    cursor: pointer;
    padding: 8px 22px;
    border: none;
    background-color: transparent;
    font-weight: 600;
    font-size: 0.9rem;
    color: var(--c-primary);
    transition: background-color 0.3s ease, color 0.3s ease;
    border-radius: 0;
    flex: 1 1 auto;
  }

  .toggle-btn:not(:last-child) {
    border-right: 1.5px solid var(--c-border);
  }

  .toggle-btn:hover,
  .toggle-btn:focus-visible {
    background-color: var(--c-accent-light);
    color: var(--c-accent);
    outline: none;
  }

  .toggle-btn.active {
    background-color: var(--c-accent);
    color: var(--c-theme);
    cursor: default;
  }

   
  table {
    width: 100%;
    border-collapse: collapse;
    table-layout: fixed;
    background-color: var(--c-theme);
    box-shadow: var(--card-shadow);  
    border-radius: 12px;
    overflow: hidden;
    transition: opacity 0.3s ease;
  }

  th,
  td {
    padding: 1rem;
    text-align: left;
    border-bottom: 1px solid var(--c-border);
  }

  th {
    font-weight: 600;
    text-transform: capitalize;
    letter-spacing: 0.06rem;
    background-color: var(--c-code-bg);
    color: var(--c-primary);
  }

  td {
    font-size: 0.95rem;
    color: var(--c-primary);
    word-break: break-word;
  }

  td a {
    text-decoration: none;
  }

  td a:hover,
  td a:focus-visible {
    text-decoration: none;
    outline: none;
  }

   
  .loading-indicator {
    text-align: center;
    font-style: italic;
    color: var(--c-accent);
    margin-top: 10px;
    font-weight: 600;
  }

   
  .pagination {
    display: flex;
    justify-content: center;
    align-items: center;
    margin-top: 25px;
    gap: 15px;
  }

  .pagination-button {
    padding: 10px 24px;
    border: none;
    border-radius: 9999px;  
    background-color: var(--c-primary);
    color: var(--c-theme);
    cursor: pointer;
    font-weight: 600;
    font-size: 0.9rem;
    transition: background-color 0.2s ease, transform 0.2s ease;
  }

  .pagination-button:hover:not(:disabled),
  .pagination-button:focus-visible:not(:disabled) {
    background-color: var(--c-primary-hover);
    transform: scale(1.05);
    outline: none;
  }

  .pagination-button:disabled {
    opacity: 0.5;
    cursor: not-allowed;
  }

  .page-info {
    font-weight: 600;
    font-size: 1rem;
    min-width: 100px;
    text-align: center;
  }

   
  @media (max-width: 600px) {
    .search-filter-wrapper {
      flex-direction: column;
      align-items: stretch;
    }

    .toggle-group {
      width: 100%;
      border-radius: 6px;
    }

    .toggle-btn {
      font-size: 1rem;
      padding: 12px;
    }

    table,
    thead,
    tbody,
    th,
    td,
    tr {
      display: block;
    }

    thead {
      display: none;
    }

    tr {
      margin-bottom: 1.25rem;
      border: 1px solid var(--c-border);
      border-radius: 12px;
      overflow: hidden;
    }

    td {
      display: flex;
      justify-content: space-between;
      padding: 0.75rem 1rem;
      border-bottom: 1px solid var(--c-border);
    }

    td::before {
      content: attr(data-label);
      font-weight: 600;
      color: var(--c-primary);
      flex-basis: 40%;
    }
  }

   
  #page-info:empty {
    font-style: italic;
    color: var(--c-border);
  }
&lt;/style&gt;

&lt;script&gt;
  let allItems = []; 
  let openItems = []; 
  let currentItems = [];
  let currentPage = 1;
  const itemsPerPage = 5;

  const username = &#34;nirav-raval&#34;;
  const tableBody = document.querySelector(&#34;#contrib-table tbody&#34;);
  const searchInput = document.getElementById(&#34;search-input&#34;);
  const prevButton = document.getElementById(&#34;prev-btn&#34;);
  const nextButton = document.getElementById(&#34;next-btn&#34;);
  const pageInfo = document.getElementById(&#34;page-info&#34;);

  const closedFilterBtn = document.getElementById(&#34;closed-filter&#34;);
  const openFilterBtn = document.getElementById(&#34;open-filter&#34;);
  const loadingIndicator = document.getElementById(&#34;loading-indicator&#34;);

  const ignoreList = [496, 3, 601, 598, 597, 602, 599, 7018];

  function setLoading(isLoading) {
    loadingIndicator.hidden = !isLoading;

    document.getElementById(&#34;contrib-table&#34;).style.opacity = isLoading
      ? &#34;0.5&#34;
      : &#34;1&#34;;
  }

  async function fetchClosedPRs(page = 1) {
    setLoading(true);
    const res = await fetch(
      `https://api.github.com/search/issues?q=author:${username}+type:pr+is:closed&amp;per_page=100&amp;page=${page}`
    );
    const data = await res.json();

    const filteredItems = data.items.filter(
      (pr) =&gt; !ignoreList.includes(pr.number)
    );
    allItems = allItems.concat(filteredItems);

    if (data.items.length === 100) {
      await fetchClosedPRs(page + 1);
    } else {
      setLoading(false);
      updateCombinedItems();
    }
  }

  async function fetchOpenPRs(page = 1) {
    setLoading(true);
    const res = await fetch(
      `https://api.github.com/search/issues?q=author:${username}+type:pr+is:open&amp;per_page=100&amp;page=${page}`
    );
    const data = await res.json();

    const filteredItems = data.items.filter(
      (pr) =&gt; !ignoreList.includes(pr.number)
    );
    openItems = openItems.concat(filteredItems);

    if (data.items.length === 100) {
      await fetchOpenPRs(page + 1);
    } else {
      setLoading(false);
      updateCombinedItems();
    }
  }

  function updateCombinedItems() {
    currentItems = [];

    if (closedFilterBtn.classList.contains(&#34;active&#34;)) {
      currentItems = currentItems.concat(allItems);
    }
    if (openFilterBtn.classList.contains(&#34;active&#34;)) {
      currentItems = currentItems.concat(openItems);
    }

    const searchTerm = searchInput.value.toLowerCase();
    if (searchTerm) {
      currentItems = currentItems.filter(
        (pr) =&gt;
          pr.title.toLowerCase().includes(searchTerm) ||
          pr.repository_url.toLowerCase().includes(searchTerm)
      );
    }

    currentItems.sort((a, b) =&gt; {
      const dateA = a.closed_at
        ? Date.parse(a.closed_at)
        : Date.parse(a.updated_at);
      const dateB = b.closed_at
        ? Date.parse(b.closed_at)
        : Date.parse(b.updated_at);
      return dateB - dateA;
    });

    currentPage = 1;
    displaySortedPRs();
  }

  function formatDate(dateStr) {
    const date = new Date(dateStr);
    return date
      .toLocaleDateString(&#34;en-GB&#34;, {
        day: &#34;2-digit&#34;,
        month: &#34;short&#34;,
        year: &#34;2-digit&#34;,
      })
      .replace(/ /g, &#34;-&#34;);
  }

  function displaySortedPRs() {
    const startIndex = (currentPage - 1) * itemsPerPage;
    const endIndex = Math.min(startIndex + itemsPerPage, currentItems.length);
    const displayedItems = currentItems.slice(startIndex, endIndex);

    tableBody.innerHTML = &#34;&#34;;

    displayedItems.forEach((pr) =&gt; {
      const repoParts = pr.repository_url.split(&#34;/&#34;).slice(-2);
      const repoOwner = repoParts[0];
      const repoName = repoParts[1];
      if (repoOwner.toLowerCase() === username.toLowerCase()) return;

      const repoPath = `${repoOwner}/${repoName}`;
      const repoUrl = `https://github.com/${repoPath}`;
      const title = pr.title;
      const dateToShow = pr.closed_at ? pr.closed_at : pr.created_at;
      const closedDate = formatDate(dateToShow);

      const link = pr.html_url;

      const row = document.createElement(&#34;tr&#34;);
      row.innerHTML = `
        &lt;td data-label=&#34;Repository&#34;&gt;&lt;a href=&#34;${repoUrl}&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34;&gt;${repoPath}&lt;/a&gt;&lt;/td&gt;
        &lt;td data-label=&#34;Title&#34;&gt;${title}&lt;/td&gt;
        &lt;td data-label=&#34;Closed Date&#34;&gt;${closedDate}&lt;/td&gt;
        &lt;td data-label=&#34;Link&#34;&gt;&lt;a href=&#34;${link}&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34;&gt;View PR&lt;/a&gt;&lt;/td&gt;
      `;
      tableBody.appendChild(row);
    });

    if (currentItems.length === 0) {
      pageInfo.innerText = &#34;No results&#34;;
    } else {
      const totalPages = Math.ceil(currentItems.length / itemsPerPage);
      pageInfo.innerText = `Page ${currentPage} / ${totalPages}`;
    }
    prevButton.disabled = currentPage === 1;
    nextButton.disabled = endIndex &gt;= currentItems.length;
  }

  function setActiveFilter(selectedBtn) {
    
    closedFilterBtn.classList.remove(&#34;active&#34;);
    closedFilterBtn.setAttribute(&#34;aria-pressed&#34;, &#34;false&#34;);
    openFilterBtn.classList.remove(&#34;active&#34;);
    openFilterBtn.setAttribute(&#34;aria-pressed&#34;, &#34;false&#34;);

    
    selectedBtn.classList.add(&#34;active&#34;);
    selectedBtn.setAttribute(&#34;aria-pressed&#34;, &#34;true&#34;);
  }

  closedFilterBtn.addEventListener(&#34;click&#34;, () =&gt; {
    if (closedFilterBtn.classList.contains(&#34;active&#34;)) return; 
    setActiveFilter(closedFilterBtn);
    updateCombinedItems();
  });

  openFilterBtn.addEventListener(&#34;click&#34;, () =&gt; {
    if (openFilterBtn.classList.contains(&#34;active&#34;)) return; 
    setActiveFilter(openFilterBtn);
    if (openItems.length === 0) {
      fetchOpenPRs();
    } else {
      updateCombinedItems();
    }
  });

  searchInput.addEventListener(&#34;input&#34;, () =&gt; {
    updateCombinedItems();
  });

  prevButton.addEventListener(&#34;click&#34;, () =&gt; {
    if (currentPage &gt; 1) {
      currentPage -= 1;
      displaySortedPRs();
    }
  });

  nextButton.addEventListener(&#34;click&#34;, () =&gt; {
    if (currentPage * itemsPerPage &lt; currentItems.length) {
      currentPage += 1;
      displaySortedPRs();
    }
  });

  
  fetchClosedPRs();
&lt;/script&gt;</description>
      <content:encoded><![CDATA[<div class="table-container">
  <div class="search-filter-wrapper">
    <input
      type="text"
      id="search-input"
      placeholder="Search..."
      class="search-input"
    />

    <div
      class="toggle-group"
      role="radiogroup"
      aria-label="Filter Pull Requests"
    >
      <button
        type="button"
        id="closed-filter"
        class="toggle-btn active"
        aria-pressed="true"
      >
        Closed
      </button>
      <button
        type="button"
        id="open-filter"
        class="toggle-btn"
        aria-pressed="false"
      >
        Open
      </button>
    </div>
  </div>

  <table
    id="contrib-table"
    aria-live="polite"
    aria-relevant="all"
    aria-label="GitHub Pull Requests"
  >
    <thead>
      <tr>
        <th style="width: 40%">Repository Name</th>
        <th style="width: 30%">Issue Title</th>
        <th style="width: 20%">Date</th>
        <th style="width: 10%"></th>
      </tr>
    </thead>
    <tbody></tbody>
  </table>

  <div
    id="loading-indicator"
    class="loading-indicator"
    aria-live="assertive"
    aria-atomic="true"
    hidden
  >
    Loading PRs...
  </div>

  <div class="github-source-info" style="text-align: center; margin-top: 20px">
    (Live from
    <a
      href="https://github.com/nirav-raval"
      target="_blank"
      rel="noopener noreferrer"
      >GitHub</a
    >
    using
    <a
      href="https://docs.github.com/en/rest/about-the-rest-api/about-the-rest-api?apiVersion=2022-11-28"
      target="_blank"
      rel="noopener noreferrer"
      >GitHub API</a
    >)
  </div>
  <div class="pagination">
    <button id="prev-btn" class="pagination-button" aria-label="Previous page">
      Previous
    </button>
    <span id="page-info" class="page-info" aria-live="polite" aria-atomic="true"
      >Page 1</span
    >
    <button id="next-btn" class="pagination-button" aria-label="Next page">
      Next
    </button>
  </div>
</div>

<style>
  .table-container {
    --c-primary: var(--primary);
    --c-primary-hover: var(--secondary);
    --c-theme: var(--theme);
    --c-border: var(--border);
    --c-code-bg: var(--tertiary);
    --c-accent: var(--primary);
    --c-accent-light: var(--entry);
    
    margin: 40px 10px 10px 10px;
    color: var(--c-primary);
  }

   
  .search-filter-wrapper {
    display: flex;
    flex-wrap: wrap;
    gap: 15px;
    align-items: center;
    margin-bottom: 20px;
  }

  .search-input {
    flex-grow: 1;
    min-width: 200px;
    padding: 10px 14px;
    border: 1.5px solid var(--c-border);
    border-radius: 6px;
    font-size: 1rem;
    background-color: var(--c-theme);
    color: var(--c-primary);
    transition: border-color 0.3s ease;
  }

  .search-input:focus {
    outline: none;
    border-color: var(--c-accent);
    box-shadow: 0 0 6px var(--c-accent-light);
  }

  .toggle-group {
    display: flex;
    border: 1.5px solid var(--c-border);
    border-radius: 25px;
    background-color: var(--c-theme);
    overflow: hidden;
    user-select: none;
  }

  .toggle-btn {
    cursor: pointer;
    padding: 8px 22px;
    border: none;
    background-color: transparent;
    font-weight: 600;
    font-size: 0.9rem;
    color: var(--c-primary);
    transition: background-color 0.3s ease, color 0.3s ease;
    border-radius: 0;
    flex: 1 1 auto;
  }

  .toggle-btn:not(:last-child) {
    border-right: 1.5px solid var(--c-border);
  }

  .toggle-btn:hover,
  .toggle-btn:focus-visible {
    background-color: var(--c-accent-light);
    color: var(--c-accent);
    outline: none;
  }

  .toggle-btn.active {
    background-color: var(--c-accent);
    color: var(--c-theme);
    cursor: default;
  }

   
  table {
    width: 100%;
    border-collapse: collapse;
    table-layout: fixed;
    background-color: var(--c-theme);
    box-shadow: var(--card-shadow);  
    border-radius: 12px;
    overflow: hidden;
    transition: opacity 0.3s ease;
  }

  th,
  td {
    padding: 1rem;
    text-align: left;
    border-bottom: 1px solid var(--c-border);
  }

  th {
    font-weight: 600;
    text-transform: capitalize;
    letter-spacing: 0.06rem;
    background-color: var(--c-code-bg);
    color: var(--c-primary);
  }

  td {
    font-size: 0.95rem;
    color: var(--c-primary);
    word-break: break-word;
  }

  td a {
    text-decoration: none;
  }

  td a:hover,
  td a:focus-visible {
    text-decoration: none;
    outline: none;
  }

   
  .loading-indicator {
    text-align: center;
    font-style: italic;
    color: var(--c-accent);
    margin-top: 10px;
    font-weight: 600;
  }

   
  .pagination {
    display: flex;
    justify-content: center;
    align-items: center;
    margin-top: 25px;
    gap: 15px;
  }

  .pagination-button {
    padding: 10px 24px;
    border: none;
    border-radius: 9999px;  
    background-color: var(--c-primary);
    color: var(--c-theme);
    cursor: pointer;
    font-weight: 600;
    font-size: 0.9rem;
    transition: background-color 0.2s ease, transform 0.2s ease;
  }

  .pagination-button:hover:not(:disabled),
  .pagination-button:focus-visible:not(:disabled) {
    background-color: var(--c-primary-hover);
    transform: scale(1.05);
    outline: none;
  }

  .pagination-button:disabled {
    opacity: 0.5;
    cursor: not-allowed;
  }

  .page-info {
    font-weight: 600;
    font-size: 1rem;
    min-width: 100px;
    text-align: center;
  }

   
  @media (max-width: 600px) {
    .search-filter-wrapper {
      flex-direction: column;
      align-items: stretch;
    }

    .toggle-group {
      width: 100%;
      border-radius: 6px;
    }

    .toggle-btn {
      font-size: 1rem;
      padding: 12px;
    }

    table,
    thead,
    tbody,
    th,
    td,
    tr {
      display: block;
    }

    thead {
      display: none;
    }

    tr {
      margin-bottom: 1.25rem;
      border: 1px solid var(--c-border);
      border-radius: 12px;
      overflow: hidden;
    }

    td {
      display: flex;
      justify-content: space-between;
      padding: 0.75rem 1rem;
      border-bottom: 1px solid var(--c-border);
    }

    td::before {
      content: attr(data-label);
      font-weight: 600;
      color: var(--c-primary);
      flex-basis: 40%;
    }
  }

   
  #page-info:empty {
    font-style: italic;
    color: var(--c-border);
  }
</style>

<script>
  let allItems = []; 
  let openItems = []; 
  let currentItems = [];
  let currentPage = 1;
  const itemsPerPage = 5;

  const username = "nirav-raval";
  const tableBody = document.querySelector("#contrib-table tbody");
  const searchInput = document.getElementById("search-input");
  const prevButton = document.getElementById("prev-btn");
  const nextButton = document.getElementById("next-btn");
  const pageInfo = document.getElementById("page-info");

  const closedFilterBtn = document.getElementById("closed-filter");
  const openFilterBtn = document.getElementById("open-filter");
  const loadingIndicator = document.getElementById("loading-indicator");

  const ignoreList = [496, 3, 601, 598, 597, 602, 599, 7018];

  function setLoading(isLoading) {
    loadingIndicator.hidden = !isLoading;

    document.getElementById("contrib-table").style.opacity = isLoading
      ? "0.5"
      : "1";
  }

  async function fetchClosedPRs(page = 1) {
    setLoading(true);
    const res = await fetch(
      `https://api.github.com/search/issues?q=author:${username}+type:pr+is:closed&per_page=100&page=${page}`
    );
    const data = await res.json();

    const filteredItems = data.items.filter(
      (pr) => !ignoreList.includes(pr.number)
    );
    allItems = allItems.concat(filteredItems);

    if (data.items.length === 100) {
      await fetchClosedPRs(page + 1);
    } else {
      setLoading(false);
      updateCombinedItems();
    }
  }

  async function fetchOpenPRs(page = 1) {
    setLoading(true);
    const res = await fetch(
      `https://api.github.com/search/issues?q=author:${username}+type:pr+is:open&per_page=100&page=${page}`
    );
    const data = await res.json();

    const filteredItems = data.items.filter(
      (pr) => !ignoreList.includes(pr.number)
    );
    openItems = openItems.concat(filteredItems);

    if (data.items.length === 100) {
      await fetchOpenPRs(page + 1);
    } else {
      setLoading(false);
      updateCombinedItems();
    }
  }

  function updateCombinedItems() {
    currentItems = [];

    if (closedFilterBtn.classList.contains("active")) {
      currentItems = currentItems.concat(allItems);
    }
    if (openFilterBtn.classList.contains("active")) {
      currentItems = currentItems.concat(openItems);
    }

    const searchTerm = searchInput.value.toLowerCase();
    if (searchTerm) {
      currentItems = currentItems.filter(
        (pr) =>
          pr.title.toLowerCase().includes(searchTerm) ||
          pr.repository_url.toLowerCase().includes(searchTerm)
      );
    }

    currentItems.sort((a, b) => {
      const dateA = a.closed_at
        ? Date.parse(a.closed_at)
        : Date.parse(a.updated_at);
      const dateB = b.closed_at
        ? Date.parse(b.closed_at)
        : Date.parse(b.updated_at);
      return dateB - dateA;
    });

    currentPage = 1;
    displaySortedPRs();
  }

  function formatDate(dateStr) {
    const date = new Date(dateStr);
    return date
      .toLocaleDateString("en-GB", {
        day: "2-digit",
        month: "short",
        year: "2-digit",
      })
      .replace(/ /g, "-");
  }

  function displaySortedPRs() {
    const startIndex = (currentPage - 1) * itemsPerPage;
    const endIndex = Math.min(startIndex + itemsPerPage, currentItems.length);
    const displayedItems = currentItems.slice(startIndex, endIndex);

    tableBody.innerHTML = "";

    displayedItems.forEach((pr) => {
      const repoParts = pr.repository_url.split("/").slice(-2);
      const repoOwner = repoParts[0];
      const repoName = repoParts[1];
      if (repoOwner.toLowerCase() === username.toLowerCase()) return;

      const repoPath = `${repoOwner}/${repoName}`;
      const repoUrl = `https://github.com/${repoPath}`;
      const title = pr.title;
      const dateToShow = pr.closed_at ? pr.closed_at : pr.created_at;
      const closedDate = formatDate(dateToShow);

      const link = pr.html_url;

      const row = document.createElement("tr");
      row.innerHTML = `
        <td data-label="Repository"><a href="${repoUrl}" target="_blank" rel="noopener noreferrer">${repoPath}</a></td>
        <td data-label="Title">${title}</td>
        <td data-label="Closed Date">${closedDate}</td>
        <td data-label="Link"><a href="${link}" target="_blank" rel="noopener noreferrer">View PR</a></td>
      `;
      tableBody.appendChild(row);
    });

    if (currentItems.length === 0) {
      pageInfo.innerText = "No results";
    } else {
      const totalPages = Math.ceil(currentItems.length / itemsPerPage);
      pageInfo.innerText = `Page ${currentPage} / ${totalPages}`;
    }
    prevButton.disabled = currentPage === 1;
    nextButton.disabled = endIndex >= currentItems.length;
  }

  function setActiveFilter(selectedBtn) {
    
    closedFilterBtn.classList.remove("active");
    closedFilterBtn.setAttribute("aria-pressed", "false");
    openFilterBtn.classList.remove("active");
    openFilterBtn.setAttribute("aria-pressed", "false");

    
    selectedBtn.classList.add("active");
    selectedBtn.setAttribute("aria-pressed", "true");
  }

  closedFilterBtn.addEventListener("click", () => {
    if (closedFilterBtn.classList.contains("active")) return; 
    setActiveFilter(closedFilterBtn);
    updateCombinedItems();
  });

  openFilterBtn.addEventListener("click", () => {
    if (openFilterBtn.classList.contains("active")) return; 
    setActiveFilter(openFilterBtn);
    if (openItems.length === 0) {
      fetchOpenPRs();
    } else {
      updateCombinedItems();
    }
  });

  searchInput.addEventListener("input", () => {
    updateCombinedItems();
  });

  prevButton.addEventListener("click", () => {
    if (currentPage > 1) {
      currentPage -= 1;
      displaySortedPRs();
    }
  });

  nextButton.addEventListener("click", () => {
    if (currentPage * itemsPerPage < currentItems.length) {
      currentPage += 1;
      displaySortedPRs();
    }
  });

  
  fetchClosedPRs();
</script>

]]></content:encoded>
    </item>
  </channel>
</rss>
