Mastering the Smart Function Block in Analytics Builder: 3 Real-World Recipes

You’ve heard the news: the Smart Function block brings standard JavaScript directly into Analytics Builder. But how does it actually change your daily model-building workflow?

In this companion tutorial, we are moving from concept to code. We will explore three real-world scenarios where replacing standard visual blocks with a few lines of JavaScript can drastically improve your model’s hygiene, readability, and maintainability.

:hammer_and_wrench: 1. The Quick Fix (String Manipulation)

Extracting a machine ID from a complex string like “FactoryA-Machine5-001”.

  • Before: A chain of 3 expression blocks to 1) find the first occurrence of ‘-’ in the complex string, 2) find the second occurrence of '-'in the complex string starting from the first occurrence and 3) substring of the complex string between first and second occurrence with some quite

  • With Smart Function: a simple 1-line function

String Manipulation Model (Example)

The screenshot shown below illustrates the difference at the model-level, with at the top the “before”-situation and below the “new way”. It might not look that much different, but the real big difference is in the reduced complexity of the string manipulation statements (and the resulting reduction in debugging effort to ensure it works as expected)

  • Expression Block1: input1.find(“-”).toString()
  • Expression Block2: input1.findFrom(“-”,input2.toInteger()+“1”.toInteger()).toString()
  • Expression Block3: input1.substring(input2.toInteger()+“1”.toInteger(),input3.toInteger())

String Manipulation Smart Function (Example)

return [ inputs[0].value.split('-')[1] ];

:shuffle_tracks_button: 2. The Smart Router (Conditional Logic)

Need to route an event based on a status code?

  • Before: A massive chain of “Expression” blocks that clutter your screen. (although the grouping capability of Analytics Builder comes in handy to bring some order to the chaos)
  • With Smart Function: A simple JavaScript switch statement that dynamically routes signals to the correct output port.

Routing Model (Example):

The example below demonstrates a use-case for monitoring a fleet of solar inverters: when an inverter reports an issue, it sends a raw integer error code. You need to route these to the correct maintenance team.

The screenshot shown below illustrates the difference at the model-level, with on the left the “before”-situation and on the right the “new way”. You can easily spot the collapsing / condensing in terms of number of blocks, which makes it look much more insightful / readable, but the real big difference is in the reduction of complexity and spreading out of logic over Expression blocks and Text Substitution blocks: from 8 separate expressions to a single logic in JavaScript.

Routing Smart Function (Example):

const event = inputs[0];
const code = event.properties["text"];
const device = event.properties["source.name"];

  // Define our output array structure and initialize to null:
  let outputs = [null, null, null, null, null];


  // The Logic Switch
  switch (true) {

    // Case A: Network Issues (100-199) -> Trigger IT Ticket (Port 1)
    case (code >= 100 && code <= 199):
      outputs[0] = `Warning: Connectivity lost on ${device} (Code ${code})`;
      break;

    // Case B: Grid Fluctuation (200-299) -> Trigger DB Log Only (Port 2)
    case (code >= 200 && code <= 299):
      outputs[1] = `Info: Voltage fluctuation recorded (Code ${code})`;
      break;

    // Case C: OVERHEATING (301) -> Trigger Emergency Stop (Port 3)
    case (code === 301):
      outputs[2] = `CRITICAL: ${device} OVERHEATING! SHUTTING DOWN.`;
      break;

    // Case D: Component Failure (4041) -> Trigger Field Ops Dispatch (Port 4)
    case (code === 4041):
      outputs[3] = `Alert: Hardware failure on ${device}. Tech required.`;
      break;

    // Case E: Firmware (5000+) -> Trigger Updater (Port 5)
    case (code >= 5000):
      outputs[4] = `Maintenance: Firmware update available for ${device}.`;
      break;
  }

  return outputs;

}

:test_tube: 3. The ML Wrapper (Data Normalization)

Connecting to a Machine Learning model often requires precise data formatting: Incoming data usually needs to be preprocessed so that it matches the expected input format of the model, and the model output needs to be post-processed to convert raw scores into meaningful, actionable insights.

  • Before: 3 blocks to normalize data (0-100 → 0.0-1.0) and 3 blocks to interpret the result.
  • With Smart Function: One block to normalize inputs, pass them to your ML model. One block to interpret the model output into a clear boolean alert.

ML Wrapper Model (Example):

The example shown below demonstrates a simple use case that reads vibration data from a connected device, preprocesses it, sends it to a vibration anomaly detection model, and visualizes the results as graphs and events.

Previously, building such a workflow required multiple Analytics Builder blocks to handle data preprocessing (for example, normalization and conversion into the required input format) and multiple blocks to interpret the model output, apply thresholds, and generate anomaly indicators and events. This resulted in more complex and harder-to-maintain models.

With the Smart Function block, both preprocessing and post-processing logic can now be implemented in a clean and compact way. This makes the workflow easier to understand, maintain, and adapt. It also provides greater flexibility, as different machine learning models often require different preprocessing and post-processing steps.

ML Pre-processing Smart Function (Example):

The following Smart Function performs min-max normalization of incoming vibration signals and prepares the data in the format expected by the ML model.

// Min-Max normalization ranges (matching with training)

const RANGES = {
  vx: [-3.0, 3.0],
  vy: [-3.0, 3.0],
  vz: [-3.0, 3.0]
};


function clamp(x, lo, hi) {
  if (x < lo) return lo;
  if (x > hi) return hi;
  return x;
}


function minmax(x, lo, hi) {
  if (hi === lo) return 0.0;
  x = clamp(x, lo, hi);
  return (x - lo) / (hi - lo); 
}


export function onInput(inputs, context) {
  const vx = Number(inputs[0]?.value);
  const vy = Number(inputs[1]?.value);
  const vz = Number(inputs[2]?.value);
     if (!Number.isFinite(vx) || !Number.isFinite(vy) || !Number.isFinite(vz)) return [];
  const x = minmax(vx, RANGES.vx[0], RANGES.vx[1]);
  const y = minmax(vy, RANGES.vy[0], RANGES.vy[1]);
  const z = minmax(vz, RANGES.vz[0], RANGES.vz[1]);

  const payload = JSON.stringify([[x, y, z]]);

  return [payload];

}

In this step, raw device measurements are normalized and packaged into a structured payload before being sent to the ML execution stage.

ML Post-processing Smart Function (example):

The post-processing Smart Function interprets the model score, applies a threshold, generates a human-readable label, and produces both events and measurements.

const THRESHOLD = 0.878; // <-- threshold through model training

export function onInput(inputs, context) {

  const score = Number(inputs[0]?.value);
     if (!Number.isFinite(score)) return [];
  const isAnomaly = score > THRESHOLD;
  const label = isAnomaly ? "ANOMALY" : "OK";
  const flag = isAnomaly ? 1 : 0;
  const trigger = isAnomaly;

  const text = `Vibration anomaly (minmax): ${label} | score=${score.toFixed(3)} | thr=${THRESHOLD.toFixed(3)}`;

  const props = {
    c8y_AI: {
      label,
      anomalyScore: score,
      threshold: THRESHOLD
    }
  };


  return [
    text,     // out0 event text
    trigger,  // out1 event trigger (true only for anomaly)
    props,    // out2 event props
    flag      // out3 measurement flag 0/1
  ];

}

This step converts the raw model output into actionable insights such as anomaly labels, event messages, and visualization signals.

Overall, using Smart Functions significantly reduces the number of required blocks, simplifies model design, and enables preprocessing and post-processing logic to be expressed in a clear, reusable, and maintainable way.

A Personal Tip: Finding the “Sweet Spot”

Because this new block is so flexible, you might be wondering: Should I just write my entire analytics model inside one giant Smart Function and only use the visual canvas to connect the inputs and outputs? Honestly? You absolutely can. If you are a developer who prefers writing code over dragging boxes, it is perfectly valid to use Analytics Builder purely as the connectivity shell that wires your JavaScript logic to the real world.

But as a personal recommendation, I’d suggest aiming for a hybrid approach.

The real magic of a visual canvas is observability. It allows you to see the “story” of your data at a glance, makes visual debugging a breeze, and lets you collaborate easily with domain experts who might not know how to read JavaScript. If you bury 500 lines of code inside a single block, it becomes a black box and you lose some of that collaborative power.

Use the standard visual blocks for your “macro-flow” (the major decision branches, windowing, and high-level routing) and deploy Smart Functions for your “micro-logic” (the messy data normalization, complex math, and string parsing). It gives you the absolute best of both worlds!

2 Likes