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:
$ nexus validate flows/reshape.flow.mderror: step 'reshape': this XSLT sheet is outside the subset this platform compiles, and there isno 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.