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.
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.
Step 1: XSLT 1.0 with inline C
The prompt
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 < 1
Use the Order namespace `http://schemas.contoso.com/Order`.
What Claude reads first
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.
Source — Order.xml with namespace http://schemas.contoso.com/Order
Target — ShipmentOutput.xsd, no namespace:
Claude also reads the existing .vscode/launch.json to know where to append the new debug configuration. Now it writes.
The write sequence
The skill enforces a specific write order: launch config first, stylesheet last. This ensures the debug configuration is in place before the hook fires on the .xslt write.
File 1: .vscode/launch.json — appends a new configuration:
{
"type": "xslt",
"request": "launch",
"name": "Debug Order2Shipment_20260327",
"engine": "compiled",
"stylesheet": "${workspaceFolder}/Artifacts/Maps/Order2Shipment_20260327.xslt",
"xml": "${workspaceFolder}/Artifacts/SampleData/Order.xml",
"output": "${workspaceFolder}/Artifacts/Maps/out/Order2Shipment_20260327-out.xml",
"stopOnEntry": true,
"debug": true,
"logLevel": "log"
}
Engine is compiled — the .NET XslCompiledTransform engine that supports msxsl:script inline C#. No hook fires on this file.
File 2: Artifacts/Maps/Order2Shipment_20260327.xslt — the stylesheet with three C# helpers in a msxsl:script block:
-
PrefixShipmentRef(string orderId)— prepends"SHP-" -
ReformatDate(string isoDate)— parsesyyyy-MM-dd, returnsdd/MM/yyyy -
ExecutionDate()— returnsDateTime.UtcNowasyyyy-MM-ddTHH:mm:ssZ
The hook fires. While Claude is still in the same response turn, the XSLT Debugger API compiles the inline C# via Roslyn, runs the transform against Order.xml, and returns the result:
That's the clean path. Before moving to XSLT 2.0, let's see what happens when this same map has bugs.
What the hook catches: two classes of failure
Silent data loss — the namespace bug
XSLT's most dangerous bugs produce structurally correct output with silently missing data. No error, no warning — just empty elements.
Claude drops the ord: namespace prefix from one XPath:
<!-- Bug: missing namespace prefix -->
<Recipient>
<xsl:value-of select="Customer/CustomerName"/>
</Recipient>
The hook fires:
<Recipient></Recipient>
Exit code 0. The transform "succeeded". But <Recipient> is empty. Without the hook, this ships to production and nobody notices until a downstream system complains.
Claude sees the empty element, identifies the namespace mismatch (the skill's #1 debug pattern), and edits the file. The hook fires again on the Edit — not just on the initial Write. Every change to an .xslt file triggers a fresh transform:
<Recipient>John Doe</Recipient>
Fixed. Two hook invocations, one fix.
Hard compile failure — the C# syntax error
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.
These are two fundamentally different failure modes:
| Failure | What happens | Exit code | Catchable without hook? |
|---|---|---|---|
| Namespace mismatch | Valid XML, empty values | 0 | No — CI pipelines see "success" |
| C# compile error | No output, error message | 1 | Yes — but only if you run it |
The namespace bug is why the hook matters most. It catches the failures that look like success.
Step 2: XSLT 2.0 with Saxon
Next prompt:
Now create the same map as XSLT 2.0 for Saxon. Create a new file.
Same write sequence: launch config first (engine saxonnet), then the stylesheet. No msxsl:script this time — XSLT 2.0 has native functions for everything the C# was doing:
| XSLT 1.0 + C# | XSLT 2.0 native |
|---|---|
fn:PrefixShipmentRef(string(ord:OrderID)) |
concat('SHP-', ord:OrderID) |
fn:ReformatDate(string(ord:OrderDate)) |
format-date(xs:date(ord:OrderDate), '[D01]/[M01]/[Y0001]') |
fn:ExecutionDate() |
format-dateTime(current-dateTime(), '[Y0001]-[M01]-[D01]T[H01]:[m01]:[s01]Z') |
The hook fires on the .xslt write:
Same output structure, same values. Two engines, two versions, same result — both verified by the hook automatically.
Step 3: LML compiled to XSLT 3.0
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.
The prompt
Now create an LML for the same Order-to-Shipment mapping and test it.
The dependency chain
Claude reads the LML file and prepares the next actions
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:
-
Launch config — debug configuration pointing at the compiled
.xslt(not the.lml) -
Custom functions —
Artifacts/DataMapper/Extensions/Functions/ShipmentFunctions.xml -
LML file —
Artifacts/MapDefinitions/Order2Shipment_20260327-lml.lml(written last — the compile hook fires on this, usinglml-compile)
The launch config points at Artifacts/Maps/Order2Shipment_20260327-lml.xslt — a file that doesn't exist yet. It will be generated when the LML compile hook fires. Engine is saxonnet because LML always compiles to XSLT 3.0.
The custom functions
Claude creates three functions in ShipmentFunctions.xml:
<customfunctions>
<function name="prefixShipRef" as="xs:string">
<param name="orderId" as="xs:string"/>
<value-of select="concat('SHP-', $orderId)"/>
</function>
<function name="reformatDate" as="xs:string">
<param name="dateVal" as="xs:date"/>
<value-of select="format-date($dateVal, '[D01]/[M01]/[Y0001]')"/>
</function>
<function name="executionDateTime" as="xs:string">
<value-of select="format-dateTime(...)"/>
</function>
</customfunctions>
No hook fires — this is an .xml file, not .lml or .xslt. But this file has a bug that won't surface until the LML compiles. executionDateTime has zero parameters.
The LML file
$version: 1
$input: XML
$output: XML
$sourceSchema: OrderSchema.xsd
$targetSchema: ShipmentOutput.xsd
$sourceNamespaces:
ns0: http://schemas.contoso.com/Order
Shipments:
Shipment:
ShipmentRef: prefixShipRef(/ns0:Order/ns0:OrderID)
ShipDate: reformatDate(/ns0:Order/ns0:OrderDate)
Recipient: /ns0:Order/ns0:Customer/ns0:CustomerName
ExecutionDate: executionDateTime()
Lines:
$for(/ns0:Order/ns0:Items/ns0:Item):
$if(is-greater-or-equal(ns0:Quantity, 1)):
Line:
Product: ns0:ProductName
Quantity: ns0:Quantity
Claude writes this file. The LML compile hook fires — calling lml-compile, a dotnet global tool that wraps the Logic Apps SDK's DataMapTestExecutor.GenerateXslt(). And now the fixing cycle begins.
The fixing cycle
The executionDateTime function has zero parameters — the SDK's ReadCustomFunctionsDefinitionAsync() hits a NullReferenceException on parameters.Length 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: is-greater-or-equal isn't a real LML pseudofunction. Claude switches to xpath("ns0:Quantity >= 1"), edits the LML one more time, and the hook returns clean output.
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.
| Iteration | Error | Root cause | Fix |
|---|---|---|---|
| 1 |
executionDateTime unrecognized |
Zero-param SDK bug — null parameters skips entire XML file |
Add dummy <param>
|
| 2 |
is-greater-or-equal unrecognized |
Not a real LML pseudofunction — UI label vs compiler syntax | Use xpath("ns0:Quantity >= 1")
|
| 3 | Success | — | — |
Three engines, one result
All three implementations now produce matching output from the same input:
| Engine | Version | Date logic | String logic | Filter |
|---|---|---|---|---|
compiled |
XSLT 1.0 | C# DateTime.TryParse
|
C# string.Concat
|
xsl:if test="ord:Quantity >= 1" |
saxonnet |
XSLT 2.0 | format-date(xs:date(...)) |
concat(...) |
xsl:if test="ord:Quantity >= 1" |
saxonnet |
XSLT 3.0 (LML) | ef:reformatDate(...) |
ef:prefixShipRef(...) |
xsl:when test="ns0:Quantity >= 1" |
<!-- All three produce: -->
<ShipmentRef>SHP-ORD-1001</ShipmentRef>
<ShipDate>31/10/2025</ShipDate>
<Recipient>John Doe</Recipient>
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.
What you actually see in VS Code
When the hook fires during a Claude response, the output appears inline in Claude's context — a PostToolUse system reminder that Claude reads as part of the same turn. You don't see a separate panel or dialog.
For the XSLT 1.0 clean path:
Claude: [reads Order.xml, ShipmentOutput.xsd, launch.json]
Claude: [writes launch.json entry]
Claude: [writes Order2Shipment_20260327.xslt]
← Hook: PostToolUse:Write — Transform result: <Shipments>...</Shipments>
Claude: "Transform ran, output correct."
For the LML fixing cycle:
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: <Shipments>...</Shipments>
Claude: "Three iterations, all fixed. Output matches."
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.
The full picture
Three parts, one system:
Skill — 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.
Hooks — close the feedback loop after every write and edit. Compile on LML change using lml-compile (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.
The loop — 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.
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.
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.
The XSLT Debugger extension is on the VS Code Marketplace for macOS and Windows.





Top comments (0)