Build a Model

Hands-on: design a reusable, data-bound model from scratch in the Model Builder.

In this tutorial you will build a device status indicator from scratch in the Model Builder. By the end you will have a reusable Model that draws a colored circle and a label, takes a target entity as a typed parameter, and recolors itself live whenever that entity's status changes. You will then save it so it can be instantiated many times against different devices.

Looking for full details?

This is a hands-on tutorial. For the complete reference (every field, option, and edge case), see the Model Builder reference.

What you'll need

  • Access to the ControlBird UI from a desktop browser. The Model Builder is desktop-only and opens with a default canvas of 1200×700 px.
  • At least one entity in the store with a Status field you can watch, for example a device with values like Online or Offline.
  • About 15–20 minutes. No prior models are required; you will start from a blank canvas.

Click the canvas before using shortcuts

Keyboard shortcuts such as Ctrl+S and Ctrl+Z are blocked while a text input has focus (the Parameters, Notifications, or callback editors). After typing in a panel, click an empty spot on the canvas first, then use the shortcut.

Step by step

  1. Open the Model Builder from the ControlBird UI, then click New to start a blank model.

    Expected result: an empty canvas with the Shape Palette, Model Library, and Layers panels on the left, and the Properties, Parameters, Notifications, and Validation panels on the right.

  2. Add a circle. From the Shape Palette on the left, click or drag a Circle onto the canvas. With the circle selected, open the Properties panel on the right and set its id to status-circle, set fillColor to #6b7280 (gray) and fillOpacity to 0.8, and set the radius to 20.

    Expected result: a solid gray dot on the canvas. The id is what your callback will reference later, so the exact value matters.

  3. Add a text label. From the Shape Palette, add a Text shape and position it next to the circle. In the Properties panel set its text to ${deviceLabel}. The ${paramName} syntax substitutes a parameter value at instantiation time.

    Expected result: the label currently shows the literal placeholder; it will render the bound value once the deviceLabel parameter exists and is supplied.

  4. Define your parameters. Open the Parameters panel and click Add. Create the two inputs this model needs:

    • name = targetDevice, type = entity, validation required: true: the device this indicator watches.
    • name = deviceLabel, type = string, defaultValue = Device: the text shown beside the dot.

    Expected result: both parameters appear in the panel. Because deviceLabel has a default, the label no longer counts as missing in validation.

  5. Wire up live data with a notification channel. Open the Notifications panel and add a channel. Set its name to statusWatch, set entityExpression to ${targetDevice}, set fieldName to Status, and enable triggerOnChange so the callback fires only when the value actually changes.

    Expected result: a channel named statusWatch that resolves the entity from the targetDevice parameter and observes its Status field.

  6. Write the callback. In the channel's callback editor, look up the circle by the id you set earlier and recolor it based on the incoming value:

    function(notification) {
      const status = notification.current.value.String;
      const circle = this.getShapeById('status-circle');
    
      if (status === 'Online') {
        circle.setFillColor('#10b981');   // green
      } else if (status === 'Offline') {
        circle.setFillColor('#ef4444');   // red
      } else {
        circle.setFillColor('#6b7280');   // gray
      }
    }

    Expected result: when an instance is bound to a live device, the dot turns green, red, or gray as the device's Status changes.

  7. Check the Validation panel. It runs continuously and reports unused parameters, missing parameter definitions, required parameters without defaults, and unresolved entity or model references. Resolve anything it flags. For example, if targetDevice is marked required but has no default, that is expected here because the value is supplied per instance.

    Expected result: a clean (or intentionally understood) Validation panel before you save.

  8. Tidy the layout. Use the canvas controls to fit all shapes to view and toggle grid snapping for clean alignment. If the label sits behind the circle, select it and press Ctrl+] to bring it forward (or Ctrl+[ to send it backward).

  9. Save. Click an empty area of the canvas, then press Ctrl+S. The complete model definition (shapes, parameters, notification channels, and canvas settings) is saved to the Configuration field of a new Model entity.

    Expected result: the model is persisted under /ControlBird/User Designs/Models and now appears in the Model Library for reuse and nesting.

Auto-save only kicks in after the first manual save

Auto-save (every 30 seconds) only triggers once a model has been saved at least once and has a modelId. A brand-new, never-saved model does not auto-save: if you close the tab before your first Ctrl+S, your work is lost. Save early.

Substitution has limits

The ${paramName} syntax only works inside text, html, css, event handlers, and callback expressions. It does not work in numeric properties like radius or fontSize, so set those to concrete values.

Instantiate and reuse it

Your model is now a template. To embed it inside a larger design, open another model and drag this one from the Model Library onto the canvas to create a ModelInstance. Then open the Parameter Binding Editor and supply values for its parameters. For example, bind targetDevice as an expression and set deviceLabel as a constant. Each instance can point at a different device while sharing one design.

Nested models don't inherit channels

A nested ModelInstance inherits its parameter bindings but not the parent model's notification channels. Each model must define its own channels, which is exactly why this indicator carries its own statusWatch channel and works wherever you drop it.

Next steps

You have built and saved a reusable, data-bound model. From here: