> For the complete documentation index, see [llms.txt](https://docs.kpnthings.com/kpn-things/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://docs.kpnthings.com/kpn-things/the-portal-explained/things-creator/destination-formats/destination-format-scripts.md).

# Destination Format Scripts

When your IoT devices send data to the platform, it is internally processed and standardized into the SenML (Sensor Measurement Lists) format. By default, when you set up a Destination (such as an HTTP webhook or an MQTT broker) to receive your uplink data, the platform forwards it in this native SenML format.

However, your external applications or third-party systems might require data in a specific structure. Destination Format Scripts allow you to write custom JavaScript code to transform the platform's standard SenML payload into any desired format before it is delivered to your destination.

### How It Works

A Destination Format Script acts as a data transformer at the very end of the data processing pipeline.

```
[IoT Device] ──> [Decoder] ──> [SenML] 
    ──> [Destination Format Script] ──> [Your Custom Format] ──> [Destination]
```

* Input: The script receives a valid SenML JSON array containing the device's latest measurements and metadata.
* Processing: Your custom JavaScript logic parses, filters, or reorganizes this SenML data.
* Output: The script returns the transformed payload (e.g., a custom JSON object, a string, or a binary buffer) tailored to your external system's specifications.

### Script Requirements

To ensure proper execution within the platform, your script must adhere to the following guidelines:

#### 1. JavaScript Environment

The script engine is fully compliant with modern ECMAScript standards (including ES6, ES7, and beyond). You can freely use modern syntax features such as arrow functions, template literals, optional chaining (`?.`), nullish coalescing (`??`), destructuring, `const`/`let`, and advanced collection types like `Set`.&#x20;

The destination format script is executed in a sandbox. As a result you cannot make network requests (e.g., `fetch`) or import external Node.js/third-party modules.

#### 2. The Main Function Entry Point

Your script must expose a single main function execution block that takes a single string parameter (`input`). The function itself does not require a specific name and can be written as an anonymous arrow function.

The underlying `input` argument is a JSON string that, when parsed, reveals an object with the following structural layout:

JavaScript

```javascript
{
    metadata: {
        device: {
            name: "example-device",
            urn: "urn:dev:deveui:0011223344556677"
        },
        ingestion: {
            timestamp: 1683014400000
        }
        // Additional routing metadata properties
    },
    payload: [
        // Array of raw SenML records from the device
    ]
}
```

#### 3. Processing Constraints

To maintain platform stability and performance, the execution environment enforces the following limits:

* Execution Time: Scripts must complete execution within 100 milliseconds.
* Memory Limit: Scripts are allocated a maximum of 10 MB of memory.
* No External Access: For security reasons, scripts run in an isolated sandbox. You cannot make network requests (e.g., `fetch`) or import external Node.js/third-party modules.

### Default Starter Template & Example

When you create a new Destination Format, the Script Editor pre-populates with a robust template. This template contains built-in SenML Utilities to help you handle RFC 8428 data normalization (expanding and merging base fields like `bn`, `bt`, etc.) before mapping it to your custom schema.

#### Template Script

```javascript
input => {
    
    /************************ SENML UTILS ************************/
    const ORDERED_SENML_KEYS = ['bn', 'bt', 'bu', 'bv', 'bs', 'bver', 'n', 'u', 'v', 's', 't', 'ut', 'vb', 'vs', 'vd'];
    const ORDERED_SENML_KEY_SET = new Set(ORDERED_SENML_KEYS);

    const orderRecordAttributes = (record) => {
        const ordered = {};

        ORDERED_SENML_KEYS.forEach((key) => {
            if (Object.prototype.hasOwnProperty.call(record, key)) {
                ordered[key] = record[key];
            }
        });

        Object.keys(record)
            .filter((key) => !ORDERED_SENML_KEY_SET.has(key))
            .sort()
            .forEach((key) => {
                ordered[key] = record[key];
            });

        return ordered;
    };

    /**
     * Expands SenML records to include inherited base fields (RFC 8428).
     * Base fields (bn, bt, bu, bv, bs) are inherited from prior records unless overridden,
     * and the base fields are written onto every output record.
     * Regular fields (n, t, u, v, s) are not overwritten and only come from the record itself.
     * The `bver` field is validated for consistency and propagated to output records.
     * @param {object[]} records - Array of SenML records
     * @returns {object[]} Records with inherited base fields
     * @throws {Error} If records is not an array, if a record has neither a name nor an inherited base name, or if `bver` values are inconsistent
     */
    const expandBaseFields = (records) => {
        if (!Array.isArray(records)) {
            throw new Error('Expected an array of records');
        }

        let base = {
            bn: undefined,
            bt: undefined,
            bu: undefined,
            bv: undefined,
            bs: undefined
        };
        let bver;

        return records.map((record) => {
            base = {
                bn: record.bn ?? base.bn,
                bt: record.bt ?? base.bt,
                bu: record.bu ?? base.bu,
                bv: record.bv ?? base.bv,
                bs: record.bs ?? base.bs
            };

            if (record.bver !== undefined && bver !== undefined && bver !== record.bver) {
                throw new Error(`Inconsistent bver values: ${bver} and ${record.bver}`);
            }

            if (record.bver !== undefined) {
                bver = record.bver;
            }

            const expanded = { ...record };

            // Validate that record has a name or base name
            if (record.n === undefined && base.bn === undefined) {
                throw new Error('Encountered a record without a name and no base name');
            }

            // Always include inherited base fields in output
            if (base.bn !== undefined) {
                expanded.bn = base.bn;
            }
            if (base.bt !== undefined) {
                expanded.bt = base.bt;
            }
            if (base.bu !== undefined) {
                expanded.bu = base.bu;
            }
            if (base.bv !== undefined) {
                expanded.bv = base.bv;
            }
            if (base.bs !== undefined) {
                expanded.bs = base.bs;
            }

            if (bver !== undefined) {
                expanded.bver = bver;
            }

            return orderRecordAttributes(expanded);
        });
    };

    /**
     * Merges base fields into regular fields and removes specified base fields.
     * Takes records with expanded base fields and selectively removes base fields based
     * on the baseFields parameter. When removing base fields, applies their values to
     * the corresponding regular fields (bn->n, bt->t, bu->u, bv->v, bs->s).
     * @param {object[]} records - Array of expanded SenML records
     * @param {string[]} baseFields - Base fields to remove (default: ['bn', 'bt', 'bu', 'bv', 'bs'])
     * @returns {object[]} Records with specified base fields removed and values merged
     * @throws {Error} If records is not an array or if baseFields is not an array
     */
    const mergeBaseFields = (records, baseFields = ['bn', 'bt', 'bu', 'bv', 'bs']) => {
        if (!Array.isArray(records)) {
            throw new Error('Expected an array of records');
        }
        if (!Array.isArray(baseFields)) {
            throw new Error('Expected baseFields to be an array');
        }

        const shouldRemove = (field) => baseFields.includes(field);

        return records.map((record) => {
            const merged = { ...record };

            if (shouldRemove('bn')) {
                // Apply base name to regular name before removing
                if (record.n !== undefined && record.bn !== undefined) {
                    merged.n = `${record.bn}${record.n}`;
                } else if (record.n === undefined && record.bn !== undefined) {
                    merged.n = record.bn;
                }
                delete merged.bn;
            }
            if (shouldRemove('bt')) {
                // Apply base time to regular time before removing
                if (record.t !== undefined && record.bt !== undefined) {
                    merged.t = record.t + record.bt;
                } else if (record.t === undefined && record.bt !== undefined) {
                    merged.t = record.bt;
                }
                delete merged.bt;
            }
            if (shouldRemove('bu')) {
                // Apply base unit to regular unit before removing
                if (record.u === undefined && record.bu !== undefined) {
                    merged.u = record.bu;
                }
                delete merged.bu;
            }
            if (shouldRemove('bv')) {
                // Apply base value to regular value before removing
                if (record.v !== undefined && record.bv !== undefined) {
                    merged.v = record.v + record.bv;
                }
                delete merged.bv;
            }
            if (shouldRemove('bs')) {
                // Apply base sum to regular sum before removing
                if (record.s !== undefined && record.bs !== undefined) {
                    merged.s = record.s + record.bs;
                }
                delete merged.bs;
            }

            return orderRecordAttributes(merged);
        });
    };

    /**
     * Normalizes an array of SenML records by expanding and merging base fields (RFC 8428).
     * Combines expandBaseFields and mergeBaseFields: first expands all base fields across
     * records, then selectively merges and removes base fields based on the mergeBaseFields parameter.
     * @param {{records: object[], baseFields?: string[]}} input - Config object
     * @returns {object[]} Normalized records with base fields selectively applied and removed
     * @throws {Error} If records is not an array, if a record has neither a name nor an inherited base name, or if `bver` values are inconsistent
     */
    const normalizeSenmlRecords = ({ records, baseFieldsToMerge = ['bn', 'bt', 'bu', 'bv', 'bs'] }) => {
        const expanded = expandBaseFields(records);
        return mergeBaseFields(expanded, baseFieldsToMerge);
    };
    /************************ END OF SENML UTILS ************************/


    /************************ YOUR SCRIPT ************************/
    const { payload, metadata } = JSON.parse(input);

    /**
     * In this example the SenML is normalized in the following way:
     * 1) all SenML records are expanded with the inherited SenML base name field (if present).
     * 2) the SenML base fields for time, unit, value and sum are merged into the regular SenML Fields.
     * 3) the attributes in each record is returned in canonical SenML key order
     * * Note: if you want to merge the base name field into the regular name fields, you just add 'bn' to the
     * baseFieldsToMerge array (or completely leave out the baseFieldsToMerge parameter, which defaults 
     * to merging all base fields).
     * * The normalized SenML data can be used as the basis for your own transformation.
     */
    const normalizedSenml = normalizeSenmlRecords({ records: payload, baseFieldsToMerge: ['bt', 'bu', 'bv', 'bs'] });

    const output = {
        device: {
            name: metadata?.device?.name,
            urn: metadata?.device?.urn
        },
        ingestion:{
            timestamp: metadata?.ingestion?.timestamp,
        },
        measurements: normalizedSenml
    };
    
    return JSON.stringify(output);
    /************************ END OF YOUR SCRIPT ************************/

}
```

### Testing Your Script

Before saving your script, verify its execution using the built-in Script Editor Test Tool:

1. Paste your JavaScript code into the editor workspace.
2. Use one of the pre-defined SenML Payload Data inputs in the Tester, or provide a sample JSON test string containing a mock `metadata` object and `payload` array into the Test Input field.
3. Click Run script with test payload.
4. Review the generated Output block to ensure the data structure matches your target destination parameters, or review the Console Output Logs if a runtime exception is thrown.


---

# Agent Instructions
This documentation is published with GitBook. GitBook is the documentation platform designed so that both humans and AI agents can read, navigate, and reason over technical content effectively. Learn more at gitbook.com.

## Querying This Documentation
If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter, and the optional `goal` query parameter:

```
GET https://docs.kpnthings.com/kpn-things/the-portal-explained/things-creator/destination-formats/destination-format-scripts.md?ask=<question>&goal=<endgoal>
```

`ask` is the immediate question: it should be specific, self-contained, and written in natural language.
`goal` is optional and describes the broader end goal you are ultimately trying to accomplish on behalf of the user. GitBook uses it to tailor the answer towards what is most useful for that goal.

The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
