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.
The fix isn't a smarter model. It's giving the model a playbook.
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.
What is a Claude Code skill?
A skill is a markdown file in .claude/skills/<name>/SKILL.md. When Claude detects a matching task, it reads the skill and follows its instructions. Reference files alongside it provide deeper detail on demand.
.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
The skill built in this post is available at imdj360/dev-skills.
Mode detection
The skill first classifies the task into one of five modes:
| Mode | Trigger |
|---|---|
| Generate | XML samples, schemas, "write me an XSLT" |
| Debug | Broken XSLT, error message, wrong output |
| Migrate | "Convert to 3.0", "rewrite as 1.0" |
| Compatibility | "Will this run in BizTalk?" |
| LML |
.lml file, Data Mapper, "compile to XSLT" |
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.
Engine classification
This is where most AI-generated XSLT fails. The skill forces engine classification before any code is written:
-
BizTalk / XslCompiledTransform / msxsl:script → XSLT 1.0,
compiledengine - Logic Apps (Transform XML action) → XSLT 1.0 by default
-
Saxon / SaxonCS → XSLT 2.0 or 3.0,
saxonnetengine
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 .NET XslCompiledTransform will choke on.
Version discipline
Never auto-upgrade. If the stylesheet says version="1.0", output stays 1.0. No sneaking in xsl:function, matches(), or xsl:sequence.
For 1.0, the skill enforces:
-
xsl:for-eachonly — noxsl:for-each-group -
contains(),substring(),translate()— notmatches(),replace() - Named templates, not
xsl:function
Inline C# for XSLT 1.0
Pure XSLT 1.0 has gaps — no regex, no date formatting. The msxsl:script extension fills them with inline C#:
<msxsl:script language="C#" implements-prefix="fn">
<![CDATA[
public string ReformatDate(string isoDate) {
if (System.DateTime.TryParse(isoDate, out System.DateTime dt))
return dt.ToString("dd/MM/yyyy");
return isoDate;
}
]]>
</msxsl:script>
The skill knows the type mappings (XPath numbers → double, node-sets → XPathNodeIterator) and the constraint: msxsl:script runs on .NET XslCompiledTransform only, never Saxon.
Canonical skeletons
The skill provides starting templates so Claude doesn't construct structure from scratch.
XSLT 1.0 + inline C#:
<xsl:stylesheet version="1.0"
xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
xmlns:msxsl="urn:schemas-microsoft-com:xslt"
xmlns:fn="urn:my-functions"
exclude-result-prefixes="msxsl fn">
<xsl:output method="xml" indent="yes"/>
<!-- msxsl:script block, then templates -->
</xsl:stylesheet>
XSLT 3.0 / SaxonCS:
<xsl:stylesheet version="3.0"
xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
xmlns:xs="http://www.w3.org/2001/XMLSchema"
exclude-result-prefixes="#all"
expand-text="yes">
<xsl:output method="xml" indent="yes"/>
<xsl:mode on-no-match="shallow-skip"/>
<!-- templates -->
</xsl:stylesheet>
LML-generated XSLT 3.0 uses different conventions (mode="azure.workflow.datamapper", explicit namespace exclusions). The skill tracks both so Claude doesn't confuse them.
Debug taxonomy
For Debug mode, Claude classifies the bug before attempting a fix:
| Class | Danger |
|---|---|
| Namespace mismatch — empty output, XPath returns nothing | High — exit code 0, looks like success |
C# compile error — Roslyn error in msxsl:script
|
Obvious — exit code 1 |
| Template priority conflict — wrong template fires | Medium |
| Version mismatch — works in Saxon, fails in .NET | Medium |
Namespace mismatch is the most dangerous: the transform succeeds with valid XML structure but every value is empty.
The output bundle
For Generate and LML modes, the skill requires a complete output bundle.
Generate mode writes:
- Launch config — appended to
.vscode/launch.json - Sample input XML — if none exists for the source schema
- Stylesheet — the
.xsltfile
LML mode writes (order matters):
- Source and target schemas — XSD files in
Artifacts/Schemas/, if not already present - Launch config — points at the compiled
.xslt - Custom extension functions — if the map uses them (must be written before LML compiles)
- Sample input XML — if none exists for the source schema
- LML file — written last (compile hook fires on this)
- Compiled XSLT — generated automatically by the compile hook
A launch config:
{
"name": "Debug Order2Shipment",
"type": "xslt",
"request": "launch",
"engine": "saxonnet",
"stylesheet": "${workspaceFolder}/Artifacts/Maps/Order2Shipment.xslt",
"xml": "${workspaceFolder}/Artifacts/SampleData/Order.xml",
"output": "${workspaceFolder}/Artifacts/Maps/out/Order2Shipment-out.xml",
"stopOnEntry": true
}
output is optional — omit it to see results in the Debug Console only. Press F5 and debug with breakpoints — no manual setup.
LML mode: custom functions and gotchas
LML maps can call custom functions in Artifacts/DataMapper/Extensions/Functions/*.xml:
<customfunctions>
<function name="reformatDate" as="xs:string">
<param name="dateVal" as="xs:date"/>
<value-of select="format-date($dateVal, '[D01]/[M01]/[Y0001]')"/>
</function>
</customfunctions>
The skill documents SDK bugs Claude needs to work around:
-
Zero-parameter functions crash the compiler —
NullReferenceException, entire XML file silently skipped. Workaround: add a dummy<param>. -
Multi-step paths in function arguments fail — wrap in
xpath(). -
Bare
@attris invalid YAML — usexpath("@lineNum").
What the skill doesn't do
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.
Its value is preventing mistakes before they happen. Part 2 covers the hooks that catch errors at write time.
The XSLT Debugger extension is on the VS Code Marketplace for macOS and Windows.
Top comments (0)