<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Daniel Jonathan</title>
    <description>The latest articles on DEV Community by Daniel Jonathan (@imdj).</description>
    <link>https://dev.to/imdj</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3511351%2F03a6aadb-3b26-441e-906b-83fc70af6b8f.jpg</url>
      <title>DEV Community: Daniel Jonathan</title>
      <link>https://dev.to/imdj</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/imdj"/>
    <language>en</language>
    <item>
      <title>Logic Apps Agent Loop + MCP: Two Bugs Worth Knowing About</title>
      <dc:creator>Daniel Jonathan</dc:creator>
      <pubDate>Sun, 03 May 2026 22:35:02 +0000</pubDate>
      <link>https://dev.to/imdj/logic-apps-agent-loop-mcp-two-bugs-worth-knowing-about-3h6n</link>
      <guid>https://dev.to/imdj/logic-apps-agent-loop-mcp-two-bugs-worth-knowing-about-3h6n</guid>
      <description>&lt;p&gt;I spent the long weekend pushing Logic Apps MCP server capabilities further than I had before — and hit two bugs worth documenting. Both are filed. If you're building in this space, save yourself the debugging time.&lt;/p&gt;




&lt;h2&gt;
  
  
  Context
&lt;/h2&gt;

&lt;p&gt;If you've been following along, the MCP server and BODMAS Agent are covered in the previous posts. This post is just about what broke when I wired them together.&lt;/p&gt;




&lt;h2&gt;
  
  
  Bug 1 — Intermittent duplicate key error at tool registration
&lt;/h2&gt;

&lt;h3&gt;
  
  
  What happens
&lt;/h3&gt;

&lt;p&gt;The Agent Loop fails with a &lt;code&gt;BadRequest&lt;/code&gt; before making a single MCP call:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;HTTP request failed: 'An item with the same key has already been added. Key: {tool_name}'.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Both the tool names and the MCP server names are unique in the workflow definition — no duplicates anywhere in the JSON.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F59mjunr8ll2k29ixcbvi.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F59mjunr8ll2k29ixcbvi.png" alt=" " width="800" height="299"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  What makes it particularly frustrating to diagnose
&lt;/h3&gt;

&lt;p&gt;It is intermittent. Some runs fail, others succeed with identical configuration and identical input. No changes between a failing and a succeeding run — same workflow, same expression, same everything.&lt;/p&gt;

&lt;h3&gt;
  
  
  Load test
&lt;/h3&gt;

&lt;p&gt;I ran three test patterns across 60 total requests — all using expressions covering power, square root, and division.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Test&lt;/th&gt;
&lt;th&gt;Requests&lt;/th&gt;
&lt;th&gt;Succeeded&lt;/th&gt;
&lt;th&gt;Failed&lt;/th&gt;
&lt;th&gt;Failure Rate&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Fully parallel&lt;/td&gt;
&lt;td&gt;30&lt;/td&gt;
&lt;td&gt;17&lt;/td&gt;
&lt;td&gt;13&lt;/td&gt;
&lt;td&gt;~43%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Pairs of 2 (10s gap)&lt;/td&gt;
&lt;td&gt;20&lt;/td&gt;
&lt;td&gt;14&lt;/td&gt;
&lt;td&gt;6&lt;/td&gt;
&lt;td&gt;~30%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Sequential (15s gap)&lt;/td&gt;
&lt;td&gt;10&lt;/td&gt;
&lt;td&gt;7&lt;/td&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;~30%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Total&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;60&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;38&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;22&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;~37%&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Failure rate drops when concurrency is reduced but never goes away — even fully sequential calls at 15-second spacing still hit ~30%.&lt;/p&gt;

&lt;p&gt;View from Dev Tools:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fw7j6bkfiq93tfv9dckyj.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fw7j6bkfiq93tfv9dckyj.png" alt=" " width="800" height="526"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Resubmitting the failed runs from the portal confirms the intermittent nature — the same expression that failed goes through successfully after one or more resubmits, with no changes to the workflow or inputs.&lt;/p&gt;

&lt;h3&gt;
  
  
  What you can't do
&lt;/h3&gt;

&lt;p&gt;The &lt;code&gt;Agent&lt;/code&gt; action has a default retry policy, but it does not help here. A &lt;code&gt;BadRequest&lt;/code&gt; (400) is not treated as a transient error — the retry policy targets server-side failures (5xx), not client errors. So even with retries configured, the duplicate key error causes an immediate terminal failure. There is no clean in-workflow workaround.&lt;/p&gt;




&lt;h2&gt;
  
  
  Bug 2 — MCP Connector does not support OAuth
&lt;/h2&gt;

&lt;h3&gt;
  
  
  What happens
&lt;/h3&gt;

&lt;p&gt;Both the MCP server and the MCP client are Logic Apps Standard. When OAuth is configured on the MCP server side, the workflow doesn't trigger at all — it never reaches the Logic App. The connection gets corrupted at design time with the OAuth setup, and no run is created.&lt;/p&gt;

&lt;p&gt;Tools don't load but you can save the workflow.&lt;br&gt;
&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F06jgzy48yneghmdigabj.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F06jgzy48yneghmdigabj.png" alt=" " width="800" height="325"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;You get a 502 bad gateway error when you push a request.&lt;br&gt;
&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fyn8pp4m1d0kf189ppmnj.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fyn8pp4m1d0kf189ppmnj.png" alt=" " width="800" height="431"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The same endpoint called directly from Postman with a valid bearer token works fine.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F881o3w1k4rh7jl8s5n1t.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F881o3w1k4rh7jl8s5n1t.png" alt=" " width="800" height="537"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Why it matters
&lt;/h3&gt;

&lt;p&gt;To get the Agent Loop working, the MCP server has to run with either &lt;strong&gt;anonymous authentication&lt;/strong&gt; or &lt;strong&gt;key-based authentication&lt;/strong&gt;. OAuth simply does not work with the built-in MCP client connector.&lt;/p&gt;




&lt;h2&gt;
  
  
  Current state
&lt;/h2&gt;

&lt;p&gt;Both issues are filed on the Logic Apps GitHub repo:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;&lt;a href="https://github.com/Azure/logicapps/issues/1526" rel="noopener noreferrer"&gt;Agent Loop: "An item with the same key has already been added" when using McpClientTool&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The issue covers both bugs with full workflow JSON, reproduction steps, and screenshots. If you've hit either of these, add a reaction or comment — the more signal on the issue, the better.&lt;/p&gt;




&lt;h2&gt;
  
  
  What works in the meantime
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Set &lt;code&gt;"type": "anonymous"&lt;/code&gt; in the &lt;code&gt;McpServerEndpoints&lt;/code&gt; authentication block in &lt;code&gt;host.json&lt;/code&gt; — removes the OAuth blocker for dev and demo use&lt;/li&gt;
&lt;li&gt;Accept the intermittent failure rate on the Agent Loop and re-trigger manually when it hits — not a fix, but the success rate is high enough to keep building and testing&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;Both issues are filed. If you hit either of them, the GitHub issue is the right place to add signal.&lt;/p&gt;

</description>
      <category>logicapps</category>
      <category>mcp</category>
      <category>agentloop</category>
      <category>azure</category>
    </item>
    <item>
      <title>Running Multiple MCP Servers with Azure Logic Apps</title>
      <dc:creator>Daniel Jonathan</dc:creator>
      <pubDate>Fri, 01 May 2026 23:31:46 +0000</pubDate>
      <link>https://dev.to/imdj/running-multiple-mcp-servers-with-azure-logic-apps-2j6i</link>
      <guid>https://dev.to/imdj/running-multiple-mcp-servers-with-azure-logic-apps-2j6i</guid>
      <description>&lt;p&gt;Model Context Protocol (MCP) has become the standard way to expose tools to AI agents.&lt;/p&gt;

&lt;p&gt;With &lt;strong&gt;Azure Logic Apps&lt;/strong&gt;, you can create and run &lt;strong&gt;multiple MCP servers&lt;/strong&gt; and let an agent consume them together — cleanly and modularly.&lt;/p&gt;

&lt;p&gt;In this post, we'll build:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A &lt;strong&gt;Basic Arithmetic MCP server&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;An &lt;strong&gt;Extended Arithmetic MCP server&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;And connect an agent to both&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  The Scenario
&lt;/h2&gt;

&lt;p&gt;We'll create &lt;strong&gt;two MCP servers&lt;/strong&gt; using Logic App workflows and expose them to an agent.&lt;br&gt;
Both servers share &lt;strong&gt;Anonymous authentication&lt;/strong&gt;.&lt;/p&gt;


&lt;h2&gt;
  
  
  Step 1 — Create and Group MCP Workflows in Logic Apps
&lt;/h2&gt;

&lt;p&gt;Each operation is implemented as a Logic App workflow and exposed as an MCP tool.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F8zqftc8jqfixhb7ytne7.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F8zqftc8jqfixhb7ytne7.png" alt="Creating MCP Server" width="800" height="443"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Each workflow:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Accepts inputs (typically two numbers and an operation)&lt;/li&gt;
&lt;li&gt;Executes the required logic&lt;/li&gt;
&lt;li&gt;Returns a structured response&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;You then group these workflows into MCP servers:&lt;/p&gt;
&lt;h3&gt;
  
  
  Basic Arithmetic Server
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Add&lt;/li&gt;
&lt;li&gt;Subtract&lt;/li&gt;
&lt;li&gt;Multiply&lt;/li&gt;
&lt;li&gt;Divide&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;
  
  
  Extended Arithmetic Server
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Power&lt;/li&gt;
&lt;li&gt;Square Root&lt;/li&gt;
&lt;li&gt;Modulo&lt;/li&gt;
&lt;/ul&gt;


&lt;h3&gt;
  
  
  How This Is Stored (mcpservers.json)
&lt;/h3&gt;

&lt;p&gt;Once workflows are grouped into MCP servers, the configuration is automatically persisted in the &lt;strong&gt;&lt;code&gt;mcpservers.json&lt;/code&gt;&lt;/strong&gt; file.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ftl61aa24gilwqdoojxfl.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ftl61aa24gilwqdoojxfl.png" alt="MCP Server JSON" width="800" height="482"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This file contains:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;MCP server definitions&lt;/li&gt;
&lt;li&gt;Workflow (tool) mappings&lt;/li&gt;
&lt;/ul&gt;

&lt;blockquote&gt;
&lt;p&gt;💡 &lt;strong&gt;Key idea:&lt;/strong&gt; What you define as MCP servers (grouped workflows) is what gets written to &lt;code&gt;mcpservers.json&lt;/code&gt; — automatically.&lt;/p&gt;
&lt;/blockquote&gt;


&lt;h3&gt;
  
  
  MCP Server Endpoints
&lt;/h3&gt;

&lt;p&gt;Once registered, each MCP server is reachable at a predictable URL following this pattern:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight http"&gt;&lt;code&gt;&lt;span class="err"&gt;https://&amp;lt;your-logic-app&amp;gt;.azurewebsites.net/api/mcpservers/&amp;lt;ServerName&amp;gt;/mcp
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For example:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Basic server → &lt;code&gt;/api/mcpservers/BasicArithmetic/mcp&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Extended server → &lt;code&gt;/api/mcpservers/ExtendedArithmetic/mcp&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;blockquote&gt;
&lt;p&gt;💡 The server name in the URL matches exactly what you defined when grouping your workflows — no extra configuration needed.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  Step 2 — Connect the Agent to Both Servers
&lt;/h2&gt;

&lt;p&gt;The agent connects to both MCP servers using separate MCP client connections.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fxm99zuuffpif3fqt84t1.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fxm99zuuffpif3fqt84t1.png" alt="Consuming MCP Servers" width="800" height="273"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This allows the agent to:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Use basic operations from one server&lt;/li&gt;
&lt;li&gt;Use advanced operations from another&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;👉 Clean separation — no need for a single large service.&lt;/p&gt;




&lt;h2&gt;
  
  
  Step 3 — Execution Result
&lt;/h2&gt;

&lt;p&gt;When the agent runs, it invokes operations across both MCP servers.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ffqthyewsp9zq7g56wy5i.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ffqthyewsp9zq7g56wy5i.png" alt="Final Result" width="800" height="329"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The result:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Multiple MCP servers used seamlessly&lt;/li&gt;
&lt;li&gt;Operations resolved correctly&lt;/li&gt;
&lt;li&gt;Agent behaves as if using a unified toolset&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Why This Matters
&lt;/h2&gt;

&lt;p&gt;Running multiple MCP servers from a single Logic Apps instance gives you &lt;strong&gt;separation of concerns&lt;/strong&gt;, &lt;strong&gt;modular extensibility&lt;/strong&gt;, and &lt;strong&gt;independent scalability&lt;/strong&gt; — without changing your agent design.&lt;/p&gt;




&lt;h2&gt;
  
  
  Key Takeaway
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Workflows group into MCP servers, each exposed via a predictable endpoint&lt;/li&gt;
&lt;li&gt;Configuration is automatically managed in &lt;code&gt;mcpservers.json&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Agents can connect to multiple MCP servers simultaneously — no extra wiring required&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;This pattern enables a &lt;strong&gt;modular, composable tool ecosystem for AI agents&lt;/strong&gt; using Azure Logic Apps.&lt;/p&gt;

</description>
      <category>mcp</category>
      <category>azure</category>
      <category>logicapps</category>
      <category>agenticworkflow</category>
    </item>
    <item>
      <title>Logic Apps Local Dev Tools — Now with ACA and Azure Sign-in Support</title>
      <dc:creator>Daniel Jonathan</dc:creator>
      <pubDate>Thu, 30 Apr 2026 08:16:14 +0000</pubDate>
      <link>https://dev.to/imdj/logic-apps-local-dev-tools-now-with-aca-and-azure-sign-in-support-3e3h</link>
      <guid>https://dev.to/imdj/logic-apps-local-dev-tools-now-with-aca-and-azure-sign-in-support-3e3h</guid>
      <description>&lt;p&gt;The &lt;a href="https://marketplace.visualstudio.com/items?itemName=DanielJonathan.logic-apps-run-history-view-tool" rel="noopener noreferrer"&gt;Logic Apps Local Dev Tools&lt;/a&gt; VS Code extension started as a local dev tool — connect to a Logic Apps container running via Docker and browse run history without leaving the editor.&lt;/p&gt;

&lt;p&gt;This update adds two new connection types: &lt;strong&gt;ACA support via direct FQDN&lt;/strong&gt; and &lt;strong&gt;Azure Sign-in for cloud-hosted Logic Apps Standard&lt;/strong&gt;. If you used the original version for local Docker, everything still works — you just have more options now.&lt;/p&gt;




&lt;h2&gt;
  
  
  One dashboard, three connection types
&lt;/h2&gt;

&lt;p&gt;The extension now supports three types of Logic Apps instances in a single panel:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Runtime&lt;/strong&gt; — direct HTTP endpoint, works for local Docker (&lt;code&gt;localhost:7074&lt;/code&gt;) or an ACA FQDN&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;AZ-LA&lt;/strong&gt; — Azure Sign-in, connects to Logic Apps Standard via your subscription&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;EasyAuth&lt;/strong&gt; — ACA endpoint secured with Azure AD ingress auth; the extension handles the Bearer token using a Service Principal&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;All instances appear side by side in the Logic Apps Instances view:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Frayk57kx17r54nyimtn9.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Frayk57kx17r54nyimtn9.png" alt=" " width="800" height="569"&gt;&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  Managing connections
&lt;/h2&gt;

&lt;p&gt;Click &lt;strong&gt;Connections&lt;/strong&gt; to add, edit, or remove any instance. Each connection has a label, type, and endpoint or Azure resource identifier:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fgd0lpny7xsfl05o632f1.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fgd0lpny7xsfl05o632f1.png" alt=" " width="800" height="251"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Runtime&lt;/strong&gt; connections take a plain URL — ACA FQDN or &lt;code&gt;localhost&lt;/code&gt; for Docker&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;AZ-LA&lt;/strong&gt; connections use Azure Sign-in: select subscription, resource group, and Logic App — no endpoint URL needed&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;EasyAuth&lt;/strong&gt; connections take the ACA FQDN plus a Service Principal (client ID + secret) — the extension acquires a Bearer token automatically on each request&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  EasyAuth — connecting to a protected ACA endpoint
&lt;/h2&gt;

&lt;p&gt;If your Logic Apps container on ACA has Azure AD ingress auth enabled (&lt;code&gt;unauthenticatedClientAction: Return401&lt;/code&gt;), the &lt;strong&gt;Runtime&lt;/strong&gt; type won't work — requests without a Bearer token get a 401 before reaching the container.&lt;/p&gt;

&lt;p&gt;Use &lt;strong&gt;EasyAuth&lt;/strong&gt; instead: provide the ACA FQDN and a Service Principal with access to the app registration. The extension acquires a token via client credentials flow and attaches it to every request. SAS payload fetches (run history inputs/outputs) pass through the ACA platform exemption for &lt;code&gt;/runtime/webhooks/*&lt;/code&gt; without needing a token.&lt;/p&gt;

&lt;p&gt;When to use each type:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Scenario&lt;/th&gt;
&lt;th&gt;Connection type&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Local Docker (&lt;code&gt;localhost&lt;/code&gt;)&lt;/td&gt;
&lt;td&gt;Runtime&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;ACA container, no auth&lt;/td&gt;
&lt;td&gt;Runtime&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;ACA container, ACA Easy Auth enabled&lt;/td&gt;
&lt;td&gt;EasyAuth&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Cloud Logic Apps Standard (App Service)&lt;/td&gt;
&lt;td&gt;AZ-LA&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;




&lt;h2&gt;
  
  
  ACA workflow view
&lt;/h2&gt;

&lt;p&gt;Clicking &lt;strong&gt;View Workflows&lt;/strong&gt; on an ACA instance shows the full workflow list with kind, status, health, and trigger. From here you can get the callback URL or jump straight into run history:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fk72pf1ihc6daardr3qi1.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fk72pf1ihc6daardr3qi1.png" alt=" " width="800" height="480"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The run history panel works the same as local — full input/output per action, no Azure Portal needed.&lt;/p&gt;




&lt;p&gt;Install from the VS Code Marketplace:&lt;br&gt;
&lt;strong&gt;&lt;a href="https://marketplace.visualstudio.com/items?itemName=DanielJonathan.logic-apps-run-history-view-tool" rel="noopener noreferrer"&gt;Logic Apps Local Dev Tools&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;For the original local dev walkthrough — Docker setup, Azurite, and the design → test loop:&lt;br&gt;
&lt;strong&gt;&lt;a href="https://dev.to/imdj/logic-apps-local-dev-tools-visual-walkthrough-5gph"&gt;Logic Apps Local Dev Tools — Visual Walkthrough&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;

</description>
      <category>vscode</category>
      <category>devtool</category>
      <category>logicapps</category>
      <category>azure</category>
    </item>
    <item>
      <title>Host LogicApps as an MCP Server on Azure Container Apps</title>
      <dc:creator>Daniel Jonathan</dc:creator>
      <pubDate>Thu, 30 Apr 2026 07:43:25 +0000</pubDate>
      <link>https://dev.to/imdj/host-logicapps-as-an-mcp-server-on-azure-container-apps-508</link>
      <guid>https://dev.to/imdj/host-logicapps-as-an-mcp-server-on-azure-container-apps-508</guid>
      <description>&lt;p&gt;Run Logic Apps Standard as an MCP server in a Docker container on Azure Container Apps — then call it from another Logic App using the built-in MCP client connector inside an agent loop.&lt;/p&gt;

&lt;p&gt;This post connects two earlier pieces:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;MCP server setup&lt;/strong&gt;: &lt;a href="https://dev.to/imdj/-logic-apps-mcp-expose-arithmetic-tools-add-subtract-4o24"&gt;Logic Apps ❤️ MCP — Expose Arithmetic Tools&lt;/a&gt; — enabling the &lt;code&gt;/api/mcp&lt;/code&gt; endpoint and testing the tools&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Running on ACA&lt;/strong&gt;: &lt;a href="https://dev.to/imdj/logic-apps-standard-on-azure-container-apps-cloud-deployment-part-1"&gt;Logic Apps Standard on ACA&lt;/a&gt; — we take the same MCP server principles and host it as a Docker container on Azure Container Apps&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you haven't read those, here's what's already running before this post starts.&lt;/p&gt;




&lt;h2&gt;
  
  
  What's already deployed
&lt;/h2&gt;

&lt;p&gt;Seven arithmetic workflows — &lt;code&gt;add&lt;/code&gt;, &lt;code&gt;sub&lt;/code&gt;, &lt;code&gt;mul&lt;/code&gt;, &lt;code&gt;div&lt;/code&gt;, &lt;code&gt;mod&lt;/code&gt;, &lt;code&gt;pow&lt;/code&gt;, &lt;code&gt;sqrt&lt;/code&gt; — each an HTTP trigger workflow that takes inputs and returns a result. They're baked into a Docker image and running as a single container on ACA.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F06pfcdgmxfrnmpdhqub6.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F06pfcdgmxfrnmpdhqub6.png" alt="la-arithmeticmcp running on ACA" width="800" height="342"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Running locally first
&lt;/h3&gt;

&lt;p&gt;Before pushing to ACA you can run the same image locally. &lt;code&gt;docker-compose up&lt;/code&gt; starts the Logic Apps runtime on port &lt;strong&gt;7074&lt;/strong&gt; backed by Azurite for blob and queue storage. The MCP endpoint is immediately available at:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;http://localhost:7074/api/mcp
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;No auth is needed locally — the &lt;code&gt;"type": "anonymous"&lt;/code&gt; setting in &lt;code&gt;host.json&lt;/code&gt; covers both local dev and the ACA demo deployment. This is the right place to verify tool schemas, test workflow logic, and confirm the runtime discovers all seven tools before you deploy anything to the cloud.&lt;/p&gt;

&lt;p&gt;One block in &lt;code&gt;host.json&lt;/code&gt; enables the MCP endpoint:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="nl"&gt;"workflow"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"McpServerEndpoints"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"enable"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"authentication"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"anonymous"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The runtime exposes each workflow as an MCP tool automatically. No extra code, no separate service — the container itself is the MCP server, reachable at:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;https://la-arithmeticmcp.&amp;lt;env&amp;gt;.westeurope.azurecontainerapps.io/api/mcp
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;All seven tools are immediately discoverable:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fkasq8dvfcuymvob0bx74.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fkasq8dvfcuymvob0bx74.png" alt="MCPInspector" width="800" height="318"&gt;&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  Consuming it from another Logic App
&lt;/h2&gt;

&lt;p&gt;A second Logic App — &lt;strong&gt;BODMASAgent&lt;/strong&gt; — receives a math expression via HTTP and uses an Agent action (Azure OpenAI) to solve it. The agent has one tool available: the MCP server connector pointing at the endpoint above.&lt;/p&gt;

&lt;p&gt;When you add the MCP server action inside the Agent loop and connect it to the endpoint, the designer auto-discovers all tools and lets you pick which ones the agent can call:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F3v0tr9u7qo5fwc74nqqj.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F3v0tr9u7qo5fwc74nqqj.png" alt="MCPConnectorToolFetcgh" width="800" height="354"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;POST &lt;code&gt;(2 + 3) * 4^2 / 2&lt;/code&gt; and the agent works through BODMAS order on its own, calling one tool per step:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;✅ Step 1: (2 + 3) = 5   → wf_arithmetic_add
✅ Step 2: 4² = 16        → wf_arithmetic_pow
✅ Step 3: 5 × 16 = 80    → wf_arithmetic_mul
✅ Step 4: 80 ÷ 2 = 40    → wf_arithmetic_div
✅ Final Answer: 40
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fpkjgbiorgiqceyc0ws1r.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fpkjgbiorgiqceyc0ws1r.png" alt="Agent log" width="800" height="351"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;No orchestration code in the consuming Logic App. The agent decides the order and arguments; each tool call triggers a real workflow run on the server container and returns the result.&lt;/p&gt;




&lt;h2&gt;
  
  
  Securing the MCP endpoint on ACA
&lt;/h2&gt;

&lt;p&gt;The &lt;code&gt;host.json&lt;/code&gt; above sets &lt;code&gt;"type": "anonymous"&lt;/code&gt; — fine for local dev and demos, but for production you want the endpoint protected.&lt;/p&gt;

&lt;p&gt;ACA doesn't have App Service Easy Auth built in, but it has its own ingress-level authentication that works the same way: the ACA runtime validates the Azure AD Bearer token &lt;strong&gt;before the request reaches the container&lt;/strong&gt;. The Logic App MCP endpoint stays anonymous internally; ACA acts as the auth gateway.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fkmrapzaq0bu6ne94ihny.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fkmrapzaq0bu6ne94ihny.png" alt="ACA Authentication blade — Azure AD provider configured" width="800" height="520"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Setup — two steps:&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Create an Azure AD app registration (&lt;code&gt;la-arithmeticmcp-auth&lt;/code&gt;) and create a service principal for it in the tenant:
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;az ad sp create &lt;span class="nt"&gt;--id&lt;/span&gt; &amp;lt;app-id&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ol&gt;
&lt;li&gt;Enable ACA auth via ARM REST — &lt;strong&gt;do not use &lt;code&gt;az containerapp auth update&lt;/code&gt;&lt;/strong&gt;, it has a known CLI bug that silently strips the first and last character of &lt;code&gt;excludedPaths&lt;/code&gt; values, producing invalid config:
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;az rest &lt;span class="nt"&gt;--method&lt;/span&gt; PUT &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--url&lt;/span&gt; &lt;span class="s2"&gt;"https://management.azure.com/subscriptions/&amp;lt;sub&amp;gt;/resourceGroups/&amp;lt;rg&amp;gt;/providers/Microsoft.App/containerApps/la-arithmeticmcp/authConfigs/current?api-version=2024-03-01"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--body&lt;/span&gt; &lt;span class="s1"&gt;'{
    "properties": {
      "globalValidation": {
        "unauthenticatedClientAction": "Return401",
        "excludedPaths": ["/runtime/webhooks/workflow/scaleUnits/*"]
      },
      "platform": { "enabled": true },
      "identityProviders": {
        "azureActiveDirectory": {
          "registration": {
            "clientId": "&amp;lt;app-id&amp;gt;",
            "openIdIssuer": "https://sts.windows.net/&amp;lt;tenant-id&amp;gt;/"
          },
          "validation": {
            "allowedAudiences": ["api://&amp;lt;app-id&amp;gt;"]
          }
        }
      }
    }
  }'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;excludedPaths&lt;/code&gt; entry for &lt;code&gt;scaleUnits/*&lt;/code&gt; is required. ACA enforces Bearer token validation on all paths by default — including &lt;code&gt;/runtime/webhooks/*&lt;/code&gt;. The &lt;code&gt;scaleUnits&lt;/code&gt; subtree hosts SAS-authenticated payload fetch URLs (run inputs/outputs) that the Logic Apps runtime generates per run action. Excluding it lets those requests pass through on SAS params alone.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Do not set &lt;code&gt;WORKFLOWAPP_AAD_CLIENTID&lt;/code&gt; or &lt;code&gt;WORKFLOWAPP_AAD_TENANTID&lt;/code&gt; on the container.&lt;/strong&gt; These env vars activate DirectApi Azure AD token validation inside the Logic Apps runtime. Any client in EasyAuth mode sends a Bearer token on every request — including to the SAS-authenticated &lt;code&gt;scaleUnits&lt;/code&gt; URLs. When both a Bearer token and SAS params are present on the same request, the runtime rejects with &lt;code&gt;DirectApiRequestHasMoreThanOneAuthorization&lt;/code&gt;. ACA validates the Bearer token; the runtime validates the SAS params. They operate independently — don't configure them to overlap.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;strong&gt;Getting a token (client credentials):&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-X&lt;/span&gt; POST &lt;span class="s2"&gt;"https://login.microsoftonline.com/&amp;lt;tenant-id&amp;gt;/oauth2/v2.0/token"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--data-urlencode&lt;/span&gt; &lt;span class="s2"&gt;"grant_type=client_credentials"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--data-urlencode&lt;/span&gt; &lt;span class="s2"&gt;"client_id=&amp;lt;app-id&amp;gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--data-urlencode&lt;/span&gt; &lt;span class="s2"&gt;"client_secret=&amp;lt;secret&amp;gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--data-urlencode&lt;/span&gt; &lt;span class="s2"&gt;"scope=api://&amp;lt;app-id&amp;gt;/.default"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Result:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;# No token
HTTP 401  ← blocked by ACA ingress

# Valid Bearer token → initialize session
HTTP 200  {"protocolVersion":"2025-06-18","serverInfo":{"name":"Logic Apps Remote MCP Server",...}}

# Session established → tools/list
HTTP 200  {"tools":[{"name":"wf_arithmetic_div",...},{"name":"wf_arithmetic_add",...}]}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;What ACA auth actually protects:&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Path&lt;/th&gt;
&lt;th&gt;Auth&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;/api/mcp&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;✅ Bearer token required&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;/runtime/webhooks/workflow/api/management/*&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;✅ Bearer token required&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;/runtime/webhooks/workflow/scaleUnits/*&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;❌ Excluded — SAS params only&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The MCP client (Postman, Logic App connector, or any client) just needs to include &lt;code&gt;Authorization: Bearer &amp;lt;token&amp;gt;&lt;/code&gt; on every request.&lt;/p&gt;

&lt;h3&gt;
  
  
  Validating with VS Code Copilot
&lt;/h3&gt;

&lt;p&gt;VS Code Copilot can talk to any HTTP MCP server directly from the IDE. Add the secured endpoint to &lt;code&gt;.vscode/mcp.json&lt;/code&gt; in your workspace:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"servers"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"la-arithmeticmcp"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"http"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"url"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"https://la-arithmeticmcp.&amp;lt;env&amp;gt;.westeurope.azurecontainerapps.io/api/mcp"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"headers"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"Authorization"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Bearer &amp;lt;token&amp;gt;"&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;On workspace open, Copilot sends &lt;code&gt;initialize&lt;/code&gt; → &lt;code&gt;tools/list&lt;/code&gt; and populates its tool list from the response. The screenshot below shows the result — all 7 arithmetic tools returned from the live ACA endpoint, confirming that the Bearer token clears ACA ingress and the MCP session handshake completes successfully end to end.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Frslr8m751flclfom2rjh.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Frslr8m751flclfom2rjh.png" alt="VS Code Copilot — all 7 tools from the secured ACA endpoint" width="800" height="822"&gt;&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  Further reading — how MCP session handling works under the hood
&lt;/h2&gt;

&lt;p&gt;The built-in MCP client connector handles session initialization, tool discovery, and JSON-RPC framing automatically. If you're building a custom client or want to understand what's happening behind the scenes — how &lt;code&gt;initialize&lt;/code&gt; establishes a session, how &lt;code&gt;tools/list&lt;/code&gt; returns the catalog, and how &lt;code&gt;tools/call&lt;/code&gt; invokes a tool — this post walks through it manually:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;a href="https://dev.to/imdj/consume-an-mcp-endpoint-from-azure-logic-apps-with-an-agent-loop-52jh"&gt;Consume an MCP Endpoint from Azure Logic Apps with an Agent Loop&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;

</description>
      <category>mcp</category>
      <category>logicapps</category>
      <category>azure</category>
      <category>ipaas</category>
    </item>
    <item>
      <title>Running Azure Logic Apps Standard on Azure Container Apps</title>
      <dc:creator>Daniel Jonathan</dc:creator>
      <pubDate>Wed, 29 Apr 2026 14:16:18 +0000</pubDate>
      <link>https://dev.to/imdj/running-azure-logic-apps-standard-on-azure-container-apps-3nm1</link>
      <guid>https://dev.to/imdj/running-azure-logic-apps-standard-on-azure-container-apps-3nm1</guid>
      <description>&lt;h2&gt;
  
  
  Should you use Logic Apps Standard on ACA instead of n8n?
&lt;/h2&gt;

&lt;p&gt;n8n is popular for workflow automation — Docker-native, visual editor, hundreds of integrations. But if you're already in Azure, it means running and paying for another self-hosted service on top of your existing infrastructure.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Logic Apps Standard on ACA is a cost-effective alternative&lt;/strong&gt; if your workflows stay within the built-in connector set: Azure Blob, Queue, Service Bus, Event Hubs, HTTP, OpenAI, AI Search. No extra services, no OAuth setup. Durable run history, GitOps-friendly JSON definitions, and event-driven triggers — all included at container economics instead of an always-on App Service plan.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Hard limits — know them before you start:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Not a supported scenario.&lt;/strong&gt; This is not an officially supported deployment model. Microsoft support will not cover issues in this configuration — scaling and observability features are unavailable. Use for dev/test and experimentation only — production use is at your own risk.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No managed connectors.&lt;/strong&gt; Gallery connectors (O365, SharePoint, SQL, etc.) require an App Service MSI endpoint that ACA doesn't provide.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No XSLT maps.&lt;/strong&gt; The Transform XML action uses &lt;code&gt;NetFxWorker.exe&lt;/code&gt; — a Windows-only .NET Framework binary that won't run on Linux. Liquid/JSON transforms work fine (they run in-process).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Rebuild to deploy.&lt;/strong&gt; Workflows are baked into the image. Any change = Docker build + push + ACA update.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Visual designer needs local Docker.&lt;/strong&gt; Design and test locally, then deploy.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cold starts.&lt;/strong&gt; Scale-to-zero means latency after idle — matters for synchronous HTTP workflows.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If any of those are blockers, use App Service Standard instead. If they're not — keep reading.&lt;/p&gt;

&lt;p&gt;One thing that doesn't change moving to ACA: &lt;em&gt;&lt;strong&gt;workflow state and run history are still backed by Azure Table Storage&lt;/strong&gt;&lt;/em&gt;, the same as App Service. Durability is unchanged — the container is just the runtime host.&lt;/p&gt;




&lt;h2&gt;
  
  
  What we're building
&lt;/h2&gt;

&lt;p&gt;Six workflows deployed as a single Docker container on ACA:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Workflow&lt;/th&gt;
&lt;th&gt;Trigger&lt;/th&gt;
&lt;th&gt;Purpose&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;wf1&lt;/td&gt;
&lt;td&gt;HTTP GET&lt;/td&gt;
&lt;td&gt;Stateful HTTP request/response&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;wf2&lt;/td&gt;
&lt;td&gt;Azure Blob Storage&lt;/td&gt;
&lt;td&gt;Fires on blob upload, reads metadata&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;wf3&lt;/td&gt;
&lt;td&gt;Azure Queue Storage&lt;/td&gt;
&lt;td&gt;Processes queue messages&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;wf4&lt;/td&gt;
&lt;td&gt;Azure Service Bus&lt;/td&gt;
&lt;td&gt;Processes messages from wf4queue&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;wf5&lt;/td&gt;
&lt;td&gt;Azure Service Bus&lt;/td&gt;
&lt;td&gt;Receives SB message, calls external HTTP endpoint&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;wf6&lt;/td&gt;
&lt;td&gt;HTTP POST&lt;/td&gt;
&lt;td&gt;JSON-to-JSON transform via Liquid map&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;




&lt;h2&gt;
  
  
  The Docker image
&lt;/h2&gt;

&lt;p&gt;No official pre-built image exists for Logic Apps Standard — you build your own with the Functions Core Tools and your workflow files baked in:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight docker"&gt;&lt;code&gt;&lt;span class="k"&gt;FROM&lt;/span&gt;&lt;span class="s"&gt; mcr.microsoft.com/dotnet/sdk:8.0&lt;/span&gt;

&lt;span class="k"&gt;ENV&lt;/span&gt;&lt;span class="s"&gt; DEBIAN_FRONTEND=noninteractive&lt;/span&gt;
&lt;span class="k"&gt;WORKDIR&lt;/span&gt;&lt;span class="s"&gt; /home/site/wwwroot&lt;/span&gt;

&lt;span class="k"&gt;RUN &lt;/span&gt;apt-get update &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="se"&gt;\
&lt;/span&gt;    apt-get &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-y&lt;/span&gt; curl gnupg unzip coreutils &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="se"&gt;\
&lt;/span&gt;    curl &lt;span class="nt"&gt;-fsSL&lt;/span&gt; https://deb.nodesource.com/setup_18.x | bash - &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="se"&gt;\
&lt;/span&gt;    apt-get &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-y&lt;/span&gt; nodejs &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="se"&gt;\
&lt;/span&gt;    npm &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-g&lt;/span&gt; azure-functions-core-tools@4 &lt;span class="nt"&gt;--unsafe-perm&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nb"&gt;true&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="se"&gt;\
&lt;/span&gt;    apt-get clean &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;rm&lt;/span&gt; &lt;span class="nt"&gt;-rf&lt;/span&gt; /var/lib/apt/lists/&lt;span class="k"&gt;*&lt;/span&gt;

&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; . .&lt;/span&gt;

&lt;span class="k"&gt;ENV&lt;/span&gt;&lt;span class="s"&gt; FUNCTIONS_WORKER_RUNTIME="node"&lt;/span&gt;
&lt;span class="k"&gt;ENV&lt;/span&gt;&lt;span class="s"&gt; FUNCTIONS_WORKER_RUNTIME_VERSION="~4"&lt;/span&gt;
&lt;span class="k"&gt;ENV&lt;/span&gt;&lt;span class="s"&gt; AzureWebJobsFeatureFlags="EnableMultiLanguageWorker"&lt;/span&gt;
&lt;span class="k"&gt;ENV&lt;/span&gt;&lt;span class="s"&gt; AzureWebJobsSecretStorageType="Files"&lt;/span&gt;
&lt;span class="k"&gt;ENV&lt;/span&gt;&lt;span class="s"&gt; APP_KIND="workflowapp"&lt;/span&gt;

&lt;span class="k"&gt;EXPOSE&lt;/span&gt;&lt;span class="s"&gt; 7074&lt;/span&gt;
&lt;span class="k"&gt;ENTRYPOINT&lt;/span&gt;&lt;span class="s"&gt; ["func", "start", "--verbose", "--port", "7074"]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Workflow JSON files are baked in at &lt;code&gt;COPY . .&lt;/code&gt;. The runtime reads and executes them — no compilation step.&lt;/p&gt;




&lt;h2&gt;
  
  
  Project structure
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;LABasicDemo/
├── host.json                  # Extension bundle declaration
├── connections.json           # Service provider connections
├── Dockerfile
├── Artifacts/Maps/            # Liquid maps (wf6)
├── wf1/workflow.json ... wf6/workflow.json
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;connections.json&lt;/code&gt; maps each connection name (e.g. &lt;code&gt;servicebus&lt;/code&gt;) to a &lt;code&gt;serviceProviderId&lt;/code&gt; and a connection string via &lt;code&gt;@appsetting(...)&lt;/code&gt;. The runtime resolves these at startup — no ARM roundtrip.&lt;/p&gt;




&lt;h2&gt;
  
  
  Bicep infrastructure
&lt;/h2&gt;

&lt;p&gt;The sections below highlight the non-obvious parts. ACR, Log Analytics, and ACA Environment are standard boilerplate.&lt;/p&gt;

&lt;h3&gt;
  
  
  Service Bus — Basic SKU is enough
&lt;/h3&gt;

&lt;p&gt;Basic SKU covers queues. Standard is only needed for topics or managed API connections — which don't work in ACA anyway.&lt;/p&gt;

&lt;h3&gt;
  
  
  The critical env vars — stability fixes
&lt;/h3&gt;

&lt;p&gt;The Logic Apps runtime generates a &lt;strong&gt;15-character LAIdentifier hash&lt;/strong&gt; to namespace all Azure Table Storage tables for run history. By default the hash is derived from the host ID — if that changes on restart, run history appears lost.&lt;/p&gt;

&lt;p&gt;Three env vars pin the identity across pod restarts:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;{ name: 'AzureFunctionsWebHost__hostid', value: appName }
{ name: 'WEBSITE_HOSTNAME',              value: '${appName}.${acaEnv.properties.defaultDomain}' }
{ name: 'WEBSITE_CONTENTSHARE',          value: contentShareName }
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Without &lt;code&gt;AzureFunctionsWebHost__hostid&lt;/code&gt;, every restart generates a new host ID, a new LAIdentifier, new storage tables — and prior run history is effectively orphaned.&lt;/p&gt;

&lt;h3&gt;
  
  
  Azure Files mount — critical path
&lt;/h3&gt;

&lt;p&gt;Mount at &lt;code&gt;.azure-webjobs-hosts&lt;/code&gt;, &lt;strong&gt;not&lt;/strong&gt; at &lt;code&gt;/home/site/wwwroot&lt;/code&gt;. Mounting at the root wipes all workflow files baked into the image.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;volumeMounts: [{ volumeName: 'content-share', mountPath: '/home/site/wwwroot/.azure-webjobs-hosts' }]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This directory holds blob trigger checkpoints and distributed locks — persisting it prevents replaying already-processed blobs after a restart.&lt;/p&gt;

&lt;h3&gt;
  
  
  Full env var list
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;env: [
  { name: 'AzureWebJobsStorage',                      secretRef: 'storage-connection-string' }
  { name: 'WORKFLOWS_STORAGE_CONNECTION_STRING',      secretRef: 'storage-connection-string' }
  { name: 'AzureBlob_connectionString',               secretRef: 'storage-connection-string' }
  { name: 'azurequeues_connectionString',              secretRef: 'storage-connection-string' }
  { name: 'servicebus_connectionString',              secretRef: 'servicebus-connection-string' }
  { name: 'FUNCTIONS_WORKER_RUNTIME',                 value: 'node' }
  { name: 'FUNCTIONS_WORKER_RUNTIME_VERSION',         value: '~4' }
  { name: 'AzureWebJobsFeatureFlags',                 value: 'EnableMultiLanguageWorker' }
  { name: 'APP_KIND',                                 value: 'workflowapp' }
  { name: 'WEBSITE_SITE_NAME',                        value: appName }
  { name: 'APPINSIGHTS_INSTRUMENTATIONKEY',           value: appInsightsKey }
  { name: 'WEBSITE_CONTENTAZUREFILECONNECTIONSTRING', secretRef: 'storage-connection-string' }
  { name: 'WEBSITE_CONTENTSHARE',                     value: contentShareName }
  { name: 'WEBSITE_HOSTNAME',                         value: '${appName}.${acaEnv.properties.defaultDomain}' }
  { name: 'AzureFunctionsWebHost__hostid',            value: appName }
  { name: 'WEBSITE_RESOURCE_GROUP',                   value: resourceGroup().name }
  { name: 'WEBSITE_OWNER_NAME',                       value: '${subscription().subscriptionId}+${resourceGroup().name}-WestEuropewebspace' }
]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Deployment
&lt;/h2&gt;

&lt;h3&gt;
  
  
  deploy.sh — provision infrastructure
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;az deployment group create &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--resource-group&lt;/span&gt; LogicAppHubRG &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--template-file&lt;/span&gt; infra/main.bicep &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--parameters&lt;/span&gt; infra/main.bicepparam &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--output&lt;/span&gt; table
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  build-push.sh — build and deploy the image
&lt;/h3&gt;

&lt;p&gt;ACA caches the image digest at revision creation time — deploying with &lt;code&gt;:latest&lt;/code&gt; may leave the container on a stale image. Always pin the exact digest:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;az acr build &lt;span class="nt"&gt;--registry&lt;/span&gt; labasicdemoacr &lt;span class="nt"&gt;--image&lt;/span&gt; logicapp-basicdemo:latest &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--file&lt;/span&gt; ../LABasicDemo/Dockerfile ../LABasicDemo

&lt;span class="nv"&gt;DIGEST&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;az acr repository show-manifests &lt;span class="nt"&gt;--name&lt;/span&gt; labasicdemoacr &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--repository&lt;/span&gt; logicapp-basicdemo &lt;span class="nt"&gt;--orderby&lt;/span&gt; time_desc &lt;span class="nt"&gt;--query&lt;/span&gt; &lt;span class="s2"&gt;"[0].digest"&lt;/span&gt; &lt;span class="nt"&gt;-o&lt;/span&gt; tsv&lt;span class="si"&gt;)&lt;/span&gt;

az containerapp update &lt;span class="nt"&gt;--name&lt;/span&gt; la-basicdemo &lt;span class="nt"&gt;--resource-group&lt;/span&gt; LogicAppHubRG &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--image&lt;/span&gt; &lt;span class="s2"&gt;"labasicdemoacr.azurecr.io/logicapp-basicdemo@&lt;/span&gt;&lt;span class="nv"&gt;$DIGEST&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;az acr build&lt;/code&gt; runs the Docker build in the cloud — no local Docker daemon needed.&lt;/p&gt;

&lt;h3&gt;
  
  
  What lands in Azure
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fhucx2mu535i976dtb080.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fhucx2mu535i976dtb080.png" alt="Azure resource group after deployment" width="800" height="224"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The Service Bus namespace and storage account live in a separate shared resource group.&lt;/p&gt;




&lt;h2&gt;
  
  
  Notable workflows
&lt;/h2&gt;

&lt;h3&gt;
  
  
  wf5 — Service Bus → HTTP action
&lt;/h3&gt;

&lt;p&gt;wf5 originally used a managed API connection for Service Bus. It was redesigned to use the service provider connector (connection string auth) + a built-in HTTP action after managed connections proved unworkable. The service provider trigger polls &lt;code&gt;wf5queue&lt;/code&gt;; on receipt it fires a GET to an external endpoint.&lt;/p&gt;

&lt;h3&gt;
  
  
  wf6 — Liquid JSON transform
&lt;/h3&gt;

&lt;p&gt;Liquid maps work in Linux containers — processed in-process with no external binary. Map stored in &lt;code&gt;Artifacts/Maps/PersonToContact.liquid&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight liquid"&gt;&lt;code&gt;{
  "fullName": "&lt;span class="cp"&gt;{{&lt;/span&gt;&lt;span class="nv"&gt;content&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nv"&gt;firstName&lt;/span&gt;&lt;span class="cp"&gt;}}&lt;/span&gt; &lt;span class="cp"&gt;{{&lt;/span&gt;&lt;span class="nv"&gt;content&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nv"&gt;lastName&lt;/span&gt;&lt;span class="cp"&gt;}}&lt;/span&gt;",
  "email": "&lt;span class="cp"&gt;{{&lt;/span&gt;&lt;span class="nv"&gt;content&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nv"&gt;email&lt;/span&gt;&lt;span class="cp"&gt;}}&lt;/span&gt;"
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Action in workflow.json:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Liquid"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"kind"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"JsonToJson"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"inputs"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"content"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"@triggerBody()"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"map"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"source"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"LogicApp"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"PersonToContact.liquid"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Test: &lt;code&gt;POST {"firstName":"John","lastName":"Doe","email":"john@example.com"}&lt;/code&gt; → &lt;code&gt;{"fullName":"John Doe","email":"john@example.com"}&lt;/code&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  Agentic flows work out of the box
&lt;/h2&gt;

&lt;p&gt;One capability worth calling out explicitly: &lt;strong&gt;AI agent workflows are fully supported in ACA containers.&lt;/strong&gt; Azure OpenAI and Azure AI Search are both built-in service provider connectors — they authenticate via API key in app settings, no ARM token required. This means you can build agentic patterns (LLM calls, RAG pipelines, tool-use loops) directly in Logic Apps Standard and deploy them to ACA with no additional setup.&lt;/p&gt;

&lt;p&gt;This is a meaningful advantage over n8n, which relies on community nodes for OpenAI integration. Logic Apps gives you native stateful orchestration, durable run history per step, and retry policies — all built into the agent workflow without extra infrastructure.&lt;/p&gt;




&lt;h2&gt;
  
  
  The connector boundary
&lt;/h2&gt;

&lt;p&gt;Logic Apps connectors come in two families:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Service provider connectors (built-in)&lt;/strong&gt; — authenticate via connection strings, no ARM roundtrip. Work in containers:&lt;br&gt;
Azure Blob, Azure Queue, Azure Service Bus, Azure Event Hubs, HTTP/HTTPS, Azure OpenAI, Azure AI Search.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Managed API connections (&lt;code&gt;Microsoft.Web/connections&lt;/code&gt;)&lt;/strong&gt; — the 400+ gallery connectors. Require an ARM token acquired via the App Service MSI endpoint (&lt;code&gt;IDENTITY_ENDPOINT&lt;/code&gt; + &lt;code&gt;IDENTITY_HEADER&lt;/code&gt;). App Service injects this automatically; ACA does not.&lt;/p&gt;

&lt;p&gt;We tried two approaches: service principal via &lt;code&gt;WORKFLOWAPP_AAD_CLIENTID&lt;/code&gt; / &lt;code&gt;TENANTID&lt;/code&gt; / &lt;code&gt;CLIENTSECRET&lt;/code&gt;, and user-assigned managed identity via &lt;code&gt;AZURE_CLIENT_ID&lt;/code&gt;. Neither worked — the &lt;code&gt;WORKFLOWAPP_AAD_*&lt;/code&gt; variables are only active in the Hybrid Deployment Model (Arc-enabled AKS + ACA Logic Apps extension), not the custom image approach.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;XSLT maps&lt;/strong&gt; also don't work: the Transform XML action delegates to &lt;code&gt;NetFxWorker.exe&lt;/code&gt; — a Windows PE32 binary that the Linux kernel refuses to execute.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Feature&lt;/th&gt;
&lt;th&gt;ACA (Linux)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Service provider connectors&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Liquid / JSON transforms&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Managed API connections&lt;/td&gt;
&lt;td&gt;❌ No MSI endpoint&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;XSLT maps&lt;/td&gt;
&lt;td&gt;❌ Windows-only binary&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;




&lt;h2&gt;
  
  
  Verifying stability across restarts
&lt;/h2&gt;

&lt;p&gt;The key test: trigger each workflow, stop and restart the container, then check that the same run IDs are still visible in history.&lt;/p&gt;

&lt;p&gt;The easiest way to inspect run history is the &lt;strong&gt;&lt;a href="https://marketplace.visualstudio.com/items?itemName=DanielJonathan.logic-apps-run-history-view-tool" rel="noopener noreferrer"&gt;Logic Apps Run History View Tool&lt;/a&gt;&lt;/strong&gt; VS Code extension — connect it to the deployed ACA endpoint and browse runs per workflow directly in the editor, with full input/output per action visible.&lt;/p&gt;

&lt;p&gt;Before the &lt;code&gt;AzureFunctionsWebHost__hostid&lt;/code&gt; fix, run history was orphaned on every restart. After the fix it survives indefinitely.&lt;/p&gt;




&lt;h2&gt;
  
  
  Cost comparison vs n8n
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;n8n (self-hosted)&lt;/th&gt;
&lt;th&gt;Logic Apps Standard on ACA&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Compute&lt;/td&gt;
&lt;td&gt;Fixed VM/container cost&lt;/td&gt;
&lt;td&gt;Serverless, scale to 0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;State storage&lt;/td&gt;
&lt;td&gt;SQLite / Postgres&lt;/td&gt;
&lt;td&gt;Azure Table Storage (~pennies)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Built-in connectors&lt;/td&gt;
&lt;td&gt;400+ community nodes&lt;/td&gt;
&lt;td&gt;Service providers + HTTP&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Managed connectors (O365 etc.)&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;❌ App Service only&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;XSLT maps&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;❌ Windows only&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Liquid transforms&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Run history&lt;/td&gt;
&lt;td&gt;Basic&lt;/td&gt;
&lt;td&gt;Full input/output per action&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Visual designer&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;✅ VS Code (local)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;GitOps / IaC&lt;/td&gt;
&lt;td&gt;Manual&lt;/td&gt;
&lt;td&gt;Native JSON + Bicep&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Sweet spot&lt;/strong&gt;: Azure-native event-driven pipelines — blob, queue, Service Bus, outbound HTTP — where you want durable run history and GitOps deployment without an always-on App Service plan.&lt;/p&gt;




&lt;h2&gt;
  
  
  What's next
&lt;/h2&gt;

&lt;p&gt;For local development — running the same container with Docker, Azurite, and the Logic Apps VS Code extension — see:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;a href="https://dev.to/imdj/logic-apps-local-dev-tools-visual-walkthrough-5gph"&gt;Logic Apps Local Dev Tools — Visual Walkthrough&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;That post covers the full design → test → deploy loop without repeating what's here.&lt;/p&gt;

</description>
      <category>logicapps</category>
      <category>azure</category>
      <category>containers</category>
      <category>azurecontainerapps</category>
    </item>
    <item>
      <title>Event Debouncing with Logic Apps and Azure Table Storage</title>
      <dc:creator>Daniel Jonathan</dc:creator>
      <pubDate>Mon, 20 Apr 2026 17:58:35 +0000</pubDate>
      <link>https://dev.to/imdj/event-debouncing-with-logic-apps-and-azure-table-storage-58cd</link>
      <guid>https://dev.to/imdj/event-debouncing-with-logic-apps-and-azure-table-storage-58cd</guid>
      <description>&lt;p&gt;Forwarding every webhook event directly to a downstream API is a recipe for throttling and duplicate processing. This post walks through how to fix that with three Logic Apps and one Azure Table Storage table.&lt;/p&gt;




&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Debouncing&lt;/strong&gt; is a term from frontend development — wait for the noise to stop, then act once.&lt;br&gt;&lt;br&gt;
In integration, this pattern is better described as &lt;strong&gt;event buffering with deduplication&lt;/strong&gt;: absorb bursts, collapse repeated updates per entity, and process only the final state.  &lt;/p&gt;

&lt;p&gt;In this implementation, Azure Table Storage is not the source of truth — it acts as a &lt;strong&gt;deduplication index&lt;/strong&gt;. We store only the entity ID, and at processing time we re-fetch the authoritative state from the source system before calling downstream APIs.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  The Problem
&lt;/h2&gt;

&lt;p&gt;Source systems fire event bursts — hundreds of events at once during bulk imports, and multiple rapid updates for the same entity. You don't need to process every intermediate state — only the final one per entity.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;A burst of 200 events may touch 50 entities, each updated multiple times. Every entity should be processed once, with its latest state — and the downstream API called once per entity.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  The Pattern
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Source System Webhook
        │
        ▼
  rcv-events  (HTTP trigger)
        │  upsert each event → EventBuffer table
        ▼
  Azure Table Storage: EventBuffer
        │  PartitionKey: "relation-events"  RowKey: entityId  Status: "Pending"
        ▼
  prc-events  (Timer: every 5 min)
        │  query Pending rows older than X min → dispatch each
        ▼
  prc-process-single-event
        │  mark Processing → fetch fresh from source → call downstream
        │  delete on success  /  reset to Pending on failure
        ▼
  Downstream API
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Step 1 — Receive
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;rcv-events&lt;/code&gt; accepts a batch of events via HTTP and upserts each one into the buffer table. No queue, no broker — the HTTP trigger is the ingress.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F38ph8htc9ejak3ascvb0.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F38ph8htc9ejak3ascvb0.png" alt="rcv-events workflow — HTTP trigger with ForEach upsert to EventBuffer table" width="800" height="357"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Each row looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"PartitionKey"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"relation-events"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"RowKey"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"&amp;lt;entityId&amp;gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"Event"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"updated"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"EntityType"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Record"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"Status"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Pending"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"ReceivedAt"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2026-04-20T14:30:00Z"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;&lt;code&gt;RowKey = entityId&lt;/code&gt;&lt;/strong&gt; is the key insight. No matter how many events arrive for the same entity, there is always exactly one row. The tenth update overwrites the ninth. Deduplication is a schema decision, not code.&lt;/p&gt;




&lt;h2&gt;
  
  
  Step 2 — Wait
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;prc-events&lt;/code&gt; runs on a timer (every 5 minutes) and queries rows where &lt;code&gt;Status eq 'Pending'&lt;/code&gt; and &lt;code&gt;LastUpdated &amp;lt;= utcNow() - X minutes&lt;/code&gt;. The time window is your debounce threshold — nothing gets processed until the burst settles.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ft5pa90q3q27aabt30uye.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ft5pa90q3q27aabt30uye.png" alt="prc-events workflow — timer trigger querying pending rows and dispatching each to prc-process-single-event" width="800" height="382"&gt;&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  Step 3 — Process
&lt;/h2&gt;

&lt;p&gt;For each pending row, &lt;code&gt;prc-process-single-event&lt;/code&gt;:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Marks the row &lt;strong&gt;Processing&lt;/strong&gt; — prevents double-processing if the timer fires again mid-run&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Fetches the current state from the source system&lt;/strong&gt; — never trusts the buffered payload, which may already be stale&lt;/li&gt;
&lt;li&gt;Calls the downstream API with fresh data&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Deletes&lt;/strong&gt; the row on success / resets to &lt;strong&gt;Pending&lt;/strong&gt; on failure&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F61xx2i38kgb78m8sj35r.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F61xx2i38kgb78m8sj35r.png" alt="prc-process-single-event workflow — mark Processing, fetch from source, call downstream, delete on success or reset to Pending on failure" width="800" height="454"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This gives at-least-once delivery with automatic retry — no custom infrastructure needed.&lt;/p&gt;




&lt;h2&gt;
  
  
  Status Lifecycle
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Pending  →  Processing  →  [deleted]
                 │
                 └──(on failure)──→  Pending
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Three states, one field. Fully visible in Azure Storage Explorer during an incident.&lt;/p&gt;




&lt;h2&gt;
  
  
  Why It Works
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Deduplication for free&lt;/strong&gt; — one row per entity, always the latest&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No ordering concerns&lt;/strong&gt; — you fetch fresh data at processing time, so intermediate states are irrelevant&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Respects downstream rate limits&lt;/strong&gt; — 20 updates in 30 minutes still results in one API call to the downstream system&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Parallel processing&lt;/strong&gt; — &lt;code&gt;prc-events&lt;/code&gt; fans out each pending row as an independent call, so entities are processed concurrently with isolated retry state&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Operationally transparent&lt;/strong&gt; — query the table, see exactly what's pending or stuck&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No broker needed&lt;/strong&gt; at low-to-moderate scale — if your HTTP trigger can handle the inbound burst and your timer cadence keeps up with the queue depth, you don't need Service Bus&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Consider adding Service Bus only if you need strict ordering, dead-lettering, or multiple consumers on the same stream.&lt;/p&gt;

&lt;h2&gt;
  
  
  When Not to Use This Pattern
&lt;/h2&gt;

&lt;p&gt;Avoid it when you need strict event ordering, every event preserved independently, near-real-time latency, multiple consumers, or very high throughput. In those cases, reach for Service Bus or Event Hubs instead.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;No Service Bus. No custom retry logic. No ordering guarantees needed. Just a table, a timer, and one row per entity.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>logicapps</category>
      <category>serverless</category>
      <category>azure</category>
      <category>eventdriven</category>
    </item>
    <item>
      <title>Debugging XSLT vs Liquid in VS Code</title>
      <dc:creator>Daniel Jonathan</dc:creator>
      <pubDate>Fri, 03 Apr 2026 09:39:35 +0000</pubDate>
      <link>https://dev.to/imdj/debugging-xslt-vs-liquid-in-vs-code-32h4</link>
      <guid>https://dev.to/imdj/debugging-xslt-vs-liquid-in-vs-code-32h4</guid>
      <description>&lt;p&gt;Both the XSLT Debugger and DotLiquid Debugger let you step through a template and inspect variables. But they work differently under the hood — and those differences affect what you can do while debugging.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Fundamental Difference
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;XSLT debugging is live.&lt;/strong&gt; The XSLT Debugger supports two engines, each with its own instrumentation strategy:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Saxon (XSLT 2.0/3.0)&lt;/strong&gt; — exposes a &lt;code&gt;TraceListener&lt;/code&gt; interface with &lt;code&gt;Enter&lt;/code&gt; and &lt;code&gt;Leave&lt;/code&gt; callbacks that fire as each instruction executes. The engine cooperates natively.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;.NET &lt;code&gt;XslCompiledTransform&lt;/code&gt; (XSLT 1.0)&lt;/strong&gt; — has no TraceListener, so the debugger rewrites the stylesheet at load time, injecting &lt;code&gt;&amp;lt;dbg:probe&amp;gt;&lt;/code&gt; extension calls into every &lt;code&gt;template&lt;/code&gt;, &lt;code&gt;if&lt;/code&gt;, &lt;code&gt;for-each&lt;/code&gt;, and &lt;code&gt;when&lt;/code&gt; block. A registered extension object handles each probe and pauses execution on a &lt;code&gt;TaskCompletionSource&lt;/code&gt; until you click Step.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Both approaches genuinely pause execution. You're inspecting a running process.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Liquid debugging is replay.&lt;/strong&gt; DotLiquid has no such API — &lt;code&gt;Template.Render()&lt;/code&gt; runs the whole template and returns. The extension records a trace during that render, then lets you step through the recording. By the time you click Step, the template has already finished.&lt;/p&gt;




&lt;h2&gt;
  
  
  Capability Comparison
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;XSLT Debugger&lt;/th&gt;
&lt;th&gt;DotLiquid Debugger&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Step model&lt;/td&gt;
&lt;td&gt;Live pause/resume&lt;/td&gt;
&lt;td&gt;Trace replay&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Breakpoints&lt;/td&gt;
&lt;td&gt;Yes — set and hit mid-execution&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Backward stepping&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;Always — it's just a recording&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Variable state&lt;/td&gt;
&lt;td&gt;Live, from the running engine&lt;/td&gt;
&lt;td&gt;Recorded snapshot at each step&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Modify and continue&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;No — edit and re-render&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Conditional breakpoints&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Filter chain tracing&lt;/td&gt;
&lt;td&gt;N/A&lt;/td&gt;
&lt;td&gt;Yes — each filter is a step&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Branch visibility&lt;/td&gt;
&lt;td&gt;Taken branch only&lt;/td&gt;
&lt;td&gt;Taken branch only&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Re-render cost&lt;/td&gt;
&lt;td&gt;Steps are free (engine is paused)&lt;/td&gt;
&lt;td&gt;One render upfront, steps are free after&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;




&lt;h2&gt;
  
  
  What This Looks Like in Practice
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Setting a breakpoint:&lt;/strong&gt;&lt;br&gt;
In the XSLT debugger you can set a breakpoint on a specific &lt;code&gt;&amp;lt;xsl:template&amp;gt;&lt;/code&gt; or &lt;code&gt;&amp;lt;xsl:for-each&amp;gt;&lt;/code&gt;, hit F5, and the debugger stops there — even if that template fires 50 iterations in. You never see the first 49.&lt;/p&gt;

&lt;p&gt;In the DotLiquid debugger there are no breakpoints. You start at step 1 and click forward. For a template with many iterations you can drag the step slider to jump ahead quickly, but you can't say "stop when &lt;code&gt;item.qty &amp;gt; 10&lt;/code&gt;".&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Backward stepping:&lt;/strong&gt;&lt;br&gt;
The DotLiquid debugger supports backward stepping — you're just moving a cursor through a recording. The XSLT Debugger does not support step back.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Modifying state:&lt;/strong&gt;&lt;br&gt;
Neither debugger supports modify-and-continue. In both cases you edit the template or input and re-render from scratch.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Filter chain tracing:&lt;/strong&gt;&lt;br&gt;
This is where the DotLiquid debugger has an advantage. Because every filter application is recorded as a separate step, you can step through &lt;code&gt;name | Upcase | Truncate: 10 | Append: "…"&lt;/code&gt; and see the value after each filter. XSLT doesn't have filter chains — XPath functions are composed inline and there's no equivalent granularity.&lt;/p&gt;




&lt;h2&gt;
  
  
  Why the Difference Exists
&lt;/h2&gt;

&lt;p&gt;Both XSLT engines provide a path to genuine pause/resume — either through a native TraceListener (Saxon) or through stylesheet rewriting at load time (.NET XSLT 1.0). The key is that XSLT execution is structured: templates fire, instructions execute in sequence, and there are clear entry/exit points to hook into.&lt;/p&gt;

&lt;p&gt;DotLiquid was designed as a simple, safe rendering library. It has no extension points for interrupting execution, and its render loop is a single synchronous call with no observable mid-execution state. The replay approach is the only option available without forking the engine.&lt;/p&gt;




&lt;h2&gt;
  
  
  Which Should You Use?
&lt;/h2&gt;

&lt;p&gt;If you're debugging &lt;strong&gt;XSLT maps&lt;/strong&gt; — especially complex structural transformations, &lt;code&gt;apply-templates&lt;/code&gt; logic, or recursive templates — the live debugger is significantly more powerful. Breakpoints and live state make it practical to debug templates that are hundreds of lines long. Use the &lt;code&gt;compiled&lt;/code&gt; engine for XSLT 1.0 (including inline C#); use Saxon for XSLT 2.0/3.0. The full series is covered in &lt;a href="https://dev.to/imdj/series/33862"&gt;XSLT Debugging in Logic Apps&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;If you're debugging &lt;strong&gt;Liquid maps&lt;/strong&gt; — filter results, conditional branches, loop variable values — the replay model covers the common cases well. The main limitation is the absence of breakpoints; for most Liquid templates this is a minor inconvenience rather than a real blocker. For a deeper look at Liquid templates and the debugger extension, see &lt;a href="https://dev.to/imdj/series/38019"&gt;DotLiquid Debugging in Logic Apps&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>logicapps</category>
      <category>azure</category>
      <category>xslt</category>
      <category>dotliquid</category>
    </item>
    <item>
      <title>Debug DotLiquid Templates Locally with the VS Code DotLiquid Debugger</title>
      <dc:creator>Daniel Jonathan</dc:creator>
      <pubDate>Fri, 03 Apr 2026 09:38:12 +0000</pubDate>
      <link>https://dev.to/imdj/debug-dotliquid-templates-locally-with-the-vs-code-dotliquid-debugger-2eb3</link>
      <guid>https://dev.to/imdj/debug-dotliquid-templates-locally-with-the-vs-code-dotliquid-debugger-2eb3</guid>
      <description>&lt;h2&gt;
  
  
  The Problem We Are Solving
&lt;/h2&gt;

&lt;p&gt;Shopify’s Liquid preview is useful, but Logic Apps Standard runs DotLiquid, not Shopify Liquid.&lt;br&gt;
That means behavior can differ, so a template that looks correct in Shopify preview can still fail in Logic Apps.&lt;/p&gt;

&lt;p&gt;The default Logic App testing loop is slow: update template, execute, wait, inspect run history, repeat.&lt;br&gt;
For real B2B transforms, that guesswork is costly.&lt;/p&gt;

&lt;p&gt;This post shows how to debug DotLiquid locally in VS Code with fast feedback and runtime-accurate results.&lt;/p&gt;


&lt;h2&gt;
  
  
  The Core Idea: Local Preview with the Exact Same Engine
&lt;/h2&gt;

&lt;p&gt;The DotLiquid Debugger VS Code extension runs your templates &lt;strong&gt;locally&lt;/strong&gt;, using the &lt;strong&gt;exact same DotLiquid 2.0.361 engine&lt;/strong&gt; that Azure Logic Apps Standard uses in production.&lt;/p&gt;

&lt;p&gt;This is the critical part: it uses the &lt;strong&gt;exact same engine&lt;/strong&gt; — not a simulation.&lt;/p&gt;

&lt;p&gt;Not a compatible implementation. The same NuGet package, the same version, the same sentence-cased filters, the same &lt;code&gt;content&lt;/code&gt; wrapping behaviour, the same integer division quirks.&lt;/p&gt;

&lt;p&gt;If it works here, your DotLiquid behavior should match Logic Apps closely, with far fewer deployment surprises.&lt;/p&gt;


&lt;h2&gt;
  
  
  How It Works Under the Hood
&lt;/h2&gt;

&lt;p&gt;At a high level, the extension is a thin VS Code UI over a real .NET renderer.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;VS Code Extension (TypeScript)
  ├─ WebView panel (preview UI)
  ├─ Auto-refresh on save / keystroke
  └─ LiquidBackend ──► DotLiquidRenderer.dll (.NET 8)
                             NDJSON over stdin/stdout
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;.NET&lt;/code&gt; renderer is a long-running subprocess (kept alive for performance). It is compiled from source on first use and then kept alive for the session — so after the first warm-up, renders are nearly instant.&lt;/p&gt;

&lt;p&gt;Communication uses NDJSON (one JSON request per line, one response per line) with &lt;code&gt;id&lt;/code&gt;-paired responses, so the TypeScript side can handle concurrent requests without blocking.&lt;/p&gt;




&lt;h2&gt;
  
  
  What You Actually Get
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. Live Preview
&lt;/h3&gt;

&lt;p&gt;Open any &lt;code&gt;.liquid&lt;/code&gt; file and press &lt;code&gt;Ctrl+Shift+L&lt;/code&gt; (or &lt;code&gt;Cmd+Shift+L&lt;/code&gt; on macOS). A preview panel opens beside the editor showing the rendered output in real time.&lt;/p&gt;

&lt;p&gt;The extension auto-refreshes on every save (or on every keystroke, debounced). The round-trip from editing to seeing the output is under 100ms for most templates. You still run after each change, but it happens locally and near-instantly instead of waiting on a Logic App execution.&lt;/p&gt;

&lt;p&gt;This alone removes the biggest bottleneck in Liquid development.&lt;/p&gt;

&lt;p&gt;Input data comes from a paired &lt;code&gt;.liquid.json&lt;/code&gt; file in the same folder:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;sales-order-transform.liquid
sales-order-transform.liquid.json   ← edit this with your test data
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If the input file doesn't exist, a banner offers to create a sample file for you.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F5y8oajwp8nx573omplb8.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F5y8oajwp8nx573omplb8.png" alt=" " width="800" height="487"&gt;&lt;/a&gt;&lt;/p&gt;




&lt;h3&gt;
  
  
  2. Step Debugger
&lt;/h3&gt;

&lt;p&gt;This is where everything changes.&lt;/p&gt;

&lt;p&gt;Click the &lt;strong&gt;Debug&lt;/strong&gt; button in the preview toolbar. The template replays step by step — every &lt;code&gt;assign&lt;/code&gt;, loop iteration, &lt;code&gt;if&lt;/code&gt;/&lt;code&gt;elsif&lt;/code&gt;/&lt;code&gt;else&lt;/code&gt;, and output chunk gets its own step.&lt;/p&gt;

&lt;p&gt;At each step:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The source line is highlighted in the editor&lt;/li&gt;
&lt;li&gt;The Variables panel shows every variable and its current value&lt;/li&gt;
&lt;li&gt;Variables not yet assigned are dimmed, so you see exactly what exists at this point in execution&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Use the slider or &lt;strong&gt;Prev/Next&lt;/strong&gt; buttons to navigate. Jump to the first or last step with &lt;code&gt;|◀&lt;/code&gt; and &lt;code&gt;▶|&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;You can finally see &lt;strong&gt;why&lt;/strong&gt; a value is wrong — not just that it is wrong.&lt;/p&gt;

&lt;p&gt;This turns "why is this variable wrong?" from a guessing game into a one-minute diagnosis.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fdxafuob6yklcbyxkyjqa.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fdxafuob6yklcbyxkyjqa.png" alt=" " width="800" height="473"&gt;&lt;/a&gt;&lt;/p&gt;




&lt;h3&gt;
  
  
  3. Filter Chain Tracing
&lt;/h3&gt;

&lt;p&gt;Every &lt;code&gt;assign&lt;/code&gt; step that uses filters shows the full chain below the debug bar:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;499.9 | Times:5 → 2499.5 | DividedBy:100 → 24.995 | Round:2 → 25.0
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;31 filters are covered: all the math filters (&lt;code&gt;Times&lt;/code&gt;, &lt;code&gt;DividedBy&lt;/code&gt;, &lt;code&gt;Plus&lt;/code&gt;, &lt;code&gt;Minus&lt;/code&gt;, &lt;code&gt;Modulo&lt;/code&gt;, &lt;code&gt;Round&lt;/code&gt;, &lt;code&gt;Ceil&lt;/code&gt;, &lt;code&gt;Floor&lt;/code&gt;, &lt;code&gt;Abs&lt;/code&gt;, &lt;code&gt;AtLeast&lt;/code&gt;, &lt;code&gt;AtMost&lt;/code&gt;), string filters (&lt;code&gt;Upcase&lt;/code&gt;, &lt;code&gt;Downcase&lt;/code&gt;, &lt;code&gt;Capitalize&lt;/code&gt;, &lt;code&gt;Append&lt;/code&gt;, &lt;code&gt;Prepend&lt;/code&gt;, &lt;code&gt;Strip&lt;/code&gt;, &lt;code&gt;Replace&lt;/code&gt;, &lt;code&gt;Truncate&lt;/code&gt;, etc.), and array filters (&lt;code&gt;Split&lt;/code&gt;, &lt;code&gt;Join&lt;/code&gt;, &lt;code&gt;First&lt;/code&gt;, &lt;code&gt;Last&lt;/code&gt;, &lt;code&gt;Sort&lt;/code&gt;, &lt;code&gt;Map&lt;/code&gt;, &lt;code&gt;Reverse&lt;/code&gt;, &lt;code&gt;Size&lt;/code&gt;).&lt;/p&gt;

&lt;p&gt;This is something you simply cannot do in Azure Logic Apps.&lt;/p&gt;

&lt;p&gt;Most calculation bugs live in filter chains — this feature isolates them instantly.&lt;/p&gt;

&lt;p&gt;This is especially useful when a chain of four or five filters produces an unexpected result — you can see exactly where the value diverged.&lt;/p&gt;




&lt;h3&gt;
  
  
  4. Condition Evaluation
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;if&lt;/code&gt;, &lt;code&gt;elsif&lt;/code&gt;, &lt;code&gt;else&lt;/code&gt;, &lt;code&gt;unless&lt;/code&gt;, and &lt;code&gt;when&lt;/code&gt; steps show the condition that was evaluated and a &lt;strong&gt;✓ taken&lt;/strong&gt; indicator if the branch was taken:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight liquid"&gt;&lt;code&gt;? if: content.priority == "HIGH"   ✓ taken
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Un-taken branches produce no step at all — which matches DotLiquid's execution model exactly.&lt;/p&gt;

&lt;p&gt;This makes it obvious why a branch did or did not execute.&lt;/p&gt;




&lt;h3&gt;
  
  
  5. Variable Panel and Line Map
&lt;/h3&gt;

&lt;p&gt;The &lt;strong&gt;Variables panel&lt;/strong&gt; lists every &lt;code&gt;assign&lt;/code&gt; value with the line number where it was last set. Click any row to jump to that line in the source.&lt;/p&gt;

&lt;p&gt;The &lt;strong&gt;Line Map&lt;/strong&gt; links every output chunk back to the template line that produced it. Click any output region to jump to the source line. This is especially useful for templates that produce XML or JSON — you can click &lt;code&gt;"grandTotal": 336.20&lt;/code&gt; in the output and jump straight to the line that calculated it.&lt;/p&gt;

&lt;p&gt;Both panels collapse independently. When one collapses, the other expands to fill the full sidebar height.&lt;/p&gt;

&lt;p&gt;Together, these eliminate the need for "temporary debug output" hacks.&lt;/p&gt;




&lt;h3&gt;
  
  
  6. Output Formats: JSON, XML, HTML, Plain Text
&lt;/h3&gt;

&lt;p&gt;Because the extension renders raw text output, it works equally well for all four output formats Logic Apps supports:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Template&lt;/th&gt;
&lt;th&gt;Output&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;invoice-flat.liquid&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Flat JSON for downstream API&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;sales-order-transform.liquid&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Complex JSON with B2B calculations&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;order-to-xml.liquid&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;XML for EDI / ERP integration&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;customer-to-xml.liquid&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;XML ERP customer import&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;email-notification.liquid&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;HTML order confirmation email&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;shipping-label.liquid&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Plain text packing slip&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;




&lt;h2&gt;
  
  
  Getting Started
&lt;/h2&gt;

&lt;p&gt;Setup takes under 2 minutes.&lt;/p&gt;

&lt;h3&gt;
  
  
  Requirements
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;VS Code 1.85 or later&lt;/li&gt;
&lt;li&gt;.NET 8 SDK (&lt;a href="https://dotnet.microsoft.com/download" rel="noopener noreferrer"&gt;download&lt;/a&gt;)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Verify .NET:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;dotnet &lt;span class="nt"&gt;--version&lt;/span&gt;   &lt;span class="c"&gt;# should print 8.x.x or later&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Install
&lt;/h3&gt;

&lt;p&gt;Install from the VS Code Marketplace:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;code &lt;span class="nt"&gt;--install-extension&lt;/span&gt; danieljonathan.dotliquid-template-debugger
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Or download the latest &lt;code&gt;.vsix&lt;/code&gt; from &lt;a href="https://github.com/imdj360/VSCodeDotLiquidDebugger" rel="noopener noreferrer"&gt;github.com/imdj360/VSCodeDotLiquidDebugger&lt;/a&gt; and install it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;code &lt;span class="nt"&gt;--install-extension&lt;/span&gt; ./dotliquid-template-debugger-0.5.0.vsix
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You can also search &lt;strong&gt;DotLiquid Template Debugger for Logic Apps&lt;/strong&gt; in the VS Code Extensions panel.&lt;/p&gt;

&lt;h3&gt;
  
  
  Your First Preview
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;Create a file &lt;code&gt;hello.liquid&lt;/code&gt;:
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight liquid"&gt;&lt;code&gt;&lt;span class="cp"&gt;{%-&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;assign&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;name&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;content&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nv"&gt;name&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="cp"&gt;-%}&lt;/span&gt;
&lt;span class="cp"&gt;{%-&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;assign&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;items&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;content&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nv"&gt;items&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="cp"&gt;-%}&lt;/span&gt;
&lt;span class="cp"&gt;{%-&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;assign&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;total&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="cp"&gt;-%}&lt;/span&gt;
&lt;span class="cp"&gt;{%-&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;for&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;item&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;in&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;items&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="cp"&gt;-%}&lt;/span&gt;
  &lt;span class="cp"&gt;{%-&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;assign&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;total&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;total&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;|&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;Plus&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;item&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nv"&gt;price&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="cp"&gt;-%}&lt;/span&gt;
&lt;span class="cp"&gt;{%-&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;endfor&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="cp"&gt;-%}&lt;/span&gt;
{
  "greeting": "Hello, &lt;span class="cp"&gt;{{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;name&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="cp"&gt;}}&lt;/span&gt;!",
  "itemCount": &lt;span class="cp"&gt;{{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;items&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;|&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;Size&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="cp"&gt;}}&lt;/span&gt;,
  "total": &lt;span class="cp"&gt;{{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;total&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;|&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;Round&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="cp"&gt;}}&lt;/span&gt;
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ol&gt;
&lt;li&gt;Create &lt;code&gt;hello.liquid.json&lt;/code&gt; in the same folder:
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"World"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"items"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"price"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;9.99&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"price"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;14.50&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"price"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;4.00&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ol&gt;
&lt;li&gt;Press &lt;code&gt;Ctrl+Shift+L&lt;/code&gt; to open the preview. You should see:
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"greeting"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Hello, World!"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"itemCount"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"total"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;28.49&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;At this point, each change can be validated with a quick local run instead of rerunning the Logic App each iteration.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Press &lt;strong&gt;Debug&lt;/strong&gt; and step through to watch &lt;code&gt;total&lt;/code&gt; accumulate across the loop.&lt;/li&gt;
&lt;/ol&gt;




&lt;h2&gt;
  
  
  The F5 Launch Config Workflow
&lt;/h2&gt;

&lt;p&gt;For a proper development workflow — especially when working with multiple templates — add a &lt;code&gt;type: "dotliquid"&lt;/code&gt; launch configuration to your &lt;code&gt;.vscode/launch.json&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"dotliquid"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"request"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"launch"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"My: sales-order-transform"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"template"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"${workspaceFolder}/templates/sales-order-transform.liquid"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now press &lt;strong&gt;F5&lt;/strong&gt; from the Run and Debug panel. The extension:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Opens the template in the editor&lt;/li&gt;
&lt;li&gt;Auto-detects the paired &lt;code&gt;.liquid.json&lt;/code&gt; input&lt;/li&gt;
&lt;li&gt;Opens the preview panel&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;No manual reload. No "Developer: Reload Window". Just F5.&lt;/p&gt;

&lt;p&gt;This is the closest thing to a real development experience for Liquid templates.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;input&lt;/code&gt; field can be provided in launch config, but current preview execution reads the paired &lt;code&gt;&amp;lt;template&amp;gt;.liquid.json&lt;/code&gt; file.&lt;/p&gt;




&lt;h2&gt;
  
  
  Sample Templates
&lt;/h2&gt;

&lt;p&gt;Seven ready-to-use sample templates covering all output formats are available in a companion repository. Clone it and open it in VS Code:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;GitHub:&lt;/strong&gt; &lt;a href="https://github.com/imdj360/Xslt-Liquid-DebuggerTestFiles" rel="noopener noreferrer"&gt;Xslt-Liquid-DebuggerTestFiles&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Each template has a paired &lt;code&gt;.liquid.json&lt;/code&gt; input file. Pick any &lt;strong&gt;Liquid:&lt;/strong&gt; config from the Run and Debug dropdown and press F5.&lt;/p&gt;




&lt;h2&gt;
  
  
  A Typical Debugging Session
&lt;/h2&gt;

&lt;p&gt;Here is a real example: &lt;code&gt;grandTotal&lt;/code&gt; shows &lt;code&gt;0&lt;/code&gt; instead of &lt;code&gt;336.20&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Without local debugging, you update the template, run the Logic App, wait, check output, and repeat.&lt;/p&gt;

&lt;p&gt;With the extension:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Press &lt;strong&gt;Debug&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;Go to &lt;code&gt;assign grandTotal&lt;/code&gt; and check the filter chain.&lt;/li&gt;
&lt;li&gt;See &lt;code&gt;couponAmt&lt;/code&gt; is &lt;code&gt;0&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Step back to &lt;code&gt;couponDiscount&lt;/code&gt; and confirm it is &lt;code&gt;0&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Step back again and see &lt;code&gt;if content.couponCode == "WINTER20"&lt;/code&gt; was not taken.&lt;/li&gt;
&lt;li&gt;Check input JSON: &lt;code&gt;couponCode&lt;/code&gt; is &lt;code&gt;"winter20"&lt;/code&gt; (lowercase).&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Done in minutes, with local runs between changes.&lt;/p&gt;




&lt;h2&gt;
  
  
  Where Local Debugging Fits
&lt;/h2&gt;

&lt;p&gt;Use local runs for fast iteration.&lt;br&gt;
Use Logic App execution for final validation.&lt;/p&gt;




&lt;h2&gt;
  
  
  Recommended Workflow
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;Write your template in VS Code.&lt;/li&gt;
&lt;li&gt;Run locally with &lt;code&gt;.liquid.json&lt;/code&gt; input.&lt;/li&gt;
&lt;li&gt;Debug until correct.&lt;/li&gt;
&lt;li&gt;Deploy and validate in Logic Apps.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Local inner loop: update → run locally → inspect → repeat.&lt;/p&gt;




&lt;p&gt;Liquid works best when you can see what it is doing.&lt;/p&gt;




&lt;h2&gt;
  
  
  Links
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Source &amp;amp; VSIX&lt;/strong&gt;: &lt;a href="https://github.com/imdj360/VSCodeDotLiquidDebugger" rel="noopener noreferrer"&gt;github.com/imdj360/VSCodeDotLiquidDebugger&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Marketplace&lt;/strong&gt;: &lt;a href="https://marketplace.visualstudio.com/items?itemName=danieljonathan.dotliquid-template-debugger" rel="noopener noreferrer"&gt;DotLiquid Template Debugger for Logic Apps&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Sample Templates&lt;/strong&gt;: &lt;a href="https://github.com/imdj360/Xslt-Liquid-DebuggerTestFiles" rel="noopener noreferrer"&gt;github.com/imdj360/Xslt-Liquid-DebuggerTestFiles&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;&lt;em&gt;Also check out the XSLT Debugger — a sister extension for debugging XSLT transforms in Logic Apps Standard: &lt;a href="https://marketplace.visualstudio.com/items?itemName=DanielJonathan.xsltdebugger-darwin" rel="noopener noreferrer"&gt;macOS (arm64)&lt;/a&gt; · &lt;a href="https://marketplace.visualstudio.com/items?itemName=DanielJonathan.xsltdebugger-windows" rel="noopener noreferrer"&gt;Windows&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

</description>
      <category>logicapps</category>
      <category>azure</category>
      <category>dotnet</category>
      <category>dotliquid</category>
    </item>
    <item>
      <title>Liquid Templates in Azure Logic Apps: What They Are and Why They Matter</title>
      <dc:creator>Daniel Jonathan</dc:creator>
      <pubDate>Fri, 03 Apr 2026 09:37:56 +0000</pubDate>
      <link>https://dev.to/imdj/liquid-templates-in-azure-logic-apps-what-they-are-and-why-they-matter-323e</link>
      <guid>https://dev.to/imdj/liquid-templates-in-azure-logic-apps-what-they-are-and-why-they-matter-323e</guid>
      <description>&lt;h2&gt;
  
  
  The Problem Every Logic Apps Developer Hits
&lt;/h2&gt;

&lt;p&gt;You are building an integration on Azure Logic Apps Standard. The upstream system sends you a rich, nested JSON payload — a sales order with line items, discount codes, shipping methods, and state-specific tax rates. The downstream system expects a flat, transformed structure with calculated totals, a carrier label, and an SLA timestamp.&lt;/p&gt;

&lt;p&gt;The built-in expression language gets you partway there. But the moment you need a loop, a conditional lookup table, or a running subtotal across items, you hit a wall.&lt;/p&gt;

&lt;p&gt;That is the moment you reach for &lt;strong&gt;Liquid templates&lt;/strong&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  What Is Liquid?
&lt;/h2&gt;

&lt;p&gt;Liquid is an open-source template language originally created by Shopify. It is designed to be safe, sandboxed, and easy to read — output is produced by mixing static text with template tags that reference data.&lt;/p&gt;

&lt;p&gt;There are two kinds of tags:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight liquid"&gt;&lt;code&gt;&lt;span class="cp"&gt;{{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;customer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nv"&gt;firstName&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="cp"&gt;}}&lt;/span&gt;          /* output tag — renders a value */
&lt;span class="cp"&gt;{%&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;order&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nv"&gt;total&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1000&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="cp"&gt;%}&lt;/span&gt;       /* logic tag — controls flow */
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; The &lt;code&gt;/* ... */&lt;/code&gt; markers above are code annotations for readability only — they are not valid Liquid syntax. DotLiquid comments use &lt;code&gt;{% comment %}...{% endcomment %}&lt;/code&gt;.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Variables, loops, conditionals, and filters cover most transformation needs without requiring you to write C# or JavaScript.&lt;/p&gt;




&lt;h2&gt;
  
  
  DotLiquid: The .NET Flavour Used by Logic Apps
&lt;/h2&gt;

&lt;p&gt;Azure Logic Apps Standard does not use the original Ruby Liquid gem. It uses &lt;strong&gt;DotLiquid&lt;/strong&gt;, a .NET port. This extension targets DotLiquid &lt;strong&gt;2.0.361&lt;/strong&gt;, which matches the version used by Logic Apps Standard at the time of writing.&lt;/p&gt;

&lt;p&gt;This is not a minor implementation detail. There are several behavioural differences that will catch you off guard if you are used to standard Liquid or testing with an online Liquid playground.&lt;/p&gt;




&lt;h3&gt;
  
  
  Difference 1 — Filter names are sentence-cased
&lt;/h3&gt;

&lt;p&gt;Standard Liquid uses lowercase filter names. DotLiquid uses sentence case.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Standard Liquid&lt;/th&gt;
&lt;th&gt;DotLiquid (Logic Apps)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;upcase&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;Upcase&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;downcase&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;Downcase&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;divided_by&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;DividedBy&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;times&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;Times&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;round&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;Round&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;split&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;Split&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;join&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;Join&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;sort&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;Sort&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;map&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;Map&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;replace_first&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;ReplaceFirst&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;truncate&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;Truncate&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;strip_html&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;StripHtml&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;url_encode&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;UrlEncode&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;If you use the wrong casing, the filter is silently ignored — the value passes through unchanged with no error.&lt;/p&gt;




&lt;h3&gt;
  
  
  Difference 2 — &lt;code&gt;DividedBy&lt;/code&gt; truncates when both operands are whole numbers
&lt;/h3&gt;

&lt;p&gt;This is the most common source of silent calculation errors.&lt;/p&gt;

&lt;p&gt;Both standard Liquid and DotLiquid produce an integer result when the divisor is a whole number — this is not a DotLiquid-specific quirk, it is how both implementations work:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight liquid"&gt;&lt;code&gt;&lt;span class="cp"&gt;{{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;7&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;|&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;divided_by&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="cp"&gt;}}&lt;/span&gt;   /* standard Liquid: outputs 3, not 3.5 */
&lt;span class="cp"&gt;{{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;7&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;|&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;DividedBy&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="cp"&gt;}}&lt;/span&gt;    /* DotLiquid: outputs 3, not 3.5 */
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Output: &lt;code&gt;3&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;To get decimal output, make the divisor a float:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight liquid"&gt;&lt;code&gt;&lt;span class="cp"&gt;{{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;7&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;|&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;DividedBy&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;2.0&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="cp"&gt;}}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Output: &lt;code&gt;3.5&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;This matters for percentage calculations. &lt;code&gt;total | Times: taxRate | DividedBy: 100&lt;/code&gt; will truncate if the running value is a whole number — use &lt;code&gt;100.0&lt;/code&gt; instead.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Subtle difference for negative numbers:&lt;/strong&gt; Standard Liquid (Ruby) uses &lt;em&gt;floor division&lt;/em&gt; (&lt;code&gt;-7 / 2 = -4&lt;/code&gt;), while DotLiquid (C#) uses &lt;em&gt;truncation&lt;/em&gt; (&lt;code&gt;-7 / 2 = -3&lt;/code&gt;). For positive numbers the results are identical, but if your data can contain negative values this divergence is worth knowing.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h3&gt;
  
  
  Difference 3 — &lt;code&gt;Sort&lt;/code&gt; is case-insensitive
&lt;/h3&gt;

&lt;p&gt;Standard Liquid sorts with case-sensitive comparison — uppercase letters sort before lowercase in ASCII/ordinal order (&lt;code&gt;B&lt;/code&gt; &amp;lt; &lt;code&gt;a&lt;/code&gt;), so &lt;code&gt;["Banana", "apple"]&lt;/code&gt; would sort to &lt;code&gt;["Banana", "apple"]&lt;/code&gt;. DotLiquid sorts case-insensitively (&lt;code&gt;OrdinalIgnoreCase&lt;/code&gt;), so the same array becomes &lt;code&gt;["apple", "Banana"]&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;If you are sorting strings and the casing matters to the sort order, be aware the result will differ from a Ruby Liquid environment.&lt;/p&gt;




&lt;h3&gt;
  
  
  Difference 4 — Date format uses .NET format strings, not strftime
&lt;/h3&gt;

&lt;p&gt;Standard Liquid uses &lt;code&gt;strftime&lt;/code&gt; format codes:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight liquid"&gt;&lt;code&gt;&lt;span class="cp"&gt;{{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"now"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;|&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;date&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"%Y-%m-%d"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="cp"&gt;}}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;DotLiquid uses .NET format strings:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight liquid"&gt;&lt;code&gt;&lt;span class="cp"&gt;{{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"now"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;|&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"yyyy-MM-dd"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="cp"&gt;}}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The filter name is also sentence-cased (&lt;code&gt;Date&lt;/code&gt;, not &lt;code&gt;date&lt;/code&gt;).&lt;/p&gt;




&lt;h3&gt;
  
  
  Difference 5 — Some standard filters are missing or renamed
&lt;/h3&gt;

&lt;p&gt;Some standard Liquid filters are not available in DotLiquid 2.0.361 at all; others exist but only under their sentence-cased name:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Standard Liquid filter&lt;/th&gt;
&lt;th&gt;DotLiquid equivalent&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;compact&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;Compact&lt;/code&gt; (available, sentence-cased)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;uniq&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;Uniq&lt;/code&gt; (available, sentence-cased)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;sort_natural&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Not available&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;where&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Not available&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;find&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Not available&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;sum&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Not available&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;If your template relies on these, you will need to work around them with loops and assign statements.&lt;/p&gt;




&lt;h3&gt;
  
  
  Difference 6 — The &lt;code&gt;content&lt;/code&gt; wrapper
&lt;/h3&gt;

&lt;p&gt;When Logic Apps Standard invokes a Liquid transform, the input JSON is wrapped in a &lt;code&gt;content&lt;/code&gt; object:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"content"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;...your&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;actual&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;payload...&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;So if your input is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"orderNumber"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"ORD-001"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"total"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;149.99&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Inside your template you access it as:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight liquid"&gt;&lt;code&gt;&lt;span class="cp"&gt;{{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;content&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nv"&gt;orderNumber&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="cp"&gt;}}&lt;/span&gt;
&lt;span class="cp"&gt;{{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;content&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nv"&gt;total&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="cp"&gt;}}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Miss this and every variable renders blank with no error message.&lt;/p&gt;




&lt;h3&gt;
  
  
  Quick reference
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Behaviour&lt;/th&gt;
&lt;th&gt;Standard Liquid&lt;/th&gt;
&lt;th&gt;DotLiquid 2.0.361&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Filter casing&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;upcase&lt;/code&gt;, &lt;code&gt;downcase&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;Upcase&lt;/code&gt;, &lt;code&gt;Downcase&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Integer division&lt;/td&gt;
&lt;td&gt;Truncates when divisor is integer (same behaviour)&lt;/td&gt;
&lt;td&gt;Truncates when both operands are whole numbers; floor vs truncation differs for negatives&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Sort order&lt;/td&gt;
&lt;td&gt;Case-sensitive&lt;/td&gt;
&lt;td&gt;Case-insensitive&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Date format&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;strftime&lt;/code&gt; (&lt;code&gt;%Y-%m-%d&lt;/code&gt;)&lt;/td&gt;
&lt;td&gt;.NET format (&lt;code&gt;yyyy-MM-dd&lt;/code&gt;)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Input root&lt;/td&gt;
&lt;td&gt;Direct access&lt;/td&gt;
&lt;td&gt;Wrapped in &lt;code&gt;content.*&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;compact&lt;/code&gt; / &lt;code&gt;uniq&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Available (&lt;code&gt;compact&lt;/code&gt;, &lt;code&gt;uniq&lt;/code&gt;)&lt;/td&gt;
&lt;td&gt;Available (&lt;code&gt;Compact&lt;/code&gt;, &lt;code&gt;Uniq&lt;/code&gt;)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;sort_natural&lt;/code&gt; / &lt;code&gt;where&lt;/code&gt; / &lt;code&gt;sum&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Available&lt;/td&gt;
&lt;td&gt;Not available&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;




&lt;h2&gt;
  
  
  The Core Syntax You Will Use Daily
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Output
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight liquid"&gt;&lt;code&gt;&lt;span class="cp"&gt;{{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;content&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nv"&gt;customer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nv"&gt;firstName&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="cp"&gt;}}&lt;/span&gt;
&lt;span class="cp"&gt;{{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;content&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nv"&gt;order&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nv"&gt;total&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;|&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;Round&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="cp"&gt;}}&lt;/span&gt;
&lt;span class="cp"&gt;{{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;content&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nv"&gt;status&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;|&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;Upcase&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="cp"&gt;}}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Assign (variables)
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight liquid"&gt;&lt;code&gt;&lt;span class="cp"&gt;{%-&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;assign&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;subtotal&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="cp"&gt;-%}&lt;/span&gt;
&lt;span class="cp"&gt;{%-&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;assign&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;taxRate&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;0.085&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="cp"&gt;-%}&lt;/span&gt;
&lt;span class="cp"&gt;{%-&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;assign&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;tax&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;subtotal&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;|&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;Times&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;taxRate&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;|&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;Round&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="cp"&gt;-%}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;-&lt;/code&gt; inside &lt;code&gt;{%-&lt;/code&gt; and &lt;code&gt;-%}&lt;/code&gt; strips the surrounding whitespace — essential for keeping JSON output clean.&lt;/p&gt;

&lt;h3&gt;
  
  
  Loops
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight liquid"&gt;&lt;code&gt;&lt;span class="cp"&gt;{%-&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;for&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;item&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;in&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;content&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nv"&gt;items&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="cp"&gt;-%}&lt;/span&gt;
  &lt;span class="cp"&gt;{%-&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;assign&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;lineTotal&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;item&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nv"&gt;quantity&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;|&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;Times&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;item&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nv"&gt;unitPrice&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="cp"&gt;-%}&lt;/span&gt;
  &lt;span class="cp"&gt;{%-&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;assign&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;subtotal&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;subtotal&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;|&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;Plus&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;lineTotal&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="cp"&gt;-%}&lt;/span&gt;
&lt;span class="cp"&gt;{%-&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;endfor&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="cp"&gt;-%}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Conditionals
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight liquid"&gt;&lt;code&gt;&lt;span class="cp"&gt;{%-&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;content&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nv"&gt;priority&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;==&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"CRITICAL"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="cp"&gt;-%}&lt;/span&gt;
  &lt;span class="cp"&gt;{%-&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;assign&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;slaHours&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="cp"&gt;-%}&lt;/span&gt;
&lt;span class="cp"&gt;{%-&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;elsif&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;content&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nv"&gt;priority&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;==&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"HIGH"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="cp"&gt;-%}&lt;/span&gt;
  &lt;span class="cp"&gt;{%-&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;assign&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;slaHours&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;24&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="cp"&gt;-%}&lt;/span&gt;
&lt;span class="cp"&gt;{%-&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;else&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="cp"&gt;-%}&lt;/span&gt;
  &lt;span class="cp"&gt;{%-&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;assign&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;slaHours&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;72&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="cp"&gt;-%}&lt;/span&gt;
&lt;span class="cp"&gt;{%-&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;endif&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="cp"&gt;-%}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Filter chains
&lt;/h3&gt;

&lt;p&gt;Filters are chained left to right. Each filter receives the output of the previous one:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight liquid"&gt;&lt;code&gt;&lt;span class="cp"&gt;{%-&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;assign&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;grandTotal&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;subtotal&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;|&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;Minus&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;discount&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;|&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;Plus&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;tax&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;|&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;Round&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="cp"&gt;-%}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  A Real-World Example: B2B Sales Order Transform
&lt;/h2&gt;

&lt;p&gt;Here is a condensed version of a real Logic Apps transformation. The input is a B2B order with line items, a coupon code, and a shipping method. The output is a flat JSON structure ready for an ERP system.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Input (excerpt):&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"couponCode"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"WINTER20"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"header"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"priority"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"HIGH"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"shipping"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"method"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"FEDEX_GROUND"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"lines"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"category"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"HARDWARE"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"quantity"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"unitPrice"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;24.50&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"discountPct"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"category"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"SOFTWARE"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"quantity"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"unitPrice"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;99.00&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"discountPct"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Template (excerpt):&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight liquid"&gt;&lt;code&gt;&lt;span class="cp"&gt;{%-&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;assign&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;couponDiscount&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="cp"&gt;-%}&lt;/span&gt;
&lt;span class="cp"&gt;{%-&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;content&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nv"&gt;couponCode&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;==&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"WINTER20"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="cp"&gt;-%}{%-&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;assign&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;couponDiscount&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;20&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="cp"&gt;-%}&lt;/span&gt;
&lt;span class="cp"&gt;{%-&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;elsif&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;content&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nv"&gt;couponCode&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;==&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"SAVE10"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="cp"&gt;-%}{%-&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;assign&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;couponDiscount&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="cp"&gt;-%}&lt;/span&gt;
&lt;span class="cp"&gt;{%-&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;endif&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="cp"&gt;-%}&lt;/span&gt;

&lt;span class="cp"&gt;{%-&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;assign&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;slaHours&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;72&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="cp"&gt;-%}&lt;/span&gt;
&lt;span class="cp"&gt;{%-&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;content&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nv"&gt;header&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nv"&gt;priority&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;==&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"HIGH"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="cp"&gt;-%}{%-&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;assign&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;slaHours&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;24&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="cp"&gt;-%}&lt;/span&gt;
&lt;span class="cp"&gt;{%-&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;endif&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="cp"&gt;-%}&lt;/span&gt;

&lt;span class="cp"&gt;{%-&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;assign&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;carrier&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"FedEx"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="cp"&gt;-%}&lt;/span&gt;
&lt;span class="cp"&gt;{%-&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;assign&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;serviceLevel&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Ground"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="cp"&gt;-%}&lt;/span&gt;

&lt;span class="cp"&gt;{%-&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;assign&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;subtotal&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="cp"&gt;-%}&lt;/span&gt;
&lt;span class="cp"&gt;{%-&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;for&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;line&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;in&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;content&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nv"&gt;lines&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="cp"&gt;-%}&lt;/span&gt;
  &lt;span class="cp"&gt;{%-&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;assign&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;grossLine&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;line&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nv"&gt;quantity&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;|&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;Times&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;line&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nv"&gt;unitPrice&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="cp"&gt;-%}&lt;/span&gt;
  &lt;span class="cp"&gt;{%-&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;assign&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;lineDiscount&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;grossLine&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;|&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;Times&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;line&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nv"&gt;discountPct&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;|&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;DividedBy&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="cp"&gt;-%}&lt;/span&gt;
  &lt;span class="cp"&gt;{%-&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;assign&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;netLine&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;grossLine&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;|&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;Minus&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;lineDiscount&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="cp"&gt;-%}&lt;/span&gt;
  &lt;span class="cp"&gt;{%-&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;assign&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;subtotal&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;subtotal&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;|&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;Plus&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;netLine&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="cp"&gt;-%}&lt;/span&gt;
&lt;span class="cp"&gt;{%-&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;endfor&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="cp"&gt;-%}&lt;/span&gt;

&lt;span class="cp"&gt;{%-&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;assign&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;couponAmt&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;subtotal&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;|&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;Times&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;couponDiscount&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;|&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;DividedBy&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="cp"&gt;-%}&lt;/span&gt;
&lt;span class="cp"&gt;{%-&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;assign&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;grandTotal&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;subtotal&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;|&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;Minus&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;couponAmt&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;|&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;Round&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="cp"&gt;-%}&lt;/span&gt;

{
  "carrier": "&lt;span class="cp"&gt;{{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;carrier&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="cp"&gt;}}&lt;/span&gt;",
  "serviceLevel": "&lt;span class="cp"&gt;{{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;serviceLevel&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="cp"&gt;}}&lt;/span&gt;",
  "slaHours": &lt;span class="cp"&gt;{{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;slaHours&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="cp"&gt;}}&lt;/span&gt;,
  "subtotal": &lt;span class="cp"&gt;{{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;subtotal&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;|&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;Round&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="cp"&gt;}}&lt;/span&gt;,
  "couponDiscount": &lt;span class="cp"&gt;{{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;couponAmt&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;|&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;Round&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="cp"&gt;}}&lt;/span&gt;,
  "grandTotal": &lt;span class="cp"&gt;{{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;grandTotal&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="cp"&gt;}}&lt;/span&gt;
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Output:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"carrier"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"FedEx"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"serviceLevel"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Ground"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"slaHours"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;24&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"subtotal"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;407.25&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"couponDiscount"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;81.45&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"grandTotal"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;325.8&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Output Is Not Just JSON
&lt;/h2&gt;

&lt;p&gt;A common misconception is that Logic Apps Liquid transforms only produce JSON. The template engine outputs whatever text the template produces. The three other common output formats:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;XML&lt;/strong&gt; — for B2B, EDI, or ERP integrations:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight liquid"&gt;&lt;code&gt;&amp;lt;?xml version="1.0" encoding="UTF-8"?&amp;gt;
&amp;lt;PurchaseOrder id="&lt;span class="cp"&gt;{{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;content&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nv"&gt;orderNumber&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="cp"&gt;}}&lt;/span&gt;"&amp;gt;
  &amp;lt;Vendor&amp;gt;&lt;span class="cp"&gt;{{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;content&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nv"&gt;vendor&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nv"&gt;name&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="cp"&gt;}}&lt;/span&gt;&amp;lt;/Vendor&amp;gt;
  &lt;span class="cp"&gt;{%-&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;for&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;item&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;in&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;content&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nv"&gt;lines&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="cp"&gt;-%}&lt;/span&gt;
  &amp;lt;LineItem sku="&lt;span class="cp"&gt;{{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;item&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nv"&gt;sku&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="cp"&gt;}}&lt;/span&gt;"&amp;gt;&lt;span class="cp"&gt;{{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;item&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nv"&gt;quantity&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="cp"&gt;}}&lt;/span&gt;&amp;lt;/LineItem&amp;gt;
  &lt;span class="cp"&gt;{%-&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;endfor&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="cp"&gt;-%}&lt;/span&gt;
&amp;lt;/PurchaseOrder&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;HTML&lt;/strong&gt; — for email bodies via SendGrid or similar:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight liquid"&gt;&lt;code&gt;&amp;lt;h1&amp;gt;Order Confirmation&amp;lt;/h1&amp;gt;
&amp;lt;p&amp;gt;Hi &lt;span class="cp"&gt;{{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;content&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nv"&gt;customer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nv"&gt;firstName&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="cp"&gt;}}&lt;/span&gt;,&amp;lt;/p&amp;gt;
&amp;lt;p&amp;gt;Your order total is $&lt;span class="cp"&gt;{{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;content&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nv"&gt;total&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;|&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;Round&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="cp"&gt;}}&lt;/span&gt;.&amp;lt;/p&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Plain text&lt;/strong&gt; — for warehouse printers, SMS, or legacy systems:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight liquid"&gt;&lt;code&gt;TRACKING: &lt;span class="cp"&gt;{{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;content&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nv"&gt;trackingNumber&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="cp"&gt;}}&lt;/span&gt;
SHIP TO:  &lt;span class="cp"&gt;{{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;content&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nv"&gt;recipient&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nv"&gt;name&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="cp"&gt;}}&lt;/span&gt;
          &lt;span class="cp"&gt;{{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;content&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nv"&gt;recipient&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nv"&gt;address&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nv"&gt;line1&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="cp"&gt;}}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  The Pain: Flying Blind
&lt;/h2&gt;

&lt;p&gt;Everything described above sounds straightforward — until you actually try to build it inside Logic Apps Studio.&lt;/p&gt;

&lt;p&gt;The typical development loop looks like this:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Write the template in the Azure Portal or a text editor&lt;/li&gt;
&lt;li&gt;Upload it to your Logic Apps integration account&lt;/li&gt;
&lt;li&gt;Deploy or trigger a test run&lt;/li&gt;
&lt;li&gt;Wait for the run history to populate&lt;/li&gt;
&lt;li&gt;Expand the transform action output&lt;/li&gt;
&lt;li&gt;Discover the output is wrong — or blank&lt;/li&gt;
&lt;li&gt;Guess which line is the problem&lt;/li&gt;
&lt;li&gt;Repeat&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;There is no syntax highlighting. No inline error messages. No way to inspect what a variable holds mid-template. A misnamed filter (&lt;code&gt;upcase&lt;/code&gt; instead of &lt;code&gt;Upcase&lt;/code&gt;) is silently ignored — the value passes through unchanged with no warning. A wrong dot-path (&lt;code&gt;content.order.total&lt;/code&gt; instead of &lt;code&gt;content.total&lt;/code&gt;) does the same.&lt;/p&gt;

&lt;p&gt;The round-trip from "edit" to "see output" can take five minutes or more per iteration for a non-trivial template.&lt;/p&gt;




&lt;h2&gt;
  
  
  What Comes Next
&lt;/h2&gt;

&lt;p&gt;In Part 2, we look at the &lt;strong&gt;DotLiquid Debugger&lt;/strong&gt; VS Code extension — a tool that replaces the Azure Portal round-trip with instant local feedback, a step-by-step debugger, and filter chain tracing, all using the same DotLiquid 2.0.361 engine that Logic Apps Standard runs in production.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Part 2: &lt;a href="//./part2-dotliquid-debugger-extension.md"&gt;Debug DotLiquid Templates Locally with the VS Code DotLiquid Debugger&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

</description>
      <category>logicapps</category>
      <category>dotnet</category>
      <category>dotliquid</category>
      <category>azure</category>
    </item>
    <item>
      <title>Teaching Coding Agent to Write XSLT — The Hook in Action</title>
      <dc:creator>Daniel Jonathan</dc:creator>
      <pubDate>Sat, 28 Mar 2026 07:44:17 +0000</pubDate>
      <link>https://dev.to/imdj/teaching-claude-code-to-write-xslt-the-hook-in-action-l8n</link>
      <guid>https://dev.to/imdj/teaching-claude-code-to-write-xslt-the-hook-in-action-l8n</guid>
      <description>&lt;p&gt;Parts 1 and 2 explained the skill and the hook setup. This part shows it actually running — a real session, real prompts, real hook output. No theory.&lt;/p&gt;

&lt;p&gt;We'll build the same Order-to-Shipment transformation three ways — XSLT 1.0 with inline C#, XSLT 2.0 with Saxon, and LML compiled to XSLT 3.0 — and watch the hook verify each one, catch bugs, and drive Claude through a multi-step fix cycle. Everything below is from a single session.&lt;/p&gt;




&lt;h2&gt;
  
  
  Step 1: XSLT 1.0 with inline C
&lt;/h2&gt;

&lt;h3&gt;
  
  
  The prompt
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight xml"&gt;&lt;code&gt;Create an XSLT 1.0 transformation (compatible with .NET XslCompiledTransform)
that maps an Order to a Shipment. Use the existing Order.xml as input reference
and target the ShipmentOutput.xsd schema.

Requirements:
- Map OrderID → ShipmentRef with a "SHP-" prefix (use inline C# via msxsl:script)
- Map OrderDate → ShipDate, reformatted from yyyy-MM-dd to dd/MM/yyyy (inline C#)
- Map CustomerName → Recipient
- Generate an ExecutionDate using inline C# that returns DateTime.UtcNow
- Loop over each Item → Line, mapping ProductName → Product and Quantity → Quantity
- Filter out items where Quantity &lt;span class="err"&gt;&amp;lt;&lt;/span&gt; 1

Use the Order namespace `http://schemas.contoso.com/Order`.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  What Claude reads first
&lt;/h3&gt;

&lt;p&gt;Before writing anything, Claude reads the source and target schemas to understand both structures. This is the skill's Generate mode — it requires understanding the data shapes before choosing XPath expressions.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Source&lt;/strong&gt; — &lt;code&gt;Order.xml&lt;/code&gt; with namespace &lt;code&gt;http://schemas.contoso.com/Order&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Target&lt;/strong&gt; — &lt;code&gt;ShipmentOutput.xsd&lt;/code&gt;, no namespace:&lt;/p&gt;

&lt;p&gt;Claude also reads the existing &lt;code&gt;.vscode/launch.json&lt;/code&gt; to know where to append the new debug configuration. Now it writes.&lt;/p&gt;

&lt;h3&gt;
  
  
  The write sequence
&lt;/h3&gt;

&lt;p&gt;The skill enforces a specific write order: &lt;strong&gt;launch config first, stylesheet last&lt;/strong&gt;. This ensures the debug configuration is in place before the hook fires on the &lt;code&gt;.xslt&lt;/code&gt; write.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;File 1: &lt;code&gt;.vscode/launch.json&lt;/code&gt;&lt;/strong&gt; — appends a new configuration:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"xslt"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"request"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"launch"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Debug Order2Shipment_20260327"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"engine"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"compiled"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"stylesheet"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"${workspaceFolder}/Artifacts/Maps/Order2Shipment_20260327.xslt"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"xml"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"${workspaceFolder}/Artifacts/SampleData/Order.xml"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"output"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"${workspaceFolder}/Artifacts/Maps/out/Order2Shipment_20260327-out.xml"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"stopOnEntry"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"debug"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"logLevel"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"log"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Engine is &lt;code&gt;compiled&lt;/code&gt; — the .NET &lt;code&gt;XslCompiledTransform&lt;/code&gt; engine that supports &lt;code&gt;msxsl:script&lt;/code&gt; inline C#. No hook fires on this file.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;File 2: &lt;code&gt;Artifacts/Maps/Order2Shipment_20260327.xslt&lt;/code&gt;&lt;/strong&gt; — the stylesheet with three C# helpers in a &lt;code&gt;msxsl:script&lt;/code&gt; block:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;PrefixShipmentRef(string orderId)&lt;/code&gt; — prepends &lt;code&gt;"SHP-"&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;ReformatDate(string isoDate)&lt;/code&gt; — parses &lt;code&gt;yyyy-MM-dd&lt;/code&gt;, returns &lt;code&gt;dd/MM/yyyy&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;ExecutionDate()&lt;/code&gt; — returns &lt;code&gt;DateTime.UtcNow&lt;/code&gt; as &lt;code&gt;yyyy-MM-ddTHH:mm:ssZ&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;The hook fires.&lt;/strong&gt; While Claude is still in the same response turn, the XSLT Debugger API compiles the inline C# via Roslyn, runs the transform against &lt;code&gt;Order.xml&lt;/code&gt;, and returns the result:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fwlyki27zuyl0qzb9mnsg.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fwlyki27zuyl0qzb9mnsg.png" alt="ClaudeCodePrompt1" width="800" height="365"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;That's the clean path. Before moving to XSLT 2.0, let's see what happens when this same map has bugs.&lt;/p&gt;




&lt;h2&gt;
  
  
  What the hook catches: two classes of failure
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Silent data loss — the namespace bug
&lt;/h3&gt;

&lt;p&gt;XSLT's most dangerous bugs produce structurally correct output with silently missing data. No error, no warning — just empty elements.&lt;/p&gt;

&lt;p&gt;Claude drops the &lt;code&gt;ord:&lt;/code&gt; namespace prefix from one XPath:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight xml"&gt;&lt;code&gt;&lt;span class="c"&gt;&amp;lt;!-- Bug: missing namespace prefix --&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;Recipient&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;xsl:value-of&lt;/span&gt; &lt;span class="na"&gt;select=&lt;/span&gt;&lt;span class="s"&gt;"Customer/CustomerName"&lt;/span&gt;&lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/Recipient&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The hook fires:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight xml"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;Recipient&amp;gt;&amp;lt;/Recipient&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Exit code 0. The transform "succeeded". But &lt;code&gt;&amp;lt;Recipient&amp;gt;&lt;/code&gt; is empty. Without the hook, this ships to production and nobody notices until a downstream system complains.&lt;/p&gt;

&lt;p&gt;Claude sees the empty element, identifies the namespace mismatch (the skill's #1 debug pattern), and edits the file. &lt;strong&gt;The hook fires again on the Edit&lt;/strong&gt; — not just on the initial Write. Every change to an &lt;code&gt;.xslt&lt;/code&gt; file triggers a fresh transform:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight xml"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;Recipient&amp;gt;&lt;/span&gt;John Doe&lt;span class="nt"&gt;&amp;lt;/Recipient&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Fixed. Two hook invocations, one fix.&lt;/p&gt;

&lt;h3&gt;
  
  
  Hard compile failure — the C# syntax error
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fll057gx9buk3zqpituo8.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fll057gx9buk3zqpituo8.png" alt="FixIssueviaHook" width="800" height="162"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Exit code 1. No XML output at all — Roslyn rejects the C# before the XSLT engine even starts. Claude sees the error with exact line and column, adds the semicolon, hook fires again, clean output.&lt;/p&gt;

&lt;p&gt;These are two fundamentally different failure modes:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Failure&lt;/th&gt;
&lt;th&gt;What happens&lt;/th&gt;
&lt;th&gt;Exit code&lt;/th&gt;
&lt;th&gt;Catchable without hook?&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Namespace mismatch&lt;/td&gt;
&lt;td&gt;Valid XML, empty values&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;No — CI pipelines see "success"&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;C# compile error&lt;/td&gt;
&lt;td&gt;No output, error message&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;Yes — but only if you run it&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The namespace bug is why the hook matters most. It catches the failures that look like success.&lt;/p&gt;




&lt;h2&gt;
  
  
  Step 2: XSLT 2.0 with Saxon
&lt;/h2&gt;

&lt;p&gt;Next prompt:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight xml"&gt;&lt;code&gt;Now create the same map as XSLT 2.0 for Saxon. Create a new file.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Same write sequence: launch config first (engine &lt;code&gt;saxonnet&lt;/code&gt;), then the stylesheet. No &lt;code&gt;msxsl:script&lt;/code&gt; this time — XSLT 2.0 has native functions for everything the C# was doing:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;XSLT 1.0 + C#&lt;/th&gt;
&lt;th&gt;XSLT 2.0 native&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;fn:PrefixShipmentRef(string(ord:OrderID))&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;concat('SHP-', ord:OrderID)&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;fn:ReformatDate(string(ord:OrderDate))&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;format-date(xs:date(ord:OrderDate), '[D01]/[M01]/[Y0001]')&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;fn:ExecutionDate()&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;format-dateTime(current-dateTime(), '[Y0001]-[M01]-[D01]T[H01]:[m01]:[s01]Z')&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The hook fires on the &lt;code&gt;.xslt&lt;/code&gt; write:&lt;/p&gt;

&lt;p&gt;Same output structure, same values. Two engines, two versions, same result — both verified by the hook automatically.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fxz0cehuj7kvprtuzrpqn.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fxz0cehuj7kvprtuzrpqn.png" alt="ClaudeCodePrompt2" width="800" height="333"&gt;&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  Step 3: LML compiled to XSLT 3.0
&lt;/h2&gt;

&lt;p&gt;Now the same map authored in the Data Mapper's LML format — the visual designer's YAML source that compiles to XSLT 3.0. This is the most complex path because it has dependencies that must be created in the right order.&lt;/p&gt;

&lt;h3&gt;
  
  
  The prompt
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Now create an LML for the same Order-to-Shipment mapping and test it.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  The dependency chain
&lt;/h3&gt;

&lt;p&gt;Claude reads the LML file and prepares the next actions&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fg7saoc8z053auexz4cop.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fg7saoc8z053auexz4cop.png" alt="ClaudeReadsLMLMD" width="800" height="288"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;LML maps can reference custom extension functions defined in separate XML files. Before Claude can write the LML, it needs to create these functions. The full write sequence for LML mode is:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Launch config&lt;/strong&gt; — debug configuration pointing at the &lt;em&gt;compiled&lt;/em&gt; &lt;code&gt;.xslt&lt;/code&gt; (not the &lt;code&gt;.lml&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Custom functions&lt;/strong&gt; — &lt;code&gt;Artifacts/DataMapper/Extensions/Functions/ShipmentFunctions.xml&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;LML file&lt;/strong&gt; — &lt;code&gt;Artifacts/MapDefinitions/Order2Shipment_20260327-lml.lml&lt;/code&gt; (written last — the compile hook fires on this, using &lt;a href="https://www.nuget.org/packages/lml-compile" rel="noopener noreferrer"&gt;&lt;code&gt;lml-compile&lt;/code&gt;&lt;/a&gt;)&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The launch config points at &lt;code&gt;Artifacts/Maps/Order2Shipment_20260327-lml.xslt&lt;/code&gt; — a file that doesn't exist yet. It will be generated when the LML compile hook fires. Engine is &lt;code&gt;saxonnet&lt;/code&gt; because LML always compiles to XSLT 3.0.&lt;/p&gt;

&lt;h3&gt;
  
  
  The custom functions
&lt;/h3&gt;

&lt;p&gt;Claude creates three functions in &lt;code&gt;ShipmentFunctions.xml&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight xml"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;customfunctions&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;function&lt;/span&gt; &lt;span class="na"&gt;name=&lt;/span&gt;&lt;span class="s"&gt;"prefixShipRef"&lt;/span&gt; &lt;span class="na"&gt;as=&lt;/span&gt;&lt;span class="s"&gt;"xs:string"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;param&lt;/span&gt; &lt;span class="na"&gt;name=&lt;/span&gt;&lt;span class="s"&gt;"orderId"&lt;/span&gt; &lt;span class="na"&gt;as=&lt;/span&gt;&lt;span class="s"&gt;"xs:string"&lt;/span&gt;&lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;value-of&lt;/span&gt; &lt;span class="na"&gt;select=&lt;/span&gt;&lt;span class="s"&gt;"concat('SHP-', $orderId)"&lt;/span&gt;&lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;/function&amp;gt;&lt;/span&gt;

  &lt;span class="nt"&gt;&amp;lt;function&lt;/span&gt; &lt;span class="na"&gt;name=&lt;/span&gt;&lt;span class="s"&gt;"reformatDate"&lt;/span&gt; &lt;span class="na"&gt;as=&lt;/span&gt;&lt;span class="s"&gt;"xs:string"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;param&lt;/span&gt; &lt;span class="na"&gt;name=&lt;/span&gt;&lt;span class="s"&gt;"dateVal"&lt;/span&gt; &lt;span class="na"&gt;as=&lt;/span&gt;&lt;span class="s"&gt;"xs:date"&lt;/span&gt;&lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;value-of&lt;/span&gt; &lt;span class="na"&gt;select=&lt;/span&gt;&lt;span class="s"&gt;"format-date($dateVal, '[D01]/[M01]/[Y0001]')"&lt;/span&gt;&lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;/function&amp;gt;&lt;/span&gt;

  &lt;span class="nt"&gt;&amp;lt;function&lt;/span&gt; &lt;span class="na"&gt;name=&lt;/span&gt;&lt;span class="s"&gt;"executionDateTime"&lt;/span&gt; &lt;span class="na"&gt;as=&lt;/span&gt;&lt;span class="s"&gt;"xs:string"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;value-of&lt;/span&gt; &lt;span class="na"&gt;select=&lt;/span&gt;&lt;span class="s"&gt;"format-dateTime(...)"&lt;/span&gt;&lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;/function&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/customfunctions&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;No hook fires — this is an &lt;code&gt;.xml&lt;/code&gt; file, not &lt;code&gt;.lml&lt;/code&gt; or &lt;code&gt;.xslt&lt;/code&gt;. But this file has a bug that won't surface until the LML compiles. &lt;code&gt;executionDateTime&lt;/code&gt; has zero parameters.&lt;/p&gt;

&lt;h3&gt;
  
  
  The LML file
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;$version&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;1&lt;/span&gt;
&lt;span class="na"&gt;$input&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;XML&lt;/span&gt;
&lt;span class="na"&gt;$output&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;XML&lt;/span&gt;
&lt;span class="na"&gt;$sourceSchema&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;OrderSchema.xsd&lt;/span&gt;
&lt;span class="na"&gt;$targetSchema&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ShipmentOutput.xsd&lt;/span&gt;
&lt;span class="na"&gt;$sourceNamespaces&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;ns0&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;http://schemas.contoso.com/Order&lt;/span&gt;
&lt;span class="na"&gt;Shipments&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;Shipment&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;ShipmentRef&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;prefixShipRef(/ns0:Order/ns0:OrderID)&lt;/span&gt;
    &lt;span class="na"&gt;ShipDate&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;reformatDate(/ns0:Order/ns0:OrderDate)&lt;/span&gt;
    &lt;span class="na"&gt;Recipient&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;/ns0:Order/ns0:Customer/ns0:CustomerName&lt;/span&gt;
    &lt;span class="na"&gt;ExecutionDate&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;executionDateTime()&lt;/span&gt;
    &lt;span class="na"&gt;Lines&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;$for(/ns0:Order/ns0:Items/ns0:Item)&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="s"&gt;$if(is-greater-or-equal(ns0:Quantity, 1))&lt;/span&gt;&lt;span class="err"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;Line&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
            &lt;span class="na"&gt;Product&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ns0:ProductName&lt;/span&gt;
            &lt;span class="na"&gt;Quantity&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ns0:Quantity&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Claude writes this file. The LML compile hook fires — calling &lt;a href="https://www.nuget.org/packages/lml-compile" rel="noopener noreferrer"&gt;&lt;code&gt;lml-compile&lt;/code&gt;&lt;/a&gt;, a dotnet global tool that wraps the Logic Apps SDK's &lt;code&gt;DataMapTestExecutor.GenerateXslt()&lt;/code&gt;. And now the fixing cycle begins.&lt;/p&gt;

&lt;h3&gt;
  
  
  The fixing cycle
&lt;/h3&gt;

&lt;p&gt;The &lt;code&gt;executionDateTime&lt;/code&gt; function has zero parameters — the SDK's &lt;code&gt;ReadCustomFunctionsDefinitionAsync()&lt;/code&gt; hits a &lt;code&gt;NullReferenceException&lt;/code&gt; on &lt;code&gt;parameters.Length&lt;/code&gt; and silently skips the entire XML file. All three functions become "unrecognized". Claude adds a dummy parameter, edits the LML, and the hook fires again — second error: &lt;code&gt;is-greater-or-equal&lt;/code&gt; isn't a real LML pseudofunction. Claude switches to &lt;code&gt;xpath("ns0:Quantity &amp;gt;= 1")&lt;/code&gt;, edits the LML one more time, and the hook returns clean output.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F94ork67mxnod0vvql7wh.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F94ork67mxnod0vvql7wh.png" alt="FixingCycle" width="800" height="367"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Three iterations, two bugs, one prompt. The compile hook drove the entire fix cycle — feeding each error back into Claude's context so it could diagnose and fix without manual intervention.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Iteration&lt;/th&gt;
&lt;th&gt;Error&lt;/th&gt;
&lt;th&gt;Root cause&lt;/th&gt;
&lt;th&gt;Fix&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;executionDateTime&lt;/code&gt; unrecognized&lt;/td&gt;
&lt;td&gt;Zero-param SDK bug — null &lt;code&gt;parameters&lt;/code&gt; skips entire XML file&lt;/td&gt;
&lt;td&gt;Add dummy &lt;code&gt;&amp;lt;param&amp;gt;&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;is-greater-or-equal&lt;/code&gt; unrecognized&lt;/td&gt;
&lt;td&gt;Not a real LML pseudofunction — UI label vs compiler syntax&lt;/td&gt;
&lt;td&gt;Use &lt;code&gt;xpath("ns0:Quantity &amp;gt;= 1")&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;Success&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;




&lt;h2&gt;
  
  
  Three engines, one result
&lt;/h2&gt;

&lt;p&gt;All three implementations now produce matching output from the same input:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Engine&lt;/th&gt;
&lt;th&gt;Version&lt;/th&gt;
&lt;th&gt;Date logic&lt;/th&gt;
&lt;th&gt;String logic&lt;/th&gt;
&lt;th&gt;Filter&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;compiled&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;XSLT 1.0&lt;/td&gt;
&lt;td&gt;C# &lt;code&gt;DateTime.TryParse&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;C# &lt;code&gt;string.Concat&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;&lt;code&gt;xsl:if test="ord:Quantity &amp;gt;= 1"&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;saxonnet&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;XSLT 2.0&lt;/td&gt;
&lt;td&gt;&lt;code&gt;format-date(xs:date(...))&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;concat(...)&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;xsl:if test="ord:Quantity &amp;gt;= 1"&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;saxonnet&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;XSLT 3.0 (LML)&lt;/td&gt;
&lt;td&gt;&lt;code&gt;ef:reformatDate(...)&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;ef:prefixShipRef(...)&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;xsl:when test="ns0:Quantity &amp;gt;= 1"&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight xml"&gt;&lt;code&gt;&lt;span class="c"&gt;&amp;lt;!-- All three produce: --&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;ShipmentRef&amp;gt;&lt;/span&gt;SHP-ORD-1001&lt;span class="nt"&gt;&amp;lt;/ShipmentRef&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;ShipDate&amp;gt;&lt;/span&gt;31/10/2025&lt;span class="nt"&gt;&amp;lt;/ShipDate&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;Recipient&amp;gt;&lt;/span&gt;John Doe&lt;span class="nt"&gt;&amp;lt;/Recipient&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The hook verified each automatically at write time. This matters for teams migrating between engines — you can write the same map in XSLT 1.0 for BizTalk and XSLT 3.0 for Logic Apps, and verify both produce identical output in the same session.&lt;/p&gt;




&lt;h2&gt;
  
  
  What you actually see in VS Code
&lt;/h2&gt;

&lt;p&gt;When the hook fires during a Claude response, the output appears inline in Claude's context — a &lt;code&gt;PostToolUse&lt;/code&gt; system reminder that Claude reads as part of the same turn. You don't see a separate panel or dialog.&lt;/p&gt;

&lt;p&gt;For the XSLT 1.0 clean path:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="err"&gt;Claude:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="err"&gt;reads&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;Order.xml&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;ShipmentOutput.xsd&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;launch.json&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="err"&gt;Claude:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="err"&gt;writes&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;launch.json&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;entry&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="err"&gt;Claude:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="err"&gt;writes&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;Order&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="err"&gt;Shipment_&lt;/span&gt;&lt;span class="mi"&gt;20260327&lt;/span&gt;&lt;span class="err"&gt;.xslt&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="err"&gt;←&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;Hook:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;PostToolUse:Write&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;—&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;Transform&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;result:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;&amp;lt;Shipments&amp;gt;...&amp;lt;/Shipments&amp;gt;&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="err"&gt;Claude:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Transform ran, output correct."&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For the LML fixing cycle:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Claude: [writes launch.json entry]
Claude: [writes ShipmentFunctions.xml]
Claude: [writes Order2Shipment_20260327-lml.lml]
  ← Hook: PostToolUse:Write — ERROR: 'executionDateTime' unrecognized
Claude: [edits ShipmentFunctions.xml — adds dummy param]
Claude: [edits .lml — passes dummy argument]
  ← Hook: PostToolUse:Edit — ERROR: 'is-greater-or-equal' unrecognized
Claude: [edits .lml — switches to xpath()]
  ← Hook: PostToolUse:Edit — LML compiled + Transform result: &amp;lt;Shipments&amp;gt;...&amp;lt;/Shipments&amp;gt;
Claude: "Three iterations, all fixed. Output matches."
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;From your side, you prompted once. Claude read the schemas, created the files, hit three errors, fixed all three, and came back with a verified transform. The iteration happened between Claude and the hook.&lt;/p&gt;




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

&lt;p&gt;Three parts, one system:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Skill&lt;/strong&gt; — gives Claude the rules before it writes a line. Engine classification, version discipline, write order, debug taxonomy, LML syntax, known SDK bugs. Prevents common mistakes and provides the knowledge to diagnose uncommon ones.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Hooks&lt;/strong&gt; — close the feedback loop after every write and edit. Compile on LML change using &lt;a href="https://www.nuget.org/packages/lml-compile" rel="noopener noreferrer"&gt;&lt;code&gt;lml-compile&lt;/code&gt;&lt;/a&gt; (a dotnet global tool that wraps the SDK compiler). Run transform on XSLT change via the Debugger HTTP API. Return results — output or error — to Claude's context automatically.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The loop&lt;/strong&gt; — Claude writes → hook runs → Claude reads result → Claude fixes if needed → hook runs again. One prompt from you; the iteration happens between Claude and the hook.&lt;/p&gt;

&lt;p&gt;The skill and the hooks are independent — the skill works without the hooks (slower iteration, manual verification), the hooks work without the skill (no domain rules, more trial and error). Together they make a working environment where the AI can do real XSLT work reliably: catch silent namespace bugs before they reach production, fix C# compile errors in the same turn, iterate through LML compiler quirks without you lifting a finger.&lt;/p&gt;

&lt;p&gt;The result isn't "AI-generated XSLT" that you have to review and test manually. It's XSLT that was already tested — by the same system that wrote it, in the same conversation turn, against real input data.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;The XSLT Debugger extension is on the VS Code Marketplace for &lt;a href="https://marketplace.visualstudio.com/items?itemName=danieljonathan.xsltdebugger-darwin" rel="noopener noreferrer"&gt;macOS&lt;/a&gt; and &lt;a href="https://marketplace.visualstudio.com/items?itemName=danieljonathan.xsltdebugger-windows" rel="noopener noreferrer"&gt;Windows&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>xslt</category>
      <category>claudecode</category>
      <category>ai</category>
      <category>vscode</category>
    </item>
    <item>
      <title>Teaching Coding Agent to Write XSLT — The Hook Chain</title>
      <dc:creator>Daniel Jonathan</dc:creator>
      <pubDate>Sat, 28 Mar 2026 07:41:16 +0000</pubDate>
      <link>https://dev.to/imdj/teaching-claude-code-to-write-xslt-the-hook-chain-1k7</link>
      <guid>https://dev.to/imdj/teaching-claude-code-to-write-xslt-the-hook-chain-1k7</guid>
      <description>&lt;p&gt;Part 1 covered the XSLT skill — domain knowledge that prevents mistakes upfront. This part covers the runtime side: &lt;strong&gt;two PostToolUse hooks&lt;/strong&gt; that automate compile-and-run so Claude sees the result of its own edits immediately.&lt;/p&gt;

&lt;p&gt;Claude writes a file → hooks fire → output (or error) appears in context → Claude fixes and repeats. When the debugger is running and a matching launch config exists, no manual runs are needed. Otherwise the hook reports what's missing and Claude can guide you.&lt;/p&gt;




&lt;h2&gt;
  
  
  PostToolUse hooks
&lt;/h2&gt;

&lt;p&gt;Claude Code hooks are shell commands that fire after tool events. &lt;code&gt;PostToolUse&lt;/code&gt; fires after every &lt;code&gt;Write&lt;/code&gt; or &lt;code&gt;Edit&lt;/code&gt; — meaning iterative fixes get immediate feedback on every change.&lt;/p&gt;

&lt;p&gt;Configure in &lt;code&gt;.claude/settings.json&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"hooks"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"PostToolUse"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"matcher"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Write|Edit"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"hooks"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"command"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="nl"&gt;"command"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"bash .claude/hooks/generate-xslt-from-lml.sh"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="nl"&gt;"timeout"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;45&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="nl"&gt;"statusMessage"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Compiling LML + running transform..."&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"command"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="nl"&gt;"command"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"bash .claude/hooks/run-xslt-after-edit.sh"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="nl"&gt;"timeout"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;30&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="nl"&gt;"statusMessage"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Running XSLT transform..."&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Both hooks filter on file extension internally — the first acts only on &lt;code&gt;.lml&lt;/code&gt;, the second only on &lt;code&gt;.xslt&lt;/code&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  Hook 1: LML compile
&lt;/h2&gt;

&lt;p&gt;When Claude edits a &lt;code&gt;.lml&lt;/code&gt; file, the designer isn't involved — the compiled &lt;code&gt;.xslt&lt;/code&gt; would be stale. This hook compiles it and runs the transform in one step.&lt;/p&gt;

&lt;h3&gt;
  
  
  The compiler: &lt;code&gt;lml-compile&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;The Logic Apps testing SDK includes &lt;code&gt;DataMapTestExecutor&lt;/code&gt; with a &lt;code&gt;GenerateXslt()&lt;/code&gt; method. I wrapped it as a .NET global tool:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;dotnet tool &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-g&lt;/span&gt; lml-compile
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;No running Logic Apps host. No Azurite. Just:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;lml-compile input.lml output.xslt
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  The hook
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;#!/usr/bin/env bash&lt;/span&gt;
&lt;span class="nb"&gt;set&lt;/span&gt; &lt;span class="nt"&gt;-euo&lt;/span&gt; pipefail

&lt;span class="nv"&gt;INPUT&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;cat&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;
&lt;span class="nv"&gt;FILE_PATH&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$INPUT&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; | jq &lt;span class="nt"&gt;-r&lt;/span&gt; &lt;span class="s1"&gt;'.tool_input.file_path // empty'&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;

&lt;span class="c"&gt;# Only act on .lml files&lt;/span&gt;
&lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$FILE_PATH&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="k"&gt;*&lt;/span&gt;.lml&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;;;&lt;/span&gt; &lt;span class="k"&gt;*&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nb"&gt;exit &lt;/span&gt;0 &lt;span class="p"&gt;;;&lt;/span&gt; &lt;span class="k"&gt;esac&lt;/span&gt;

&lt;span class="c"&gt;# Derive output path&lt;/span&gt;
&lt;span class="nv"&gt;BASENAME&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;basename&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$FILE_PATH&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; .lml&lt;span class="si"&gt;)&lt;/span&gt;
&lt;span class="nv"&gt;MAPS_DIR&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;cd&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;dirname&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$FILE_PATH&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;/.."&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;pwd&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;/Maps"&lt;/span&gt;
&lt;span class="nv"&gt;OUT_XSLT&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;MAPS_DIR&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;BASENAME&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;.xslt"&lt;/span&gt;
&lt;span class="nb"&gt;mkdir&lt;/span&gt; &lt;span class="nt"&gt;-p&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$MAPS_DIR&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;

&lt;span class="c"&gt;# Compile&lt;/span&gt;
&lt;span class="nb"&gt;set&lt;/span&gt; +e
&lt;span class="nv"&gt;COMPILE_OUTPUT&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;lml-compile &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$FILE_PATH&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$OUT_XSLT&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; 2&amp;gt;&amp;amp;1&lt;span class="si"&gt;)&lt;/span&gt;
&lt;span class="nv"&gt;COMPILE_EXIT&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nv"&gt;$?&lt;/span&gt;
&lt;span class="nb"&gt;set&lt;/span&gt; &lt;span class="nt"&gt;-e&lt;/span&gt;

&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="nv"&gt;$COMPILE_EXIT&lt;/span&gt; &lt;span class="nt"&gt;-ne&lt;/span&gt; 0 &lt;span class="o"&gt;]&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then
  &lt;/span&gt;jq &lt;span class="nt"&gt;-n&lt;/span&gt; &lt;span class="nt"&gt;--arg&lt;/span&gt; err &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$COMPILE_OUTPUT&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="s1"&gt;'{
    "hookSpecificOutput": {
      "hookEventName": "PostToolUse",
      "additionalContext": ("LML compile failed: " + $err)
    }
  }'&lt;/span&gt;
  &lt;span class="nb"&gt;exit &lt;/span&gt;0
&lt;span class="k"&gt;fi&lt;/span&gt;

&lt;span class="c"&gt;# Find workspace and run transform via debugger HTTP API&lt;/span&gt;
&lt;span class="c"&gt;# (walks up from file to find .vscode/launch.json)&lt;/span&gt;
&lt;span class="c"&gt;# ... matches launch config, calls POST /run-transform ...&lt;/span&gt;

jq &lt;span class="nt"&gt;-n&lt;/span&gt; &lt;span class="nt"&gt;--arg&lt;/span&gt; ctx &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$OUTPUT&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="s1"&gt;'{
  "hookSpecificOutput": {
    "hookEventName": "PostToolUse",
    "additionalContext": ("LML compiled + transform result:\n" + $ctx)
  }
}'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If compile fails, Claude sees the error. If it succeeds and the debugger is running with a matching launch config, Claude sees the transform output in the same turn. If the debugger isn't running or no config matches, the hook reports the compile success and what's missing.&lt;/p&gt;




&lt;h2&gt;
  
  
  Hook 2: Transform runner
&lt;/h2&gt;

&lt;p&gt;Fires when Claude writes or edits a &lt;code&gt;.xslt&lt;/code&gt; file directly (Generate mode).&lt;/p&gt;

&lt;p&gt;The key challenge: &lt;strong&gt;workspace derivation&lt;/strong&gt;. In hook context, &lt;code&gt;$(pwd)&lt;/code&gt; doesn't point to the project — it can resolve to &lt;code&gt;/private/tmp&lt;/code&gt;. The fix: walk up from the edited file's path to find &lt;code&gt;.vscode/launch.json&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;DIR&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;cd&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;dirname&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$EDITED_FILE&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;pwd&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;span class="nv"&gt;WORKSPACE&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;""&lt;/span&gt;
&lt;span class="k"&gt;while&lt;/span&gt; &lt;span class="o"&gt;[[&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$DIR&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="s2"&gt;"/"&lt;/span&gt; &lt;span class="o"&gt;]]&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;do
  if&lt;/span&gt; &lt;span class="o"&gt;[[&lt;/span&gt; &lt;span class="nt"&gt;-f&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$DIR&lt;/span&gt;&lt;span class="s2"&gt;/.vscode/launch.json"&lt;/span&gt; &lt;span class="o"&gt;]]&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then
    &lt;/span&gt;&lt;span class="nv"&gt;WORKSPACE&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$DIR&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nb"&gt;break
  &lt;/span&gt;&lt;span class="k"&gt;fi
  &lt;/span&gt;&lt;span class="nv"&gt;DIR&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;dirname&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$DIR&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;span class="k"&gt;done&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The hook then reads the matching launch config (resolving &lt;code&gt;${workspaceFolder}&lt;/code&gt;), extracts the input XML and engine, and calls the XSLT Debugger's HTTP API:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;RESULT&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;curl &lt;span class="nt"&gt;-s&lt;/span&gt; &lt;span class="nt"&gt;-X&lt;/span&gt; POST &lt;span class="s2"&gt;"http://127.0.0.1:&lt;/span&gt;&lt;span class="nv"&gt;$PORT&lt;/span&gt;&lt;span class="s2"&gt;/run-transform"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Content-Type: application/json"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s2"&gt;"{&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;stylesheet&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;: &lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="nv"&gt;$STYLESHEET&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;, &lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;xml&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;: &lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="nv"&gt;$XML&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;, &lt;/span&gt;&lt;span class="se"&gt;\&lt;/span&gt;&lt;span class="s2"&gt;
&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;engine&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;: &lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="nv"&gt;$ENGINE&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;}"&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The result is returned via &lt;code&gt;hookSpecificOutput&lt;/code&gt; so Claude sees the transform output in context.&lt;/p&gt;




&lt;h2&gt;
  
  
  How the hooks interact
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Claude writes &lt;code&gt;.xslt&lt;/code&gt;&lt;/strong&gt; (Generate mode):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Claude → Write .xslt → Hook 1 ignores (not .lml) → Hook 2 runs transform → result to Claude
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Claude writes &lt;code&gt;.lml&lt;/code&gt;&lt;/strong&gt; (LML mode):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Claude → Write .lml → Hook 1 compiles + runs transform → result to Claude → Hook 2 ignores (not .xslt)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;.xslt&lt;/code&gt; file written by &lt;code&gt;lml-compile&lt;/code&gt; does &lt;strong&gt;not&lt;/strong&gt; trigger Hook 2 — only files Claude writes directly via &lt;code&gt;Write&lt;/code&gt; or &lt;code&gt;Edit&lt;/code&gt; trigger hooks.&lt;/p&gt;




&lt;h2&gt;
  
  
  The feedback loop
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Claude writes XSLT → hook runs transform → output in context → error? → Claude edits → hook runs again → correct output
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;No human in the loop for the fix cycle. Claude writes, sees the result, fixes if needed, sees the result again.&lt;/p&gt;




&lt;h2&gt;
  
  
  Shell bugs worth knowing
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;set -e&lt;/code&gt; with &lt;code&gt;|| true&lt;/code&gt;:&lt;/strong&gt; Using &lt;code&gt;OUTPUT=$(cmd) || true&lt;/code&gt; resets &lt;code&gt;$?&lt;/code&gt; to 0 — the failure branch is unreachable. Fix: &lt;code&gt;set +e&lt;/code&gt;, capture &lt;code&gt;$?&lt;/code&gt;, then &lt;code&gt;set -e&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;grep -v&lt;/code&gt; returns exit 1 when no lines match:&lt;/strong&gt; Under &lt;code&gt;set -euo pipefail&lt;/code&gt;, this kills the hook. Append &lt;code&gt;|| true&lt;/code&gt; to the grep specifically.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;$(pwd)&lt;/code&gt; in hook context:&lt;/strong&gt; Doesn't inherit the project directory. Always derive workspace from the edited file's path.&lt;/p&gt;




&lt;h2&gt;
  
  
  The pattern generalises
&lt;/h2&gt;

&lt;p&gt;The pattern is: &lt;strong&gt;skill (domain rules) + hook (verify after every write) = self-correcting AI&lt;/strong&gt;. Nothing here is XSLT-specific except the domain knowledge and the tools the hooks call.&lt;/p&gt;

&lt;p&gt;The same approach works for Terraform (&lt;code&gt;terraform validate&lt;/code&gt; + &lt;code&gt;terraform plan&lt;/code&gt; after &lt;code&gt;.tf&lt;/code&gt; edits), SQL migrations, OpenAPI specs — anything where a write can be verified automatically.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;The XSLT Debugger extension is on the VS Code Marketplace for &lt;a href="https://marketplace.visualstudio.com/items?itemName=danieljonathan.xsltdebugger-darwin" rel="noopener noreferrer"&gt;macOS&lt;/a&gt; and &lt;a href="https://marketplace.visualstudio.com/items?itemName=danieljonathan.xsltdebugger-windows" rel="noopener noreferrer"&gt;Windows&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>xslt</category>
      <category>claudecode</category>
      <category>vscode</category>
      <category>ai</category>
    </item>
    <item>
      <title>Teaching Coding Agent to Write XSLT — Building a Domain Skill</title>
      <dc:creator>Daniel Jonathan</dc:creator>
      <pubDate>Sat, 28 Mar 2026 07:18:22 +0000</pubDate>
      <link>https://dev.to/imdj/teaching-coding-agent-to-write-xslt-building-a-domain-skill-138c</link>
      <guid>https://dev.to/imdj/teaching-coding-agent-to-write-xslt-building-a-domain-skill-138c</guid>
      <description>&lt;p&gt;XSLT is unforgiving. Small mistakes — a wrong namespace, a version mismatch, an engine-specific function — produce silent failures. AI assistants make the same mistakes without explicit guidance.&lt;/p&gt;

&lt;p&gt;The fix isn't a smarter model. It's &lt;strong&gt;giving the model a playbook&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Claude Code supports custom skills — markdown files that load into context when a task matches. I built one that encodes real XSLT expertise: five work modes, engine detection, version discipline, and structured output you can debug immediately.&lt;/p&gt;




&lt;h2&gt;
  
  
  What is a Claude Code skill?
&lt;/h2&gt;

&lt;p&gt;A skill is a markdown file in &lt;code&gt;.claude/skills/&amp;lt;name&amp;gt;/SKILL.md&lt;/code&gt;. When Claude detects a matching task, it reads the skill and follows its instructions. Reference files alongside it provide deeper detail on demand.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;.claude/skills/xslt/
├── SKILL.md                        # modes, rules, output spec
└── references/
    ├── lml.md                      # Logic Apps Mapping Language
    ├── logicapps-xslt.md           # Logic Apps deployment and artifacts
    ├── saxoncs-quirks.md           # SaxonCS / XSLT 3.0 gotchas
    ├── biztalk-xslt.md             # BizTalk map authoring
    └── migration.md                # version migration guide
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The skill built in this post is available at &lt;a href="https://github.com/imdj360/dev-skills" rel="noopener noreferrer"&gt;imdj360/dev-skills&lt;/a&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  Mode detection
&lt;/h2&gt;

&lt;p&gt;The skill first classifies the task into one of five modes:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Mode&lt;/th&gt;
&lt;th&gt;Trigger&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Generate&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;XML samples, schemas, "write me an XSLT"&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Debug&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Broken XSLT, error message, wrong output&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Migrate&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;"Convert to 3.0", "rewrite as 1.0"&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Compatibility&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;"Will this run in BizTalk?"&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;LML&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;.lml&lt;/code&gt; file, Data Mapper, "compile to XSLT"&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Generate and LML produce an output bundle. Generate writes up to three artifacts (debug config, sample input, stylesheet). LML writes up to four core artifacts (debug config, sample input, LML source, compiled XSLT) — plus schemas and custom functions if the map needs them. The other modes produce artifacts only when needed.&lt;/p&gt;




&lt;h2&gt;
  
  
  Engine classification
&lt;/h2&gt;

&lt;p&gt;This is where most AI-generated XSLT fails. The skill forces engine classification before any code is written:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;BizTalk / XslCompiledTransform / msxsl:script&lt;/strong&gt; → XSLT 1.0, &lt;code&gt;compiled&lt;/code&gt; engine&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Logic Apps (Transform XML action)&lt;/strong&gt; → XSLT 1.0 by default&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Saxon / SaxonCS&lt;/strong&gt; → XSLT 2.0 or 3.0, &lt;code&gt;saxonnet&lt;/code&gt; engine&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If ambiguous, Claude must ask. It never assumes. This single rule prevents the most common failure: writing XSLT 2.0 features in a file that &lt;code&gt;.NET XslCompiledTransform&lt;/code&gt; will choke on.&lt;/p&gt;




&lt;h2&gt;
  
  
  Version discipline
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Never auto-upgrade.&lt;/strong&gt; If the stylesheet says &lt;code&gt;version="1.0"&lt;/code&gt;, output stays 1.0. No sneaking in &lt;code&gt;xsl:function&lt;/code&gt;, &lt;code&gt;matches()&lt;/code&gt;, or &lt;code&gt;xsl:sequence&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;For 1.0, the skill enforces:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;xsl:for-each&lt;/code&gt; only — no &lt;code&gt;xsl:for-each-group&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;contains()&lt;/code&gt;, &lt;code&gt;substring()&lt;/code&gt;, &lt;code&gt;translate()&lt;/code&gt; — not &lt;code&gt;matches()&lt;/code&gt;, &lt;code&gt;replace()&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Named templates, not &lt;code&gt;xsl:function&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Inline C# for XSLT 1.0
&lt;/h3&gt;

&lt;p&gt;Pure XSLT 1.0 has gaps — no regex, no date formatting. The &lt;code&gt;msxsl:script&lt;/code&gt; extension fills them with inline C#:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight xml"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;msxsl:script&lt;/span&gt; &lt;span class="na"&gt;language=&lt;/span&gt;&lt;span class="s"&gt;"C#"&lt;/span&gt; &lt;span class="na"&gt;implements-prefix=&lt;/span&gt;&lt;span class="s"&gt;"fn"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="cp"&gt;&amp;lt;![CDATA[
    public string ReformatDate(string isoDate) {
      if (System.DateTime.TryParse(isoDate, out System.DateTime dt))
        return dt.ToString("dd/MM/yyyy");
      return isoDate;
    }
  ]]&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/msxsl:script&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The skill knows the type mappings (XPath numbers → &lt;code&gt;double&lt;/code&gt;, node-sets → &lt;code&gt;XPathNodeIterator&lt;/code&gt;) and the constraint: &lt;code&gt;msxsl:script&lt;/code&gt; runs on &lt;code&gt;.NET XslCompiledTransform&lt;/code&gt; only, never Saxon.&lt;/p&gt;




&lt;h2&gt;
  
  
  Canonical skeletons
&lt;/h2&gt;

&lt;p&gt;The skill provides starting templates so Claude doesn't construct structure from scratch.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;XSLT 1.0 + inline C#:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight xml"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;xsl:stylesheet&lt;/span&gt; &lt;span class="na"&gt;version=&lt;/span&gt;&lt;span class="s"&gt;"1.0"&lt;/span&gt;
  &lt;span class="na"&gt;xmlns:xsl=&lt;/span&gt;&lt;span class="s"&gt;"http://www.w3.org/1999/XSL/Transform"&lt;/span&gt;
  &lt;span class="na"&gt;xmlns:msxsl=&lt;/span&gt;&lt;span class="s"&gt;"urn:schemas-microsoft-com:xslt"&lt;/span&gt;
  &lt;span class="na"&gt;xmlns:fn=&lt;/span&gt;&lt;span class="s"&gt;"urn:my-functions"&lt;/span&gt;
  &lt;span class="na"&gt;exclude-result-prefixes=&lt;/span&gt;&lt;span class="s"&gt;"msxsl fn"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;xsl:output&lt;/span&gt; &lt;span class="na"&gt;method=&lt;/span&gt;&lt;span class="s"&gt;"xml"&lt;/span&gt; &lt;span class="na"&gt;indent=&lt;/span&gt;&lt;span class="s"&gt;"yes"&lt;/span&gt;&lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
  &lt;span class="c"&gt;&amp;lt;!-- msxsl:script block, then templates --&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/xsl:stylesheet&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;XSLT 3.0 / SaxonCS:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight xml"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;xsl:stylesheet&lt;/span&gt; &lt;span class="na"&gt;version=&lt;/span&gt;&lt;span class="s"&gt;"3.0"&lt;/span&gt;
  &lt;span class="na"&gt;xmlns:xsl=&lt;/span&gt;&lt;span class="s"&gt;"http://www.w3.org/1999/XSL/Transform"&lt;/span&gt;
  &lt;span class="na"&gt;xmlns:xs=&lt;/span&gt;&lt;span class="s"&gt;"http://www.w3.org/2001/XMLSchema"&lt;/span&gt;
  &lt;span class="na"&gt;exclude-result-prefixes=&lt;/span&gt;&lt;span class="s"&gt;"#all"&lt;/span&gt;
  &lt;span class="na"&gt;expand-text=&lt;/span&gt;&lt;span class="s"&gt;"yes"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;xsl:output&lt;/span&gt; &lt;span class="na"&gt;method=&lt;/span&gt;&lt;span class="s"&gt;"xml"&lt;/span&gt; &lt;span class="na"&gt;indent=&lt;/span&gt;&lt;span class="s"&gt;"yes"&lt;/span&gt;&lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;xsl:mode&lt;/span&gt; &lt;span class="na"&gt;on-no-match=&lt;/span&gt;&lt;span class="s"&gt;"shallow-skip"&lt;/span&gt;&lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
  &lt;span class="c"&gt;&amp;lt;!-- templates --&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/xsl:stylesheet&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;LML-generated XSLT 3.0 uses different conventions (&lt;code&gt;mode="azure.workflow.datamapper"&lt;/code&gt;, explicit namespace exclusions). The skill tracks both so Claude doesn't confuse them.&lt;/p&gt;




&lt;h2&gt;
  
  
  Debug taxonomy
&lt;/h2&gt;

&lt;p&gt;For Debug mode, Claude classifies the bug before attempting a fix:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Class&lt;/th&gt;
&lt;th&gt;Danger&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;strong&gt;Namespace mismatch&lt;/strong&gt; — empty output, XPath returns nothing&lt;/td&gt;
&lt;td&gt;High — exit code 0, looks like success&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;strong&gt;C# compile error&lt;/strong&gt; — Roslyn error in &lt;code&gt;msxsl:script&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Obvious — exit code 1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;strong&gt;Template priority conflict&lt;/strong&gt; — wrong template fires&lt;/td&gt;
&lt;td&gt;Medium&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;strong&gt;Version mismatch&lt;/strong&gt; — works in Saxon, fails in .NET&lt;/td&gt;
&lt;td&gt;Medium&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Namespace mismatch is the most dangerous: the transform succeeds with valid XML structure but every value is empty.&lt;/p&gt;




&lt;h2&gt;
  
  
  The output bundle
&lt;/h2&gt;

&lt;p&gt;For Generate and LML modes, the skill requires a complete output bundle.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Generate mode&lt;/strong&gt; writes:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Launch config — appended to &lt;code&gt;.vscode/launch.json&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Sample input XML — if none exists for the source schema&lt;/li&gt;
&lt;li&gt;Stylesheet — the &lt;code&gt;.xslt&lt;/code&gt; file&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;LML mode&lt;/strong&gt; writes (order matters):&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Source and target schemas — XSD files in &lt;code&gt;Artifacts/Schemas/&lt;/code&gt;, if not already present&lt;/li&gt;
&lt;li&gt;Launch config — points at the compiled &lt;code&gt;.xslt&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Custom extension functions — if the map uses them (must be written before LML compiles)&lt;/li&gt;
&lt;li&gt;Sample input XML — if none exists for the source schema&lt;/li&gt;
&lt;li&gt;LML file — written last (compile hook fires on this)&lt;/li&gt;
&lt;li&gt;Compiled XSLT — generated automatically by the compile hook&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;A launch config:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Debug Order2Shipment"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"xslt"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"request"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"launch"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"engine"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"saxonnet"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"stylesheet"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"${workspaceFolder}/Artifacts/Maps/Order2Shipment.xslt"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"xml"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"${workspaceFolder}/Artifacts/SampleData/Order.xml"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"output"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"${workspaceFolder}/Artifacts/Maps/out/Order2Shipment-out.xml"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"stopOnEntry"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;output&lt;/code&gt; is optional — omit it to see results in the Debug Console only. Press F5 and debug with breakpoints — no manual setup.&lt;/p&gt;




&lt;h2&gt;
  
  
  LML mode: custom functions and gotchas
&lt;/h2&gt;

&lt;p&gt;LML maps can call custom functions in &lt;code&gt;Artifacts/DataMapper/Extensions/Functions/*.xml&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight xml"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;customfunctions&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;function&lt;/span&gt; &lt;span class="na"&gt;name=&lt;/span&gt;&lt;span class="s"&gt;"reformatDate"&lt;/span&gt; &lt;span class="na"&gt;as=&lt;/span&gt;&lt;span class="s"&gt;"xs:string"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;param&lt;/span&gt; &lt;span class="na"&gt;name=&lt;/span&gt;&lt;span class="s"&gt;"dateVal"&lt;/span&gt; &lt;span class="na"&gt;as=&lt;/span&gt;&lt;span class="s"&gt;"xs:date"&lt;/span&gt;&lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;value-of&lt;/span&gt; &lt;span class="na"&gt;select=&lt;/span&gt;&lt;span class="s"&gt;"format-date($dateVal, '[D01]/[M01]/[Y0001]')"&lt;/span&gt;&lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;/function&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/customfunctions&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The skill documents SDK bugs Claude needs to work around:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Zero-parameter functions crash the compiler&lt;/strong&gt; — &lt;code&gt;NullReferenceException&lt;/code&gt;, entire XML file silently skipped. Workaround: add a dummy &lt;code&gt;&amp;lt;param&amp;gt;&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Multi-step paths in function arguments fail&lt;/strong&gt; — wrap in &lt;code&gt;xpath()&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Bare &lt;code&gt;@attr&lt;/code&gt; is invalid YAML&lt;/strong&gt; — use &lt;code&gt;xpath("@lineNum")&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  What the skill doesn't do
&lt;/h2&gt;

&lt;p&gt;The skill is domain knowledge and rules. It doesn't run anything — that's the job of hooks (Part 2). It doesn't validate XSLT at runtime — that's the debugger.&lt;/p&gt;

&lt;p&gt;Its value is &lt;strong&gt;preventing mistakes before they happen&lt;/strong&gt;. Part 2 covers the hooks that catch errors at write time.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;The XSLT Debugger extension is on the VS Code Marketplace for &lt;a href="https://marketplace.visualstudio.com/items?itemName=danieljonathan.xsltdebugger-darwin" rel="noopener noreferrer"&gt;macOS&lt;/a&gt; and &lt;a href="https://marketplace.visualstudio.com/items?itemName=danieljonathan.xsltdebugger-windows" rel="noopener noreferrer"&gt;Windows&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>vibecoding</category>
      <category>xslt</category>
      <category>ai</category>
      <category>claudecode</category>
    </item>
  </channel>
</rss>
