DEV Community

Daniel Jonathan
Daniel Jonathan

Posted on

Skip the Designer — Editing Logic Apps Data Mapper LML Files Directly

The Azure Logic Apps Data Mapper has a visual designer in VS Code. You drag lines between schemas, drop in functions, and it generates an .lml file (YAML) that compiles to XSLT 3.0.

In theory, you never touch the YAML. In practice, the designer has reliability problems that make direct LML editing the better workflow.


Why skip the designer?

The designer doesn't always persist changes correctly:

  • Changes revert on reopen — save, close, reopen, and the .lml on disk has the old value
  • String literals lose inner quotesxpath("'kg'") becomes xpath("kg"), changing a literal to an element reference
  • Expressions get corrupted — conditions like xpath("if (...) then 'Y' else 'N'") lose quote characters
  • Function arguments get rewritten — argument order or paths change silently
  • No undo across sessions — you need git diff to catch silent corruption

These happen with normal operations, not edge cases.


What LML looks like

LML is YAML. Here's a complete map:

$version: 1
$input: XML
$output: XML
$sourceSchema: OrderSchema.xsd
$targetSchema: ShipmentOutput.xsd
$sourceNamespaces:
  ns0: http://schemas.contoso.com/Order
$targetNamespaces:
  xs: http://www.w3.org/2001/XMLSchema
Shipments:
  Shipment:
    ShipmentRef: prefixShipRef(/ns0:Order/ns0:OrderID)
    ShipDate: reformatDate(/ns0:Order/ns0:OrderDate)
    Recipient: /ns0:Order/ns0:Customer/ns0:CustomerName
    Lines:
      $for(/ns0:Order/ns0:Items/ns0:Item):
        $if(xpath("ns0:Quantity >= 1")):
          Line:
            Product: ns0:ProductName
            Quantity: ns0:Quantity
Enter fullscreen mode Exit fullscreen mode

Header declares schemas and namespaces. Body maps target elements (left) to source expressions (right).


LML syntax cheat sheet

# Direct mapping
Recipient: /ns0:Order/ns0:Customer/ns0:CustomerName

# Attribute on parent element
$@shipId: /ns0:Order/ns0:OrderID

# Loop over repeating elements (paths inside are relative)
$for(/ns0:Order/ns0:Items/ns0:Item):
  Line:
    Product: ns0:ProductName

# Conditional
$if(xpath("ns0:Quantity >= 1")):
  Line:
    Product: ns0:ProductName

# Custom function (defined in Functions/*.xml)
ShipDate: reformatDate(/ns0:Order/ns0:OrderDate)

# Raw XPath escape hatch
Priority: xpath("upper-case(@priority)")
WeightUnit: xpath("'kg'")   # inner quotes = string literal
Enter fullscreen mode Exit fullscreen mode

Custom extension functions

Defined in Artifacts/DataMapper/Extensions/Functions/*.xml:

<customfunctions>
  <function name="reformatDate" as="xs:string"
            description="Reformats yyyy-MM-dd to dd/MM/yyyy.">
    <param name="dateVal" as="xs:date"/>
    <value-of select="format-date($dateVal, '[D01]/[M01]/[Y0001]')"/>
  </function>
</customfunctions>
Enter fullscreen mode Exit fullscreen mode

Gotchas

Zero-parameter functions crash the compiler. The SDK throws a NullReferenceException and silently skips the entire XML file. Workaround: add a dummy parameter.

Function arguments must be single-step element names. Multi-step paths cause compile errors — wrap them in xpath():

# INVALID
Address: formatAddress(ns0:Customer/ns0:Street, ns0:Customer/ns0:City)

# FIX
Address: formatAddress(xpath("ns0:Customer/ns0:Street"), xpath("ns0:Customer/ns0:City"))
Enter fullscreen mode Exit fullscreen mode

The workflow: edit, compile, verify

1. Edit the YAML

Open the .lml file in any text editor and make your change.

2. Compile to XSLT

Install lml-compile, a dotnet global tool that wraps the Logic Apps SDK:

dotnet tool install -g lml-compile
Enter fullscreen mode Exit fullscreen mode

Compile:

lml-compile Artifacts/MapDefinitions/MyMap-lml.lml Artifacts/Maps/MyMap-lml.xslt
# OK: MyMap-lml.xslt
Enter fullscreen mode Exit fullscreen mode

3. Verify

Run dotnet test or press F5 with the XSLT Debugger extension.


Auto-compile on save

Install the Run on Save extension and add this to your workspace settings (for multi-root workspaces, add it in the .code-workspace file — Run on Save doesn't read folder-level settings):

"emeraldwalk.runonsave": {
  "commands": [
    {
      "match": "\\.lml$",
      "cmd": "lml-compile ${file} ${fileDirname}/../Maps/${fileBasenameNoExt}.xslt"
    }
  ]
}
Enter fullscreen mode Exit fullscreen mode

Note: Run on Save uses its own variables — not VS Code task variables. ${fileBasenameNoExt} and ${fileDirname} work; ${fileBasenameNoExtension} and ${workspaceFolder} do not. Reload the window after adding the config.

Now the cycle is just: edit .lml → save → F5 or dotnet test. The compile happens automatically.

What the compiler catches

  • Malformed YAML / missing $version header
  • Missing schemas in Artifacts/Schemas/
  • Unrecognized function calls
  • Invalid XPath syntax

Errors surface on save instead of at deploy time.


Naming convention

Name hand-authored LML files with a -lml suffix:

Artifacts/MapDefinitions/OrderToShipment-lml.lml  →  Artifacts/Maps/OrderToShipment-lml.xslt
Enter fullscreen mode Exit fullscreen mode

This avoids colliding with hand-authored XSLT files and makes it clear which files are compiled output.


Summary

Use the designer to scaffold the initial map. Then switch to direct LML editing for all subsequent changes.

Edit .lml → save → auto-compile → verify
Enter fullscreen mode Exit fullscreen mode

No designer round-trip. No silent rewrites. No surprises.


The XSLT Debugger extension is on the VS Code Marketplace for macOS and Windows.

Top comments (0)