NGSI-LD Builder
The node converts arbitrary JSON objects into normalised NGSI-LD entities. It loads the JSON-LD @context and the matching Smart Data Model schema, offers an interactive field mapping, generates URN ids, detects GeoProperty and Relationship automatically and can validate the result against the schema.
- Node type
- ngsi-builder
- Package
- @htag/node-red-contrib-hypertegrity-ngsi-ld
- Inputs / outputs
- 1 / 1
- Target
- Stellio context broker (NGSI-LD)
When to use this node
The data connector nodes of this library deliver flat objects, for example a NewsObserved or WeatherObserved object with simple key-value pairs. An NGSI-LD context broker such as Stellio expects the normalised NGSI-LD form instead: every attribute is an object with a type (Property, GeoProperty or Relationship), a value and optionally an observedAt timestamp. The NGSI-LD Builder performs exactly this conversion and adds a URN id and the @context.
Typical use cases are:
- Connector output (RSS feed, weather, water levels, parking) converted into Smart Data Model compliant entities and then published with Post to Stellio.
- Third-party APIs or CSV imports mapped step by step onto a data model using the sample JSON and the interactive field mapping.
- Geodata built from separate latitude and longitude fields or from coordinate arrays into a GeoJSON
location. - Data quality ensured by checking required fields, value ranges and enumerations from the schema before the data reaches the broker.
How the node works
Trigger and filter
An incoming message starts processing. An optional topic filter only lets matching msg.topic values through.
Id and skeleton
Entity type, optional id segment and the input id form the URN. The entity receives type and @context.
Mapping
Depending on the mode (Smart, Simple, Custom) input fields become Property, GeoProperty or Relationship.
Refine
Location parsing, transformation script, global fields and overwrites add to or change the entity.
Validate and emit
Optionally the entity is checked against the Smart Data Model schema. The result leaves the node as an array in msg.payload.
Result: An array of normalised NGSI-LD entities that can be handed directly to Post to Stellio.
The node is message-driven: it processes every incoming message immediately and never fetches data on its own. If msg.payload contains an array, every element is converted into one entity; a single object yields an array with one entity. Nested arrays (an array of arrays) are discarded.
When the dialog is opened, the node fetches the @context and the schema of the entity type through a proxy on the Node-RED server. Both are cached for one hour; the Flush/Clear Cache button forces a reload.
Configuration
The edit dialog is organised in five tabs. Labels follow the English user interface.
General tab
| Field | Type | Default | Meaning |
|---|---|---|---|
| Name | Text | empty | Display name of the node in the flow. |
| Topic Filter | Text | empty | Exact match against msg.topic or a regular expression between slashes, for example /^sensor\//. Non-matching messages are discarded. |
| Context URL | Text (required) | see below | URL of the JSON-LD @context. Placeholders of the form ${VAR} are replaced from environment variables when the node starts. The URL is written unchanged into the @context field of every entity. |
| Entity Type | Text (required) | empty | Target NGSI-LD type, for example WeatherObserved. The type must point to a schema in the @context for schema discovery to work. |
| Config ID Segment | Text | empty | Optional name segment in the URN. Result: urn:ngsi-ld:<type>:[<segment>:]<input id>. |
| Flush/Clear Cache | Button | – | Clears the server-side context and schema cache. |
| Enable Debug Mode | Checkbox | off | Writes input, configuration and result as node.warn to the debug sidebar. |
Default values of the URL fields:
Context URL: https://context.${DOMAIN}/main-context
Mapping tab
| Field | Type | Default | Meaning |
|---|---|---|---|
| Mapping Mode | Select (required) | empty | Smart Mode, Simple Mode or Custom Mode, see below. |
| Sample JSON (Key-Value) | Text area | empty | A sample object (or array) of your input data. After Parse Sample all keys, including nested ones in dot notation such as location.coordinates, are available in the drop-downs. |
| Use Global observedAt Overwrite | Checkbox | off | Enables taking the timestamp from an input field. |
| Source Field | Select | empty | Input field holding an ISO 8601 timestamp (must contain a T). If the field is empty or invalid, the system time is used. |
| Ignore Required Fields | Checkbox | off | Allows saving without mapping all required fields. At runtime, items with missing required fields are then no longer dropped. |
| Load/Edit Schema | Button | – | Loads @context and schema and lists all target attributes of the data model with their description. Required fields are marked with *. |
| Search Fields | Text | empty | Filters the list of target attributes by name or description. |
| Field Mapping | List | empty | One source field selection per target attribute plus a clock icon that enables or disables observedAt for that attribute. The type attribute offers the entry « Entity type from General ». |
| Apply Mappings | Button | – | Applies the mapping, checks required fields and hides unmapped attributes. If location is mapped, the node automatically enables location parsing. |
| Custom Mapping | List | empty | Custom mode only: free pairs of source field, target property name and clock icon for observedAt. |
The three modes differ as follows:
- Smart Mode: Target attributes come from the Smart Data Model schema. You assign a source field to each attribute. If a required field is missing at runtime, the item is dropped (unless Ignore Required Fields is enabled).
- Simple Mode: Every key of the input object becomes an attribute of the same name. The keys
id,type,@contextandobservedAtare skipped. Alocationfield with invalid coordinates is omitted. - Custom Mode: Only the pairs from Custom Mapping are applied. An input field
locationis additionally taken over as GeoProperty if not already set.
First enter the Context URL and the Entity Type, for example WeatherObserved. The node looks up the type in the @context, follows the schema URL stored there, resolves $ref and allOf references and lists all attributes of the data model with description and required marker. Then paste a sample JSON, click Parse Sample and choose the matching source field for each attribute.
Location tab
| Field | Type | Default | Meaning |
|---|---|---|---|
| Enable Advanced Location Parsing | Checkbox | off | Builds a GeoProperty location from the fields below. While enabled, mapping entries targeting location have no effect. |
| Geo Type | Select | Point | Point, LineString, Polygon, MultiPoint, MultiLineString or MultiPolygon. |
| Position Format | Select | Separate Fields (Lat, Lon) | Point only: separate fields or a single array [x, y]. |
| Latitude / Longitude | Select | empty | Source fields for separate fields. Values are parsed as decimal numbers. |
| Coordinate Array Field | Select | empty | Source field for the array format of a point. |
| Polygon Array Field | Text | empty | Source field containing the GeoJSON coordinates array for LineString, Polygon and Multi types. |
| Coordinate Order | Select | Lon, Lat (GeoJSON Standard) | Order inside the arrays. With Lat, Lon the node recursively swaps all pairs into GeoJSON order. |
A map preview shows the geometry derived from the sample JSON. Invalid coordinates (latitude outside −90 to 90, longitude outside −180 to 180) are reported as an error below the map.
Logic tab
| Field | Type | Default | Meaning |
|---|---|---|---|
| Custom Transformation Logic | Checkbox | off | Enables the script. |
| Transformation Script (JavaScript) | Code editor | empty | Runs after the mapping in a sandbox. Available are entity (the entity built so far, directly modifiable), item (the input object), msg (a copy of the message with payload = item), console, setTimeout and the helper formatProperty(key, value, observedAt, includeObservedAt). |
The field reference below the editor lists all currently mapped attributes; a click inserts entity.<attribute>.value = at the cursor position. The script is also the place to add metadata the builder does not set automatically, for example entity.temperature.unitCode = "CEL";.
Advanced tab
| Field | Type | Default | Meaning |
|---|---|---|---|
| Global Value Overwrites | List (field, value, type) | empty | Static values for every entity. Types: string, number, boolean, object, array. The field id replaces the complete URN, type the entity type; other fields are set as Property. |
| Enable Data Model Validation | Checkbox | on | Checks the finished entity against the schema. Missing required fields always cause the entity to be dropped. |
| Extra fields not in context | Checkbox | off | Attributes not present in the schema count as invalid. |
| Missing optional fields | Checkbox | off | Optional schema fields without a value count as invalid. |
| Invalid values (type, range, enum) | Checkbox | off | Checks the value of each attribute for type, enum, minimum/maximum, minLength / maxLength and pattern. |
| Handle invalid optional fields | Radio | Skip invalid property | Skip invalid property removes only the affected attribute, Drop entity discards the whole entity, Ignore (just warn) emits it unchanged. |
| Global Custom Fields | List (source, target) | empty | Additional input fields mapped to a target attribute in all modes. A target of id supplies the input id for URN generation. |
Input
| Property | Type | Meaning |
|---|---|---|
msg.payload | object or array of objects | The records to convert. Every element becomes exactly one entity. Elements that are not objects are skipped. An array of arrays is discarded entirely. |
msg.topic | string | Only evaluated when a topic filter is configured. |
msg.payload[].id | string | Serves as input id when neither overwrite, custom field nor mapping supplies one. If the value starts with urn:, it is used unchanged as id. |
msg.payload[].<source field> | any | Source fields according to mapping, location parsing and observedAt source field. Dot notation and array indices such as data.items[0].value are supported. |
msg (whole) | object | Available to the transformation script as msg. |
How the URN id is built
The id is determined in this order:
- Global value overwrite with field
id: the value is the final id. - Global custom field with target
id: the value of the source field is the input id. - Mapping (smart or custom) with target
id: the value of the source field is the input id. - Input field
idof the record. - Random UUID in the form
Entity-<uuid>if none of the above applies.
If the input id already starts with urn:, it is used unchanged. Otherwise the node builds urn:ngsi-ld:<entity type>:<config id segment>:<input id>; the segment is omitted when empty.
How attributes are typed
| Input value | Result |
|---|---|
String starting with urn: | Relationship with object |
Target attribute location or object with a GeoJSON type (Point, LineString, Polygon, MultiPoint, MultiLineString, MultiPolygon) | GeoProperty with value |
| anything else (number, string, boolean, object, array) | Property with value |
If observedAt is enabled for the attribute, the timestamp is added (source field or system time). Target names in dot notation such as address.streetAddress create a Property address whose value is an object containing the sub-field. The node does not set a unitCode automatically; add it in the transformation script if needed.
Output
msg.payload always contains an array of entities, even if only one object came in. All other message properties are preserved.
Example: a weather connector delivers the following flat object. Configured are the entity type WeatherObserved, the config id segment exampletown, the observedAt source field dateObserved, location parsing as a point from the fields lat and lon, and a smart mapping for dateObserved, temperature, relativeHumidity and refDevice.
{
"id": "station-042",
"dateObserved": "2026-10-01T06:00:00Z",
"temperature": 14.2,
"relativeHumidity": 0.81,
"lat": 50.3315,
"lon": 7.3322,
"refDevice": "urn:ngsi-ld:Device:station-042"
}
The node produces:
[
{
"id": "urn:ngsi-ld:WeatherObserved:exampletown:station-042",
"type": "WeatherObserved",
"@context": ["https://context.exampletown.example/main-context"],
"location": {
"type": "GeoProperty",
"value": {"type": "Point", "coordinates": [7.3322, 50.3315]},
"observedAt": "2026-10-01T06:00:00.000Z"
},
"dateObserved": {
"type": "Property",
"value": "2026-10-01T06:00:00Z",
"observedAt": "2026-10-01T06:00:00.000Z"
},
"temperature": {
"type": "Property",
"value": 14.2,
"observedAt": "2026-10-01T06:00:00.000Z"
},
"relativeHumidity": {
"type": "Property",
"value": 0.81,
"observedAt": "2026-10-01T06:00:00.000Z"
},
"refDevice": {
"type": "Relationship",
"object": "urn:ngsi-ld:Device:station-042",
"observedAt": "2026-10-01T06:00:00.000Z"
}
}
]
| Field | Meaning |
|---|---|
id | URN built from entity type, config id segment and input id. |
type | The configured entity type (in smart mode optionally taken from an input field). |
@context | Array with the configured context URL (placeholders already replaced). |
location | GeoProperty from location parsing; coordinates always in GeoJSON order longitude, latitude. |
<attribute>.type | Property, GeoProperty or Relationship according to detection. |
<attribute>.value / .object | Value or target URN of the relationship. |
<attribute>.observedAt | ISO 8601 timestamp in UTC when enabled for the attribute. |
Attributes whose value is still undefined after all steps are removed before emitting.
Example flow
The package ships with an example flow. Import it in Node-RED via Menu → Import → Examples → @htag/node-red-contrib-hypertegrity-ngsi-ld → ngsi-builder. Alternatively download ngsi-builder.json and import it via Menu → Import.
Inject and Function
An Inject node starts a Function node that produces an array of air quality records (AirQualityObserved).
Split and Delay
The array is split into batches of five records and throttled to one message every two seconds.
NGSI Builder and Debug
The builder in smart mode converts each batch into AirQualityObserved entities; a Debug node shows the result.
Show example flow as JSON
[
{"id": "tab1", "type": "tab", "label": "DataOps", "disabled": false, "info": ""},
{"id": "inject1", "type": "inject", "z": "tab1", "name": "", "props": [{"p": "payload"}], "repeat": "", "once": false, "payload": "", "payloadType": "date", "x": 200, "y": 160, "wires": [["func1"]]},
{"id": "func1", "type": "function", "z": "tab1", "name": "Air quality", "func": "msg.payload = [\n {\"id\": \"urn:ngsi-ld:AirQualityObserved:exampledistrict:2026-09-30\", \"type\": \"AirQualityObserved\", \"dateObserved\": \"2026-09-30T00:00:00Z\", \"location\": {\"type\": \"Point\", \"coordinates\": [7.3322, 50.3315]}, \"areaServed\": \"Example District\", \"temperature\": 9.8, \"no2\": 0.000067, \"o3\": 0.1239}\n];\nreturn msg;", "outputs": 1, "x": 390, "y": 160, "wires": [["split1"]]},
{"id": "split1", "type": "split", "z": "tab1", "name": "", "splt": "\\n", "spltType": "str", "arraySplt": "5", "arraySpltType": "len", "stream": false, "property": "payload", "x": 610, "y": 160, "wires": [["delay1"]]},
{"id": "delay1", "type": "delay", "z": "tab1", "name": "", "pauseType": "rate", "timeout": "5", "timeoutUnits": "seconds", "rate": "1", "nbRateUnits": "2", "rateUnits": "second", "drop": false, "x": 180, "y": 280, "wires": [["builder1"]]},
{"id": "builder1", "type": "ngsi-builder", "z": "tab1", "name": "", "contextUrl": "https://context.${DOMAIN}/main-context", "entityType": "AirQualityObserved", "mode": "smart", "entityId": "", "addObservedAt": true,
"mappings": [
{"target": "id", "source": "id", "addObservedAt": true, "required": true},
{"target": "type", "source": "type", "addObservedAt": true, "required": true},
{"target": "dateObserved", "source": "dateObserved", "addObservedAt": true, "required": true},
{"target": "location", "source": "location.coordinates", "addObservedAt": true, "required": true},
{"target": "areaServed", "source": "areaServed", "addObservedAt": true, "required": false},
{"target": "temperature", "source": "temperature", "addObservedAt": true, "required": false},
{"target": "no2", "source": "no2", "addObservedAt": true, "required": false},
{"target": "o3", "source": "o3", "addObservedAt": true, "required": false}
],
"customFields": [], "customMappings": [], "overwrites": [], "debugMode": false, "useTransformation": false, "transformationScript": "", "topicFilter": "", "useObservedAtOverwrite": false, "observedAtField": "", "ignoreRequired": false, "enableValidation": true, "invalidExtraFields": false, "invalidMissingOptional": false, "invalidValue": false, "invalidOptionalAction": "skip",
"locationConfig": {"enabled": true, "type": "Point", "posType": "array", "latField": "", "lonField": "", "arrayField": "location.coordinates", "polyField": "", "order": "lonlat"},
"x": 420, "y": 280, "wires": [["debug1"]]},
{"id": "debug1", "type": "debug", "z": "tab1", "name": "Air Quality", "active": true, "tosidebar": true, "complete": "payload", "targetType": "msg", "x": 610, "y": 280, "wires": []}
]
The shipped flow contains several thousand daily records from a satellite service inside the Function node; the listing above is shortened to a single record.
Status and errors
| Indicator | Meaning |
|---|---|
| Green dot "ready" | Configuration is complete; the last message was processed. |
| Yellow ring "skipped (topic)" | msg.topic does not match the topic filter. The message is discarded. |
| Red ring "missing configuration" | Entity type or mapping mode is empty. An error is raised as well. |
| Yellow dot "skipped (missing or invalid mapped fields)" | No item produced a valid entity, for example because of missing required fields or failed validation. No message is sent. |
| Red dot "error" with error "[NGSI-LD Builder] Internal error: …" | Unexpected error while processing the message. |
| Error "Invalid topic filter regex: …" | The regular expression in the topic filter is malformed. |
| Error "[NGSI-LD Builder] Location parsing error: …" | The geodata could not be read; the entity continues without location. |
| Error "[NGSI-LD Builder] Script error: …" | The transformation script threw an exception; the entity continues without the script changes. |
| Error "[NGSI-LD Builder] Fatal processing error: …" | An item could not be processed and is skipped. |
| Warning "[Timestamp Validation] Invalid format in field '…'" | The observedAt source field does not hold an ISO 8601 timestamp; the system time is used. |
| Warning "[Validation] Entity … dropped (invalid): …" | A required schema field is missing; the entity is dropped. |
| Warning "[Validation] Entity … is invalid (Warning): …" | Optional fields are invalid; handling follows the Handle invalid optional fields setting. |
In debug mode the warnings "Processing input", "Result built", "Item is not an object, skipping." and "Dropping item due to missing mandatory fields: …" appear additionally. Errors are reported via node.error and can be handled with a Catch node.
Notes
- Network access while configuring: Context and schema are loaded by the Node-RED server through the admin endpoints
/ngsi-builder/proxyand/ngsi-builder/flush-cache. The server therefore needs access to the context URL and to the schema URLs of the Smart Data Models (often GitHub). Behind a proxy the usual environment variables must be set. - Validation: Without a reachable context or schema no validation takes place at runtime. Context and schema are cached for one hour.
- Timestamps:
observedAtis always emitted in UTC with milliseconds. Without a source field it reflects the processing time, not the measurement time. - Coordinates: Output follows GeoJSON, i.e. longitude before latitude. Set the coordinate order to match your input data.
- Units: The node passes values through unchanged and sets no
unitCode. Convert to metric units (°C, mm, m³/s, km/h, µg/m³) beforehand and add theunitCodein the transformation script if needed. - Transformation script: The script runs with the privileges of the Node-RED process. Restrict access to the editor accordingly.
- Personal data: The node removes no content. If input data contains names, addresses or vehicle registrations, check whether this data may be transferred before publishing it in the context broker.