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 field the input does not have counts as null. Where other text of the template surrounds it,
null is written as the four characters null: "name": "{{ $.missing }}" yields
"name": "null", and Hello {{ $.missing }} yields Hello null. A template that is only the
{{ }} yields null itself, which leaves as an empty body. To send the JSON value null for a
missing field, put the {{ }} outside quotes: "name": {{ $.missing }} yields "name": null.
Values in a JSON template
When the text of the template is a JSON document — it opens with { or [ and closes with } or
] — what a {{ }} writes depends on where it stands in the text:
- Between quotes, as a value, inside a longer string or as a key (
"{{ x }}","id-{{ x }}",{"{{ k }}": 1}), it is always one string, whatever the type of the value. A string is escaped: a quote, a backslash, a tab, a newline or any other control character becomes its JSON escape. An object or an array is written as its JSON text, escaped the same way, so{"a":1}arrives as"{\"a\":1}". An XML node is written as its text, escaped. A number, a boolean and null are written as their JSON token (1.5,true,null), inside the string. - Outside quotes (
{"a": {{ x }}},[{{ x }}]), a number, a boolean, null, an object or an array is written as the JSON it is. A string is written as one JSON string, with its quotes, escaped the same way:"1, 2, 3"in[{{ x }}]gives["1, 2, 3"], one element, and"5"stays the string"5". To write the number a string holds, convert it:{{ number(x) }}.
Either way a value cannot close a string, add a key of its own or turn into an array or an object, and the document stays JSON.
Text between quotes is text, markup included: a < inside a string is a character like any other,
so {"h": "<p>hi</p>"} answers {"h":"<p>hi</p>"} and {"note": "a<b"} answers {"note":"a<b"}.
Outside quotes, a < followed by a letter starts an XML element, as in any template
({"a": <b>x</b>}), and the template is then no longer read as a JSON document.
{ "user": "{{ ctx.headers['x-user'] }}", "role": "guest"}A header x-user: ","role":"admin answers {"user":"\",\"role\":\"admin","role":"guest"}: one
string, and the role is still guest.
To splice a fragment of JSON that a string holds, mark the interpolation with raw:
{ "order": {{ $.orderJson | raw }}, "note": "{{ $.note }}"}raw means the string is JSON you wrote or fetched, so it is written as it is. Never put a value a
caller sent behind raw. Write the parentheses when the expression is longer than a path or a call:
{{ (a + b) | raw }}. Without raw, a string in a place where JSON expects a value ("order": {{ $.orderJson }}) is written as one JSON string, and the answer stays JSON.
A JSON template whose text gives a {{ }} no single place is refused when the flow is validated or
deployed, with an error that names the step and the expression:
- the branches of an
{{ if }}end in different places, one inside a string and the other outside (a missingelsecounts as a branch that changes nothing); - the body of a
{{ for }}does not end where it started, so each pass would put the next one’s values in the other place; - inside a string, the text right before a
{{ }}ends on a single\, which would escape the value’s first character. Write\\for a backslash.
Balance the quotes the same way in every branch and inside every loop body. A < followed by a
letter after such an {{ if }} or {{ for }} cannot be placed inside or outside a string either:
the template is refused, with an error that points at the <.
Only a template that reads as a JSON document is escaped: a plain-text template, an XML template and
the text of an attribute are written as they are. A template that starts or ends with a {{ }} is
not recognised as a document; there, write json_escape(value) around a value outside quotes and
json_escape(value, true) around a value between quotes.
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. In a
template that reads as a JSON document, a < between quotes is text, not an element; see Values in
a JSON template.
```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.mdflows/reshape.flow.md:16:1: error: 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>…error: 1 validation error(s)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.