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:
Each step does one job:
| Step | Job |
|---|---|
| REST API | Fetch the XML document from the source system |
| Converter | Turn the XML into JSON |
| Transform | Map the source structure to the target structure with JSONata |
| Converter | Turn the reshaped JSON back into XML |
| REST API | Send 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:
| Setting | Value |
|---|---|
| Use connection | On, with the supplier's connection (or off, if no login is needed) |
| Endpoint URL | https://supplier.example.com/api/confirmations/{{{ body.orderNumber }}} |
| HTTP Method | get |
Under Headers, select Add header and ask for XML:
| Key | Value |
|---|---|
accept | application/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:
| Option | What it does | Setting for this scenario |
|---|---|---|
| Root element | The name of the top-level element. Mainly relevant when converting to XML (see Step 4). | Leave empty |
| Attribute name prefix | The 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 namespace | Removes 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 attributes | Drops 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": [ … ].
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 keepsExpectedLinea 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:
| Option | Setting for this scenario |
|---|---|
| Root element | InboundNotice: 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.
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:
| Setting | Value |
|---|---|
| Use connection | On, with the warehouse's connection |
| Endpoint URL | https://3pl.example.com/api/inbound-notices |
| HTTP Method | post |
Under Headers:
| Key | Value |
|---|---|
content-type | application/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 has | Lines.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 lines | Without [] | 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
- Converter component: reference for all conversions and options
- Transform component: reference for the Transform step
- JSONata Masterclass: structure larger expressions with variables and helper functions
- REST API in Practice: more patterns for the fetch and send steps
- Execution Logs: inspect the output of each step after a test run