Skip to main content

Receive, Transform and Send XML

The problem​

Plenty of business systems still speak XML: EDI gateways, logistics providers, customs brokers, older ERP modules. Often the system you receive XML from and the system you send it to use different XML formats. Element names, nesting, date formats and units all differ.

You could write string manipulation in a Code step, but XML is hard to get right by hand.

The solution​

Don't edit XML directly. Turn it into JSON, reshape the JSON, and turn it back:

XML round trip: a REST API step gets the XML, a Converter turns it into JSON, a Transform reshapes it, a second Converter turns it back into XML and a REST API step posts it.

Each step does one job:

StepJob
REST APIFetch the XML document from the source system
ConverterTurn the XML into JSON
TransformMap the source structure to the target structure with JSONata
ConverterTurn the reshaped JSON back into XML
REST APISend the new XML document to the target system

All the business logic lives in one place, the Transform step. The rest is plumbing.

The scenario​

A supplier publishes order confirmations as XML. Your third-party warehouse (3PL) wants an inbound notice in its own XML format, so it can plan receiving. The workflow fetches the supplier's confirmation and sends the matching inbound notice to the warehouse.

Prerequisites​

  • The URLs of the supplier endpoint and the warehouse endpoint, and how to authenticate with each. If they need a login, create a Connection for each.
  • Familiarity with the REST API, Converter and Transform components.

Step 1 — Fetch the XML​

Add a REST API step:

SettingValue
Use connectionOn, with the supplier's connection (or off, if no login is needed)
Endpoint URLhttps://supplier.example.com/api/confirmations/{{{ body.orderNumber }}}
HTTP Methodget

Under Headers, select Add header and ask for XML:

KeyValue
acceptapplication/xml

Under Advanced, set Version to 3.

The supplier returns a document like this:

<?xml version="1.0" encoding="UTF-8"?>
<OrderConfirmation xmlns="urn:example:orders:v1">
<Header>
<OrderNumber>PO-10023</OrderNumber>
<OrderDate>2026-09-28</OrderDate>
<Supplier id="SUP-881">Nordlys Components AS</Supplier>
</Header>
<Lines>
<Line number="1">
<Item>BOLT-M8</Item>
<Quantity unit="PCS">500</Quantity>
<Price currencyID="EUR">0.12</Price>
<ConfirmedDate>2026-10-05</ConfirmedDate>
</Line>
<Line number="2">
<Item>NUT-M8</Item>
<Quantity unit="PCS">500</Quantity>
<Price currencyID="EUR">0.05</Price>
<ConfirmedDate>2026-10-07</ConfirmedDate>
</Line>
</Lines>
</OrderConfirmation>

Step 2 — Convert the XML to JSON​

Add a Converter step and choose XML → JSON, which is the default conversion.

The XML options decide how the JSON comes out:

OptionWhat it doesSetting for this scenario
Root elementThe name of the top-level element. Mainly relevant when converting to XML (see Step 4).Leave empty
Attribute name prefixThe text placed in front of attribute names, so you can tell <Supplier id="…"> (an attribute) apart from a child element called id.Leave the default (@_)
Ignore namespaceRemoves namespace prefixes such as ns0: from element names. Turn it on when the source uses prefixed elements and you don't care about namespaces.Off: this document uses a default namespace with no prefixes
Ignore attributesDrops all attributes from the output.Off. The supplier ID, the unit and the currency are all attributes, and you'd lose them

With these settings, the converted document looks like this:

{
"OrderConfirmation": {
"Header": {
"OrderNumber": "PO-10023",
"OrderDate": "2026-09-28",
"Supplier": { "#text": "Nordlys Components AS", "@_id": "SUP-881" }
},
"Lines": {
"Line": [
{
"Item": "BOLT-M8",
"Quantity": { "#text": "500", "@_unit": "PCS" },
"Price": { "#text": "0.12", "@_currencyID": "EUR" },
"ConfirmedDate": "2026-10-05",
"@_number": "1"
},
{
"Item": "NUT-M8",
"Quantity": { "#text": "500", "@_unit": "PCS" },
"Price": { "#text": "0.05", "@_currencyID": "EUR" },
"ConfirmedDate": "2026-10-07",
"@_number": "2"
}
]
},
"@_xmlns": "urn:example:orders:v1"
}
}

Three rules explain the shape:

  • An element with only text becomes a plain value: "OrderNumber": "PO-10023".
  • An element with attributes becomes an object. Its text goes in #text, and each attribute becomes a key with the Attribute name prefix in front: "@_id": "SUP-881".
  • An element that appears more than once becomes a list: "Line": [ … ].
tip

Run the workflow once and open Execution Logs to see the exact JSON the Converter produced for your document. Write the Transform in Step 3 against that output, not against a guess. If your output sits under a body field, put body. in front of the paths in Step 3.

Step 3 — Reshape the JSON with Transform​

The warehouse expects this format:

<InboundNotice version="2">
<Reference>PO-10023</Reference>
<SupplierCode>SUP-881</SupplierCode>
<SupplierName>Nordlys Components AS</SupplierName>
<ExpectedLines>
<ExpectedLine lineNo="1">
<Sku>BOLT-M8</Sku>
<Qty uom="PCS">500</Qty>
<ExpectedDate>20261005</ExpectedDate>
</ExpectedLine>
…
</ExpectedLines>
<TotalValue>85</TotalValue>
</InboundNotice>

Add a Transform step that builds the warehouse structure in JSON, using the same conventions the Converter uses: @_ for attributes and #text for an element that has both attributes and text.

(
$order := $context.data.all.converter_1.OrderConfirmation;

/* the warehouse wants dates as YYYYMMDD */
$toDate := function($d) { $replace($d, "-", "") };

{
"@_version": "2",
"Reference": $order.Header.OrderNumber,
"SupplierCode": $order.Header.Supplier.`@_id`,
"SupplierName": $order.Header.Supplier.`#text`,
"ExpectedLines": {
"ExpectedLine": $order.Lines.Line.{
"@_lineNo": `@_number`,
"Sku": Item,
"Qty": { "@_uom": Quantity.`@_unit`, "#text": $number(Quantity.`#text`) },
"ExpectedDate": $toDate(ConfirmedDate)
}[]
},
"TotalValue": $round($sum($order.Lines.Line.($number(Quantity.`#text`) * $number(Price.`#text`))), 2)
}
)

Some details in this expression:

  • converter_1: the ID of the Converter step from Step 2. Naming it makes clear where the JSON comes from. See How step IDs are formed.
  • Backticks around `#text` and `@_id`: JSONata field names can't normally start with # or @, so wrap them in backticks.
  • [] after the line mapping: this keeps ExpectedLine a list even when the order has only one line. See Pitfalls.
  • $number(...): XML has no number type, so treat every value as text and convert it before you calculate.
  • $toDate: a small helper function, declared once and reused. See the JSONata Masterclass for more on structuring expressions.

The Transform outputs:

{
"@_version": "2",
"Reference": "PO-10023",
"SupplierCode": "SUP-881",
"SupplierName": "Nordlys Components AS",
"ExpectedLines": {
"ExpectedLine": [
{ "@_lineNo": "1", "Sku": "BOLT-M8", "Qty": { "@_uom": "PCS", "#text": 500 }, "ExpectedDate": "20261005" },
{ "@_lineNo": "2", "Sku": "NUT-M8", "Qty": { "@_uom": "PCS", "#text": 500 }, "ExpectedDate": "20261007" }
]
},
"TotalValue": 85
}

Step 4 — Convert the JSON back to XML​

Add a second Converter step and choose JSON → XML:

OptionSetting for this scenario
Root elementInboundNotice: the top-level element that wraps the output. The Transform doesn't include it, so it's set here.
Attribute name prefix@_: the same prefix the Transform used, so "@_lineNo" becomes the attribute lineNo="1" instead of a child element

The Converter turns every @_ key into an attribute and every #text value into element text, producing the InboundNotice document shown in Step 3.

note

The Attribute name prefix must match in both places. If the Transform writes @_lineNo but the Converter expects a different prefix, you get a <@_lineNo> child element instead of an attribute, and the receiving system rejects the document.

Step 5 — Send the XML​

Add a final REST API step:

SettingValue
Use connectionOn, with the warehouse's connection
Endpoint URLhttps://3pl.example.com/api/inbound-notices
HTTP Methodpost

Under Headers:

KeyValue
content-typeapplication/xml

Under Request body, use the Body path tab and set it to $, to send the output of the previous step: the XML document. Under Advanced, set Version to 3.

Check the step's statusCode in Execution Logs. Remember that a failed request doesn't stop the workflow by itself: the step returns the error code, and the warehouse's error message is in body. To flag failed runs automatically, see Error Handling Deep Dive.

Pitfalls​

One element or many?​

The Converter can only tell that an element repeats when it actually repeats. An order with two <Line> elements gives you a list. An order with one <Line> gives you a single object:

Source XML hasLines.Line becomes
Two or more <Line>A list: [ { … }, { … } ]
Exactly one <Line>An object: { … }

This is the most common XML bug: the workflow works in testing with multi-line orders and breaks on the first single-line order.

In JSONata, [] at the end of a path always gives you a list:

OrderConfirmation.Lines.Line.{ "Sku": Item }[]
Order linesWithout []With []
1{ "Sku": "BOLT-M8" }[ { "Sku": "BOLT-M8" } ]
2[ { … }, { … } ][ { … }, { … } ]

Add [] wherever the target system expects a repeating element.

Numbers arrive as text​

"500" is text, not a number. Arithmetic functions such as $sum fail on text with an error like Argument 1 of function "sum" must be an array of "numbers". Wrap values in $number() before calculating, as TotalValue does above.

Elements with and without attributes look different​

<Quantity>500</Quantity> converts to "Quantity": "500". <Quantity unit="PCS">500</Quantity> converts to "Quantity": { "#text": "500", "@_unit": "PCS" }. If a supplier adds an attribute to an element later, the paths in your Transform break. When a source is inconsistent, handle both shapes:

$type(Quantity) = "object" ? Quantity.`#text` : Quantity

Namespaces​

Some documents prefix every element with a namespace, such as <ns0:OrderNumber>. Without Ignore namespace, the prefix is part of the JSON key (ns0:OrderNumber). It's awkward to reference, and it breaks if the sender changes the prefix. Turn Ignore namespace on unless the target system needs the namespaces. Namespace declarations (xmlns="…") can still appear as attributes such as @_xmlns. The Transform above ignores it, which is fine.

If the target system needs a namespace, add it in the Transform as an attribute of the top-level element, for example "@_xmlns": "urn:example:3pl:v2".

Further reading​