Chapter 15

JSONata, Mustache, and data transformation

A piece of text that looks like a “template” may be processed by one of four entirely different engines in Node-RED and Home Assistant. This chapter first identifies the execution environment, then uses fixed, manually supplied input to practise bounded type conversions so the data contract remains predictable before the message reaches the next node.

Why this chapter matters

When all you need is a Celsius-to-Fahrenheit conversion, JSONata in a Change node makes the input and output easier to see than a full JavaScript function. When you need to compose a short line of text for a person to read, Mustache in a Template node is more direct. The problem is that JSONata, Mustache, JavaScript in a Function node, and Home Assistant Jinja all use variables, parentheses, or braces, but they do not share syntax, types, context, or execution locations. Pasting syntax from one environment into another field may not only cause an error, but can also produce a string that appears valid while having the wrong type.

This chapter targets Node-RED 5.0.2 and Home Assistant WebSocket nodes 0.80.3. Start with the least-capable tool that meets the requirement, using the matrix in the next section. If a transformation needs multiple branches, explicit error objects, or complex loops, move to the Function node in Chapter 16 rather than turning JSONata into a hard-to-maintain program.

Safety boundaries in this chapter: Every runnable example uses only a manual Inject node, a Change node, and a Debug node. The examples do not read environment settings, credentials, files, networks, or Home Assistant entities, and they do not call any actions. You can inspect each transformation in an offline Node-RED editor.
5.0.2/0.80.3 version notes: Node-RED 5.0.2 also includes the core Range, Delay, Trigger, Exec, RBE, JSON, CSV, HTML, XML, and YAML nodes that are relevant to transformation. Version 0.80.3 provides capabilities specific to Home Assistant nodes. Exec runs programs and must not be used as a shortcut for ordinary data conversion. Before using any parser, you must still bound the input size and shape.

Distinguish the four execution environments first

First identify which node provides the field, then choose the syntax. Do not call something a template merely because braces appear on the screen. The following table compares where each option runs, its input root, its types, and its escaping behaviour.

EnvironmentWhere it runsPrimary input and contextResult typeEscaping considerations
Node-RED JSONataIn the Node-RED runtime; for example, when a Change or Switch typed input is set to expressionThe input root is the entire message, so use payload, not msg.payload; the runtime separately provides $flowContext(), $globalContext(), and $env()Can return a string, number, Boolean, null, array, or object; it may return no result when no value matchesIt is not a text template and does not perform HTML escaping; validate its output against the next node’s data contract
Node-RED MustacheIn the core Node-RED Template nodeLooks up message fields first and can also retrieve values through explicit flow/global context tokensInitially renders text; when the Template output format is JSON or YAML, the text is then parsed into the corresponding typeDouble braces perform HTML escaping by default; triple braces insert raw, unescaped values and are safe only when the output context is known
Function JavaScriptIn the Node-RED Function node sandboxReads the message through msg and provides the node/flow/global context APIsAny JavaScript value you create; what you send must be a message object or an array of messages arranged by outputJavaScript does not automatically apply context-specific escaping for HTML, JSON, or notification text; see Safe JavaScript
Home Assistant JinjaThe Render Template node sends the template to Home Assistant, where HA renders itUses Home Assistant’s template environment, not Node-RED’s msg or flow contextIn 0.80.3, the node defines the result as a string and writes it to the configured result locationHA/Jinja applies its own semantics; if the result will be used as JSON, Node-RED must still parse and validate it explicitly
RequirementPreferred toolReasonStop condition
Set, move, or delete one propertyChangeThe rules are visible in the node settings; no code is requiredAdd a Switch first if the input shape has not been validated
Calculate a new object from a fixed JSON structureJSONataPreserves number, Boolean, array, and object typesSplit the operation into smaller steps or use a Function node if it scans an unbounded collection or becomes difficult to understand
Generate a short notification or explanatory textMustache TemplateInterpolation semantics are simple, with HTML escaping enabled by defaultChoose another tool when inserting a complete object or producing a non-string value
Produce multiple outputs, explicit errors, or complex branchesFunction JavaScriptProvides clear control flow and message contractsDo not add external I/O, unbounded loops, or arbitrary modules
Use HA template entity/state semanticsHA Jinja through Render TemplateHome Assistant runs the template in its own environmentDo not use it when the work must be offline, low-latency, or confined to one system
Convert external text into a structureThe corresponding parser nodeJSON, CSV, HTML, XML, and YAML each have a core parserRefuse to parse input until its size and shape are bounded

The same name does not guarantee the same type in all four environments. For example, a Home Assistant state is often a string; JSONata multiplication is numeric, whereas Mustache only inserts values into text. Guard the transformation with type checks and do not rely on implicit conversion.

Complete one safe transformation with a fixed flow

The example 09-jsonata-transform.json contains only a manual Inject node, a Change node, and a Debug node. The Inject node has no schedule and does not send automatically at startup. Its data is a fixed temperature object, and the entire path has no external I/O.

  1. Inspect the flow as text after downloading it.

    Confirm that the sole input has empty strings for both repeat and crontab, and that once is false. Confirm that there are no server configurations, credentials, URLs, files, or external nodes. Complete this check before importing the flow.

  2. Import it into a separate tab and verify the nodes.

    After import, the flow should contain only “Manually Send Temperature Data”, “Convert Temperature Data”, and “View Conversion Data”. The Change rule targets msg.payload, and its source type is a JSONata expression. If the editor shows different nodes or additional connections, do not deploy; obtain a fresh copy of the pinned file from this site.

  3. Understand the expression before deploying it.

    The expression reads payload.temperature from the top-level message and creates a new object; it does not modify an external system. The fixed expression is:

    {"Celsius": payload.temperature, "Fahrenheit": payload.temperature * 9 / 5 + 32}

    For an input of {"temperature":25}, the expected msg.payload is an object containing 25 degrees Celsius and 77 degrees Fahrenheit, not JSON text.

  4. Deploy the smallest possible scope and trigger it manually once.

    This is a pure-data flow, but first make sure its workspace is not connected to any other branch. After deployment, press Inject exactly once and use Debug to inspect the payload type and its two fields. Do not stress-test by triggering Inject many times, and do not leave complete messages visible in Debug for an extended period.

  5. Validate the contract with boundary inputs.

    Temporarily change the manual payload to {"temperature":0} and {"temperature":-10}, and confirm results of 32 and 14 Fahrenheit respectively. With JSONata 2.2.2 in Node-RED 5.0.2, keep the fixed expression {"Celsius": payload.temperature, "Fahrenheit": payload.temperature * 9 / 5 + 32}. Then change the payload to {}; Debug receives msg.payload as an empty object, {}. The expression has returned an object, but omitted both properties because their values are undefined. If the expression contained only payload.temperature, the entire expression would instead return no result. In a production flow, a Switch must reject both cases before they can reach an action.

Acceptance conditions: Only you can trigger the flow, and only manually; Debug shows only local demonstration values; the output is an object; the flow neither creates context nor reads environment settings, and it has no external side effects.

JSONata: message-rooted declarative transformation

JSONata is a functional, declarative language for JSON structures. In a Node-RED typed input, the input document is the top-level message, so payload.temperature is the correct path in this example. Writing msg.payload.temperature looks for a top-level property named msg and will usually not return the value you intended.

A safe transformation should define its output shape explicitly. The following example limits an array to its first 20 entries and retains only allowed fields. It performs no I/O and does not access any settings:

(
  $items := payload.items[type = "reading"][[0..19]];
  $items.{
    "name": $string(name),
    "value": $number(value),
    "valid": $type(value) = "number"
  }
)

Here, [[0..19]] selects an index range. Your data contract should determine the real upper limit; do not treat the number in this example as a universal limit for every system. If the input may be either a singleton or an array, you can use an array constructor to produce a semantically stable output, but you must still test null values and no-result cases separately. When JSONata cannot find a path, it may return “no result”, which is distinct from an explicit null, an empty array, or false.

Additional helpers provided by HA nodes

The JSONata service in Home Assistant WebSocket nodes 0.80.3 adds the following functions only within Home Assistant nodes; the core Change node does not automatically gain them. These helpers read entity, device, and area data currently available to the integration. They cannot be transferred for use in Mustache, Function JavaScript, or HA Jinja.

HelperExact use in 0.80.3Usage boundary
$entity()Returns the entity object that triggered the current nodeNot every node or event has a current entity; handle undefined first
$prevEntity()Allows an event node to retrieve the previous state entityDo not assume it exists during initialisation, creation, or deletion
$entities()/$entities(entity_id)Returns every cached entity, or one entity selected by entity IDThe full collection may be large; use an environment placeholder for a single ID, and do not put a real ID in the tutorial or logs
$areas(lookup)Returns areas when called without an argument; lookup can be an area, entity, or device IDAreas and IDs are environment-specific data, and a result may not exist
$areaDevices(areaId)/$areaEntities(areaId)Lists devices or entities associated with the specified areaConstrain the area first, then limit the result count so each message does not scan a large collection
$device(lookup)/$deviceEntities(device_id)Finds a device by entity ID or device name, or returns entities associated with a deviceNames may change or be duplicated; do not include real identifiers in a shareable flow
$outputData(name)Returns additional output data passed to the HA node’s JSONata service; omitting name returns the available data setAvailable keys depend on the node’s call context; do not assume a fixed shape
$sampleSize(collection,n)/$randomNumber(lower,upper,floating)Lodash sampling and random-number helpers exposed in 0.80.3Results are non-deterministic; do not use them for security, authorisation, or control decisions that must be reproducible

The safe flow in this chapter deliberately avoids these helpers because it must work completely offline without an HA connection. If a production flow needs them, first use placeholders to create independent test messages, limit the number of results, and show only selected, non-sensitive fields in Debug.

Mustache: compose strings, not objects

The core Template node in Node-RED 5.0.2 uses Mustache. Double braces look up values in the input message. For example, the following plain-text template reads msg.payload.label and msg.payload.value:

Reading name: {{payload.label}}
Reading result: {{payload.value}}

The rendered result is text. If a value contains & or angle brackets, double braces apply HTML escaping; triple braces disable that protection. Raw interpolation is not a universal “fix garbled text” switch. When the output goes to HTML, raw interpolation may cause untrusted content to be treated as markup. When the output will become JSON, do not rely on string concatenation to handle quotation marks, backslashes, and newlines. Use JSONata to create an object directly, or have the Template node’s JSON output parse a fixed template and then validate the resulting type.

A Template node can write its result to a msg, flow, or global location. In addition to explicit flow/global context tokens, Mustache lookup can read an environment value through an env.NAME token. When the node’s configured template is an empty string, version 5.0.2 uses the incoming msg.template instead. Do not let an untrusted upstream source control this dynamic template: it can select renderable environment and context values. Likewise, do not place secrets or credentials in an environment or context that the template can read. This chapter uses fixed templates and reads neither environment nor context.

That does not make context the template’s private storage. If a value persists across messages—or, depending on the store, across restarts—define its lifecycle and cleanup rules first. See Chapter 7, Time and Context. JSONata’s $flowContext(), $globalContext(), and $env() follow the same secret-handling boundaries; the fixed expression in this chapter does not use them.

Safe choice: Use Mustache with its default escaping for short, human-readable text. To preserve number, Boolean, array, or object types, prefer JSONata or another typed input in a Change node. Do not convert a value to text and then guess its type.

The boundary between Function JavaScript and HA Jinja

JavaScript in a Function node suits transformations that need explicit control flow, reusable local functions, multiple outputs, or consistent error objects. A Function node receives msg and must send a message object; JSONata treats the whole message as a document and returns the expression result. If a JSONata expression has accumulated layers of variables, hard-to-bound array expansion, and multiple exceptional cases, move it to a Function node, while still keeping the operation purely data-oriented.

In Home Assistant WebSocket nodes 0.80.3, the persisted type of Render Template is api-render-template. It sends a Jinja template to Home Assistant’s HTTP render-template capability, waits for HA to return a string, and writes that string to the configured result location. This request crosses the Node-RED/HA boundary; it is neither local Mustache nor JSONata.

Version 0.80.3 gives message fields priority for all five inputs: msg.template, msg.resultsLocation, msg.resultsLocationType, msg.templateLocation, and msg.templateLocationType. The node does not provide Block Input Overrides. An untrusted upstream source could therefore change the template, the location that stores the original template, the rendered-result location, and its msg/flow/global type. Before a message enters this node, delete all five fields or rebuild them from an allowlist of permitted values. In particular, never allow upstream input to select an arbitrary flow/global write destination. This chapter does not run Render Template, and its fixed example supplies neither a dynamic template nor any renderable secrets.

Bound types, parsing, and performance

A data contract specifies at least four things: permitted input types, required fields, the maximum collection or text size, and the failure path. Checking only that “a payload exists” is not enough, because the payload may be a string, Buffer, array, or deeply nested object. Use a Switch node to separate the paths first, then pass only valid messages to the transformation. For invalid messages, create only a minimal error summary; do not send the complete original content to Debug.

{
  "input": {"temperature": 25},
  "contract": {
    "temperatureType": "number",
    "minimum": -50,
    "maximum": 100
  },
  "output": {"Celsius": 25, "Fahrenheit": 77}
}

This is offline test data, not a flow configuration. In a production flow, record the contract in a Comment node, test instructions, or a version-controlled file. If the original data is a JSON string, first limit its size, then use a JSON parser to convert it to an object, and finally validate its fields. Apply the same process to CSV, HTML, XML, and YAML. A parser can recognise a format, but it cannot decide whether the content is reasonable for your use case, nor will it automatically limit nesting depth, column count, or sequence length.

  • Limit input: Select only the required range before passing a large array into JSONata; avoid descendant-wildcard scans at an unknown depth.
  • Limit output: Create only the fields required downstream; do not copy the entire HA cache or input object.
  • Limit frequency: Repeatedly sorting, grouping, or aggregating a large collection for every high-frequency event consumes runtime resources. Throttle first, or run the operation only when the value actually changes.
  • Limit observation: Configure Debug to show specific fields and disable it when finished. Large volumes of complete messages add serialisation and sidebar overhead.
  • Maintain determinism: Avoid sampling and random numbers in control decisions. The same fixed input should produce the same output so the behaviour can be reproduced.

A safe transformation is not complete merely because “the expression did not report an error”. Its output must satisfy the contract, and the next node must be able to reject invalid data. If the next step may have external side effects, review the input-override and target boundaries in Chapter 11, Action Node. Never treat a successful transformation as sufficient reason to execute an action.

Troubleshooting

  • JSONata shows no result: First confirm that the root path is payload, not msg.payload. Then use a fixed Inject message to check the field spelling and array index. Test “no result”, null, an empty array, and false separately; do not collapse them into one falsy check.
  • A calculation produces a string or NaN: Check the Inject node’s payloadType and the actual type shown by Debug. A Home Assistant state is often a string, but this chapter’s offline example must supply a number. When conversion is required, use $number() explicitly, after confirming that the value falls within the permitted range.
  • Mustache displays an HTML entity: This is usually the default escaping applied by double braces, not data corruption. First confirm the output context. For plain text, accept the escaping or adjust how the result is displayed. For HTML, do not switch to raw interpolation merely for appearance. Use JSONata instead when the output must be an object.
  • JSON/YAML Template parsing fails: Do not insert unescaped dynamic text directly. Reduce the problem to a fixed template and fixed test value, then check quotation marks, line breaks, and the output format. If you need a dynamic object, use JSONata to create a typed value directly.
  • HA helpers are unavailable in a Change node: These helpers are injected only by the Home Assistant node JSONata service in Home Assistant WebSocket nodes 0.80.3. The core Change node does not provide them. Restructure the flow to use message input without a helper, or use the helper only in a supported HA node field.
  • Render Template returns nothing: It requires an HA connection and Home Assistant must execute the Jinja template. First stop retries and any downstream side effects, then verify the connection and Catch path. Do not experiment by substituting Mustache or JSONata syntax. Continue detailed diagnosis in Chapter 18, Debugging and Testing.
  • A transformation causes noticeable delays: Disable the high-frequency input and reproduce the issue with a small, fixed data set. Measure the array length and determine whether the expression sorts data or scans every descendant; then constrain the data before transforming it. Do not trigger increasingly large test sets over and over.

Pinned sources and official documentation

This chapter’s technical boundaries are based on the two exact commits approved for Chapter 15; official documentation supplements them for general concepts. If rolling documentation differs from the pinned version, the behaviour in the pinned source code takes precedence.

FAQ

Why not use msg.payload in JSONata?
Node-RED treats the entire input message as JSONata’s top-level document, so use payload directly. JavaScript in a Function node accesses the same field as msg.payload.
Can Mustache insert an object directly?
Mustache’s primary purpose is to render text. Inserting an object can easily produce a string that is unsuitable for downstream use. When you need to retain an object or array type, use JSONata or a Function node to create the result.
Do triple braces solve every escaping problem?
No. They disable Mustache’s default escaping and may allow untrusted content into HTML. Choose the tool for the output context, and do not assemble object data by interpolating raw text.
Can HA’s JSONata helpers be used in a core Change node?
Do not assume so. Version 0.80.3 injects these helpers into the JSONata service used by Home Assistant nodes. This chapter’s core Change example uses only standard JSONata and the input message.
What is the quickest way to distinguish Render Template from Template?
The core Template node executes Mustache in Node-RED. Render Template sends Jinja to Home Assistant for execution and receives a string. They differ in execution location, context, and failure modes.
When should a transformation be rewritten as a Function?
Use a Function node when the expression requires unreadable layers of branching, complex exceptional cases, explicit multiple outputs, or reusable procedures. Continue to bound the input and keep the operation purely data-oriented.