Skip to content

Transforming data

The ntd fence reshapes the message. It is a text template: everything inside it is literal text except the constructs listed below. That single fact explains most of its behaviour — a template that looks like JSON produces JSON because the text you wrote happens to be valid JSON, not because the fence understands JSON.

## Step: build-response
```ntd
{
"orderId": "{{ $.orderId }}",
"total": {{ $.total }},
"receivedAt": "{{ date_format(now()) }}"
}
```

Note where the quotes are. "{{ $.orderId }}" produces a JSON string; {{ $.total }} without quotes produces a JSON number. You are writing the punctuation yourself.

Interpolation

{{ expression }} evaluates and inserts the result. Any expression works — see Expressions.

{{ $.customer.name }}
{{ upper($.code) }}
{{ length($.items) }}
{{ $.qty > 10 ? "bulk" : "single" }}

A null result inserts nothing, so "name": "{{ $.missing }}" yields "name": "".

Conditionals

{{ if <expression> }} ... {{ else }} ... {{ end }}

The else branch is optional. The condition must evaluate to a boolean; anything else is a step error.

```ntd
{
"id": "{{ $.id }}"
{{ if !is_empty($.note) }},
"note": "{{ $.note }}"
{{ end }}
}
```

Mind the commas. Because the fence is text, a conditional block that omits a comma produces invalid JSON, and the result comes back as a string instead of an object. Put the comma inside the conditional, as above.

Loops

{{ for <name> in <expression> }} ... {{ end }}

The current element is reachable two ways: by the name you declared, and as $. They are the same value. A name that is not bound by an enclosing loop is a publish-time error that names what is in scope — it does not quietly become the text of the name.

```ntd
<Order id="{{ $.orderId }}">
{{ for item in $.items }}<Line sku="{{ item.sku }}">{{ item.qty }}</Line>{{ end }}
</Order>
```

A loop works the same way in a JSON template:

```ntd
{
"count": {{ length($.items) }},
"skus": [{{ for item in $.items }}"{{ item.sku }}",{{ end }}""]
}
```

There is no separator construct and no index. The trailing "" above absorbs the last comma — crude, but it keeps the output valid. When the shape matters more than the terseness, build the array in a foreach step instead; see Composing flows.

Constructing XML

An element written literally becomes a real XML node, not text. Attribute values interpolate.

```ntd
<order id="{{ $.orderId }}" xmlns="urn:acme:orders">
<customer>{{ $.customer.name }}</customer>
<total currency="{{ $.currency }}">{{ $.total }}</total>
{{ for item in $.items }}
<line sku="{{ item.sku }}" qty="{{ item.qty }}"/>
{{ end }}
</order>
```

The result is an XML value that keeps element order, attributes and text nodes. Send it with content_type: application/xml and it is serialised properly — no string assembly, no escaping mistakes. See JSON and XML.

If such a step is the flow’s last one, that value is also the flow’s answer: /run returns the document itself, with Content-Type: application/xml; charset=utf-8. Build an object in the last step instead and the caller gets that JSON, with application/json. Either way the body is the output, unwrapped, and success is the status code — see The response body and Answering with XML.

What the fence returns

A template produces a sequence of segments — literal text, interpolated values, elements — and those are reduced to one value by fixed rules. Knowing them saves a lot of confusion.

First, a template with no {{ }} and no elements at all — wholly literal text — is read as a document: parsed as JSON if it is valid JSON, and taken verbatim as a string otherwise. Its whitespace is content, so none of it is discarded.

Every other template is a sequence of segments. Whitespace-only text segments are discarded first — there they separate interpolations rather than carry meaning. Then:

Meaningful segments Result
none null
exactly one that value, unwrapped
several, any of them XML an array of the segments
several, none XML joined into one string, then parsed as JSON; if it parses, that value, otherwise the string

The first rule was added after the defect it prevents was measured: a template forming valid JSON but containing no interpolation used to return its text, so the caller got the business answer as a string — at the time, escaped inside the response envelope that 1.0 removed. No error anywhere, a plausible output instead of a refusal. Adding a {{ }} no longer changes the type of the output.

The decision is made on the template’s operation, not on the shape of the value, which is why \n{{ $.x }}\n still lands on “exactly one segment” and the string "42" stays a string there instead of becoming the number 42.

Three further consequences worth internalising.

A template that produces valid JSON gives you an object or array. That is the JSON case in the last row — the join happens first, the parse second.

A template that produces slightly invalid JSON gives you a string. A missing comma or a trailing one does not raise an error; you get the whole thing as text, and the next step sees a string where it expected an object. If a downstream expression suddenly fails with “field access on non-object value”, check the template’s punctuation first.

A single interpolation is passed through unwrapped, keeping its type:

```ntd
{{ $.items }}
```

That yields the array itself, not a stringified copy. It is the idiomatic way to narrow the message to one of its parts.

{{ if }} and {{ for }} contribute their own segments to the surrounding template, rather than one segment holding a list. So { "a": {{ if $.flag }}1{{ else }}2{{ end }} } joins to { "a": 1 } and parses as an object, and a loop’s body lands inline between whatever precedes and follows it. An array that is a value — {{ $.tags }} — is not touched: it is still one segment holding ["a","b"], which is what makes it usable inside a JSON template.

Mixing XML and text gives you an array of segments rather than a document, so keep an XML template purely XML — put nothing outside the root element.

Reformatting with XSLT

The xslt fence applies a stylesheet to an XML message.

## Step: reshape
```xslt
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:template match="/">
<summary>
<id><xsl:value-of select="/order/@id"/></id>
<customer><xsl:value-of select="/order/customer"/></customer>
</summary>
</xsl:template>
</xsl:stylesheet>
```

A subset of XSLT 1.0 and 2.0 is compiled at build time into the same operation tree the ntd fence uses, which is why it executes with no XSLT engine at run time.

Compiled and executed: literal result elements, xsl:value-of, xsl:copy-of, xsl:if, xsl:choose with xsl:when and xsl:otherwise, xsl:for-each, and attribute value templates. The identity-with-pruning idiom — a single template using xsl:if test="normalize-space(...)!=''" around xsl:copy and xsl:apply-templates — is recognised and compiled to an empty-element-pruning operation.

Not compiled, and refused: xsl:apply-templates in any other arrangement, xsl:call-template, xsl:variable, named templates, and multi-template stylesheets in general. A sheet using any of those is rejected by nexus validate and nexus deploy, with the offending construct named:

Terminal window
$ nexus validate flows/reshape.flow.md
error: step 'reshape': this XSLT sheet is outside the subset this platform compiles, and there is
no XSLT engine at run time — unsupported XSLT construct <xsl:call-template>…

It used to deploy cleanly and fail at the first real message instead. Whether a sheet lowers is a property of the sheet, so there is no reason for you to find out in production.

For anything the subset does not cover, express the transformation with ntd instead. It reaches the same result: descendants() for //name searches, string() for an element’s text, {{ if }} and {{ for }} for xsl:choose and xsl:for-each.