Skip to main content

Data Transforms

Data Transforms let NeoMind automatically process telemetry data after device data is written to Telemetry (triggered by the write event, millisecond-scale) — using a JavaScript function to convert raw metrics into derived metrics. For example:

  • Celsius → Fahrenheit
  • Raw voltage + current → computed power
  • Device online state → human-readable status text
  • Invoke extension commands to process data (e.g. YOLO detection → extract confidence)

Derived metrics from transforms can be used just like regular device metrics in dashboards, rules, and AI Agents.

Rules vs Transforms​

DimensionRulesTransforms
PurposeCondition evaluation → execute actionsData processing → generate new metrics
OutputNotifications / commands / Agent callsNew telemetry metrics (bindable to dashboards/rules)
LogicJSON conditions + actionsJavaScript code
TimingFires when condition is metReal-time transform per data point

Interface Overview​

Switch to the Transforms tab in the Automation page:

Data transforms page — transform list, scope, code summary, enabled status

The page displays all transforms in a table, each row containing:

ColumnDescription
NameTransform display name
ScopeGlobal / Device Type / Device
CreatedWhen the transform was created
Last ExecutedWhen it last ran
Status ToggleEnable / disable switch
Actions MenuEdit, export, delete

The Import / Export button in the top right lets you bulk import/export transform JSON.

Creating a Transform via Web UI​

Step 1: Open the Transform Builder​

In the Transforms tab, click the Create button to open the full-screen builder:

Transform builder — left config rail (name, scope, output prefix), right code workspace

The builder uses a split-pane layout:

AreaDescription
Left · Config RailName, description, scope, output prefix, complexity
Right · Code WorkspaceJavaScript code editor + variables panel + test strip

Step 2: Fill in Basic Info​

FieldDescription
NameTransform display name
DescriptionOptional, explains the transform's purpose
Output PrefixNaming prefix for derived metrics. If set to converted, output metrics are named converted.temp_f
ComplexityNumber 1–5, used for execution ordering (lower complexity executes first)

Step 3: Select Scope​

Scope determines which devices' data the transform processes:

ScopeDescriptionUse Case
GlobalProcesses all devices' dataUniversal transforms (e.g. unit conversion)
Device TypeOnly processes data from a specified device typeBatch transforms for similar devices
DeviceOnly processes a single device's dataCustom transforms for a specific device

Step 4: Write the Transform Code​

Transform builder — JavaScript code editor with variables panel

Write the transform function in JavaScript in the code editor. The input variable holds the input value — single-key metric objects (e.g. {"temperature": 25}) are auto-unwrapped to the scalar so it can be used directly; access the full input object via input_raw. return an object as output:

// Celsius to Fahrenheit
return {
temp_f: input * 9/5 + 32
}

Available variables:

VariableDescription
inputThe input data — single-key metric objects (e.g. {"temperature": 25}) are auto-unwrapped to the scalar so it can participate in arithmetic directly
input_rawThe full input data object (no auto-unwrap)
extensions.invoke(ext_id, command, params)Invoke an extension command; returns the extension's result

input auto-unwrap rules — know exactly what your code receives before writing it; this is the single biggest factor in whether a transform "just works" or errors out:

Device data shapeValue of inputValue of input_rawHow to write code
Scalar (e.g. 25)2525input * 9/5 + 32
Single-key object {"temperature": 25}25 (auto-unwrapped){"temperature": 25}Use input * 9/5 + 32 directly; use input_raw.temperature when you need the key name
Multi-key object {"temperature": 25, "humidity": 60}The object as-isSame as inputinput.temperature * 9/5 + 32
Single-key vs multi-key — code is not interchangeable

The same input * 9/5 + 32 works with {"temperature": 25} (single key, auto-unwrapped) but yields NaN with {"temperature": 25, "humidity": 60} (multi-key). If a transform must be reused across device types with uncertain input shapes, write defensively: return { temp_f: (input_raw.temperature ?? input) * 9/5 + 32 }. Always verify both shapes with the test strip before saving.

Variables panel: The left panel lets you insert device metrics and extension data sources. After selecting a device type, all its metrics are listed — click to insert into code. You can also select extension commands from the extension panel to generate invocation code.

Step 5: Test the Transform​

The test strip at the bottom of the builder lets you validate the transform with mock data:

  1. Enter a mock value in the test input (e.g. 25)
  2. Click Test
  3. Check the output result

After testing, click Save to save the transform.

How Transforms Work​

Derived metrics are registered with the DataSourceId format transform:<transform_id>:<prefix>.<field>, e.g. transform:9a1b2c3d…:converted.temp_f, and appear grouped under the Transform type in data source pickers. These metrics can be:

  • Bound as data sources in dashboards
  • Referenced in rule conditions
  • Bound in Agent Focused mode

Transform Examples​

1. Celsius to Fahrenheit​

return {
temp_f: input * 9 / 5 + 32
}

Output metric: converted.temp_f

2. Compute Power (Voltage × Current)​

// Assume input contains voltage and current
return {
power: input.voltage * input.current,
power_kw: (input.voltage * input.current) / 1000
}

3. Device Status Text​

return {
status_text: input === 1 ? "Online" : "Offline",
is_online: input === 1
}

4. Invoke Extension to Process Image​

// Call YOLO extension for object detection
const result = extensions.invoke('yolo-video', 'detect', {
data: input
})

return {
detections: result.detections,
object_count: result.detections.length,
has_person: result.detections.some(d => d.class === 'person')
}

5. Call an Extension for External Data (extensions.invoke)​

extensions.invoke(extension_id, command, params) is not limited to images — any command of any installed extension can be called. For example, use a weather extension to add outdoor context to a temperature reading:

// Fetch current weather (extension ID and command name per the Extensions page)
const weather = extensions.invoke('weather.ext', 'get_current', { location: 'Beijing' })

return {
temp_f: input * 9/5 + 32,
outdoor_temp: weather.temp_f || 0
}
Execution mechanism

Before running your code, the transform engine scans it for extensions.invoke(...) calls, executes those extension commands asynchronously first, then injects the results into the code context — so the syntax above reads values synchronously, no await needed. Missing extensions or failed invocations are recorded in the execution record's warnings (see output.warning_count in step 3 of the complete lifecycle example).

6. Numeric Range Classification​

let level = 'normal'
if (input > 80) level = 'critical'
else if (input > 60) level = 'warning'
else if (input > 40) level = 'notice'

return {
level: level,
level_value: { normal: 0, notice: 1, warning: 2, critical: 3 }[level]
}

Complete Lifecycle Example: From Creation to Dashboard​

Here is the full journey in one real scenario: a device reports Celsius; we derive a Fahrenheit metric and put it on a dashboard.

Step 1 · Create the transform via CLI

# (Optional) validate the logic first — nothing is persisted
neomind transform test-code \
--code 'return { temp_f: input * 9/5 + 32 }' \
--input '{"temperature": 25}'

# Create and enable
neomind transform create \
--name "Fahrenheit Converter" \
--scope global \
--code 'return { temp_f: input * 9/5 + 32 }' \
--output-prefix converted \
--enabled true

Once created, neomind transform list shows it (with ID, scope, output prefix). --scope accepts global (all devices), device_type:TH Sensor (a type), or device:sensor-01 (a single device).

Step 2 · Device data arrives; the transform runs automatically

Nothing to trigger manually — when sensor-01 publishes {"temperature": 25}, the transform engine matches the scope, executes the code within milliseconds, and writes the derived metric converted.temp_f = 77 into the time-series store. The derived metric's full identity is transform:<transform_id>:converted.temp_f.

Step 3 · Confirm the execution

neomind transform executions <transform_id> --limit 20

A real execution record looks like this (status: "completed" means success):

{
"id": "7c44cb8f-…",
"automation_id": "f010c73c-…",
"automation_type": "transform",
"started_at": 1788930297329,
"ended_at": 1788930297338,
"status": "completed",
"error": null,
"output": { "metric_count": 1, "warning_count": 0 }
}

To see the metric values themselves, feed a test data point through the transform and inspect the output:

curl -X POST http://localhost:9375/api/automations/transforms/<transform_id>/test \
-H "Authorization: Bearer <JWT>" -H "Content-Type: application/json" \
-d '{ "device_id": "sensor-01", "data": {"temperature": 25} }'

The metrics array in the response is exactly what the transform produces (identical to what gets written to the time-series store when real data arrives):

{
"success": true,
"data": {
"transform_id": "f010c73c-…",
"metrics": [{
"device_id": "sensor-01",
"transform_id": "f010c73c-…",
"metric": "converted.temp_f",
"value": 77.0,
"timestamp": 1788930297,
"quality": 1.0
}],
"count": 1,
"warnings": []
}
}

Step 4 · Bind it on a dashboard

Open the dashboard editor, add a widget (Chart / Value) → find the Transform group in the data source picker → select converted.temp_f (full ID like transform:f010c73c-…:converted.temp_f) → save. From then on, every incoming device data point updates the derived metric on the dashboard in real time. Rule conditions can reference the same metric too (e.g. converted.temp_f > 170 triggers an alert).

CLI Management​

# Create a transform (Celsius → Fahrenheit; access the input value via `input` in JS)
neomind transform create \
--name "Fahrenheit Converter" \
--scope global \
--code 'return { temp_f: input * 9/5 + 32 }' \
--output-prefix converted \
--enabled true

# List all transforms / view details
neomind transform list
neomind transform get <id>

# View recent executions (first stop when "the code ran but produced no output")
neomind transform executions <id> --limit 20

# Enable / disable
neomind transform enable <id>
neomind transform disable <id>

# Delete
neomind transform delete <id>

REST API​

# Create transform
curl -X POST http://localhost:9375/api/automations \
-H "Content-Type: application/json" \
-d '{
"name": "Fahrenheit Converter",
"description": "Convert Celsius to Fahrenheit",
"type": "transform",
"enabled": true,
"definition": {
"scope": "global",
"js_code": "return { temp_f: input * 9/5 + 32 }",
"output_prefix": "converted",
"complexity": 2
}
}'

# List all transforms
curl http://localhost:9375/api/automations

# Update transform
curl -X PUT http://localhost:9375/api/automations/<id> \
-H "Content-Type: application/json" \
-d '{"name": "Updated Name", "definition": {"scope": "global", "js_code": "...", "output_prefix": "converted", "complexity": 2}}'

Import / Export​

The Import / Export button in the Transforms tab supports bulk management. You can also select Export from an individual transform's actions menu to export a single transform.

Export file format: neomind-transforms-YYYY-MM-DD.json.

Integration with Other Modules​

ModuleDescription
DashboardsTransform output metrics can be bound as dashboard widget data sources
Automation RulesRule conditions can reference transform output transform:<prefix>:<field> metrics
AI AgentAgent Focused mode can bind transform output metrics
DevicesTransforms process raw telemetry published by devices
ExtensionsTransform code can call extensions.invoke() to execute extension commands

Best Practices​

  • Use Output Prefixes wisely: Set different prefixes for different transforms (e.g. converted, status, aggregated) to avoid metric name collisions
  • Test before saving: The builder's Test feature quickly validates code logic without waiting for real data
  • Complexity ordering: Transforms that depend on other transforms' output should have higher complexity to ensure correct execution order
  • Minimize scope: Use Device Type instead of Global when possible to reduce unnecessary transform overhead
  • Keep code lightweight: Transforms execute on every data point — keep code simple (avoid complex loops/recursion)

Mobile​

Data transforms on mobile — single-column table layout

On mobile, the interface switches to a single-column layout supporting list viewing and status toggling. Edit transforms on desktop (the code editor needs screen space).

Next Steps​

  • Automation Rules — Reference transform-derived metrics in rule conditions
  • Use Dashboards — Bind derived metrics as dashboard widget data sources
  • Extensions — Call extension commands from transforms via extensions.invoke()

Last updated: 2026-09-09