# SCADA Without the Symbol Library

> Build a SCADA-style HMI on TagoIO with no symbol library: one Custom Widget that TagoAI writes from a prompt, with live data, confirmed commands, and alarms.

![SCADA Without the Symbol Library](https://tago.io/og/blog/scada-without-the-symbol-library.png)

The screen below is a wastewater lift station: a wet well, two pumps, a discharge valve, live flow and pressure, operator controls with a confirmation step, an alarm list, and a level trend. It is one Custom Widget on a TagoIO dashboard. [TagoAI](https://tago.io/ai) wrote it from a written description, and the whole build, including the simulator that drives it, took one afternoon.

![Pump Station PS-01, a SCADA-style HMI rendered by one TagoIO Custom Widget: the wet well at 69.3% with its setpoint and alarm lines, pump P-101 running and P-102 stopped, valve V-101 open, the operator controls, the alarm list, and a four-hour level trend.](https://tago.io/images/blog/scada-without-the-symbol-library/pump-station-hmi.webp)

This post covers what is on the screen, how data and commands move through the platform, and how you build the same thing on your own account. The first prompt we gave TagoAI is included, so you can start from it and swap in your own equipment.

## Why TagoIO has no symbol library

SCADA screens have been drawn the same way for decades: a library of symbols (tanks, pumps, valves), a canvas with fixed coordinates, and a binding dialog per symbol. The library sets the ceiling. Every piece of equipment the vendor did not draw becomes a development task, usually SVG editing plus a scripting layer that only that tool understands.

TagoIO never shipped a symbol library, and we are not going to. A Custom Widget can be a React component that the platform bundles and hosts, with hooks that deliver live device data and write commands back. Since July 2026, TagoAI writes those components from a prompt in its chat panel on the dashboard. A P&ID becomes a description: "a wet well with two pumps, a discharge valve, and a pressure transmitter on the header". The odd piece of equipment your customer runs becomes one more sentence. The diagram scales itself because it is SVG inside a single widget, so there is no fixed-column grid to manage.

The trade is plain. You do not get 100 symbols to drag around. You get a screen drawn for your process, in the style your operators expect, that you change by describing the change.

## What is on the screen

The diagram follows high-performance HMI practice: a neutral background, gray equipment, and color reserved for state. Cyan means liquid, flow, an open valve, or a running motor. Amber and red are kept for alarms, alarm limits, and warnings, so a problem stands out from everything else on the screen.

**Wet well.** The level fills the tank with a small wave at the surface. Dashed lines mark the setpoint (SP) and the high and low alarm limits (HH, LL). The large number is the level transmitter LT-101.

**Pumps P-101 and P-102.** The impeller spins while the pump runs and the label shows motor current. A fault turns the pump red with a pulsing ring, and the Start and Stop buttons are replaced by Reset fault.

**Valve V-101 and the header.** A running pump animates its own pipes, and the header animates only while the valve is open. Stop both pumps and the header goes quiet. PT-101 shows discharge pressure, FT-102 shows outflow, and a pressure above 5.5 bar turns the instrument amber.

**Operator controls.** Start and Stop per pump, Open and Close for the valve, and a setpoint stepper. Every command asks for confirmation, and a pump or valve command then shows PENDING until the field reports the new state or 20 seconds pass. Equipment commands are locked while the station is in AUTO; the setpoint stays editable.

**Alarms and trend.** Active alarms are derived from live values: level above HH, level below LL, a pump running against a closed valve, a drive fault, or no data for 30 minutes. Below them sits the alarm history with acknowledge state, and an Acknowledge button in the panel header acknowledges every entry at once. The trend shows the last four hours of level against the same SP, HH and LL references.

**Header.** The AUTO/MANUAL switch, a live indicator with the age of the newest record the widget has received, and an alarm count that blinks while a high-severity alarm is active.

## How it works

Everything on the screen is a device variable, and every command is a device record. The widget never trusts its own click on a pump or valve: it marks the equipment PENDING and waits for the state to come back from the field.

![The HMI data path: the PLC or RTU reports state through a gateway to the Pump Station 01 device and the widget reads it live; an operator command becomes a device record that runs the Pump Station Controller Analysis, which sends the field command back to the PLC.](https://tago.io/images/blog/scada-without-the-symbol-library/data-path-diagram.svg)

The device Pump Station 01 holds every value on the screen: `wet_well_level` with its setpoint and limits, `inflow`, `outflow`, `discharge_pressure`, each pump's state, current, and runtime, `valve_state`, `station_mode`, and the alarm events and acknowledgments. The widget reads those variables, as bound in its data sources, with the `useRealtimeData` hook from the TagoIO Custom Widget SDK. It receives the latest stored records on load, up to the record limit set on the widget, and then every new record as it arrives, so the trend, the active alarms and the pending markers are all computed from one feed.

Commands go the other way. When the operator confirms Start P-102, the widget calls `sendData` from the `useSendData` hook with `{ variable: "pump_2_cmd", value: "start" }`. The dashboard writes that record to the device and, because the widget is configured to run an Analysis when it sends data, runs Pump Station Controller with the record as scope. The Analysis is where the real field command lives: an MQTT publish, a LoRaWAN downlink, a [TagoTiP](https://tago.io/tip) command over MQTT, or an HTTP call to a gateway. In the demo it also simulates the process response, so the level moves and the pumps change state without hardware.

Alarms use the same records. Each time the Analysis runs, it writes an alarm event for any condition that became true since its last run; the stale-data alarm stays in the widget. The widget derives the active list from live values and marks history entries acknowledged when an `alarm_ack` record is newer than the event.

## Build it yourself

You need a TagoIO account with TagoAI enabled on the profile (Profile Settings, Services, AI Provider) and about an hour. No hardware is required for the first pass; the Analysis simulates the station.

1. **Create the device.** Use Custom HTTPS for a simulated station: it needs no serial number, and its parser leaves your records as sent. Pick mutable storage so you can edit values by hand later. Send a first set of records with the variable names the build uses: `wet_well_level`, `level_setpoint`, `level_high_alarm`, `level_low_alarm`, `inflow`, `outflow`, `discharge_pressure`, `pump_1_state`, `pump_1_speed`, `pump_1_current`, `pump_1_runtime`, the same four for pump 2, `valve_state`, `station_mode`, `alarm`, `alarm_ack`, and `alarm_state`. A few dozen `wet_well_level` points spread over the last hours give the trend something to draw.
2. **Create a Normal dashboard and open the TagoAI panel.** Paste the prompt below, name your device, and send it. TagoAI creates the Custom Widget, writes the code, and sets its data sources. Refine by chat: "make the pumps larger", "move the alarms above the trend", "use our tag names".
3. **Check the bindings.** Open the widget settings and confirm the data sources list every variable the screen reads and every command it writes (`pump_1_cmd`, `pump_2_cmd`, `pump_1_reset`, `pump_2_reset`, `valve_cmd`, `level_setpoint`, `station_mode`, `alarm_ack`), and that they load enough records to fill the four-hour trend. A variable that is not bound is dropped silently when the widget sends it.
4. **Create the controller Analysis.** Deno or Node runtime, with the device token as an environment variable. Read the last value of each state variable, apply the commands that arrived in the scope, send the field command, and write the new state back. Set this Analysis in the widget's "Run analysis when sending data" option. For a demo, add a simulation step so the level responds to the pumps.
5. **Add an Input Control widget under the HMI** with text fields for the process variables. Typing `fault` into `pump_1_state` or `90` into `wet_well_level` is the fastest way to test the equipment and alarm states on the screen.
6. **Size the widget.** Full width and about 800 px high, with the background set to transparent, Disable shadow on, and Header visibility set to Show only buttons. A header button that runs the Analysis with an empty scope is a convenient "advance simulation" control.

The prompt we started from, ready to adapt:

```text
Build a SCADA-style HMI for a wastewater lift station as one full-canvas widget for the device Pump Station 01. Dark high-performance HMI style: neutral background, gray equipment, color only for state and alarms.

Draw an SVG process diagram: a wet well showing wet_well_level as a filled level with a setpoint line (level_setpoint) and high/low alarm lines (level_high_alarm, level_low_alarm); an inlet pipe with inflow; two centrifugal pumps P-101 and P-102 driven by pump_1_state and pump_2_state (running, stopped, fault) with spinning impellers while running and pump_1_current / pump_2_current labels; a discharge header with valve V-101 (valve_state open or closed), pressure transmitter PT-101 (discharge_pressure) and outflow meter FT-102 (outflow). Animate flow only on pipes that carry flow.

Add an operator panel: Start and Stop per pump (send pump_1_cmd or pump_2_cmd = start or stop), Open and Close for the valve (send valve_cmd), a setpoint stepper that sends level_setpoint, and an AUTO/MANUAL switch (send station_mode) that locks equipment commands while in AUTO. Every command needs a confirm step and a PENDING state until the device reports the new value.

Add an alarm panel: active alarms derived from live values (level above high limit, level below low limit, pump running while the valve is closed, pump fault, no data for 30 minutes) plus the history of the alarm variable with acknowledge state; an Acknowledge button sends alarm_ack. Add a KPI strip (inflow, outflow, pressure, motor currents, run hours) and a four-hour level trend with the same setpoint and limit lines.
```

TagoAI output varies between runs, and prompt precision matters. Ask for one change at a time and keep the tag names consistent with the device; most misses come from a variable named one way in the prompt and another way on the device.

## From demo to plant

**Replace the simulator.** Keep the Analysis, delete the simulation step and the state it writes back, and point the field command at your gateway: an MQTT topic the PLC subscribes to, a LoRaWAN downlink, or a TagoTiP command over MQTT. The device then gets its state from the PLC and the screen does not change. Create the production device with Hybrid storage: a mutable device holds 50,000 records, while a Hybrid one sends readings to an immutable side and keeps commands and setpoints editable.

**Serve a fleet with one screen.** Build the same widget on a [Blueprint dashboard](https://docs.tago.io/docs/tagoio/dashboards/blueprint-dashboard). The widget binds to a Blueprint device and resolves to whichever station the user selects, so one HMI covers every lift station in the utility. Show the selected station in the header, and have the Analysis act on the device each command came from, not a fixed one.

**Give operators the screen in TagoRUN.** Custom Widgets work in [TagoRUN](https://tago.io/run) portals. Ship the HMI in your branded application, with Access Management deciding which users see which stations. Every command an operator sends is a stored device record with a timestamp, so the command history is part of the station's own data.

**Alert outside the screen.** Run the Analysis from an Action on incoming telemetry too, so alarm records get written when nobody is clicking. An Action on the `alarm` variable then sends a push notification, a webhook, or email and SMS through your SMTP, SendGrid, or Twilio account the moment a new alarm record is written. A scheduled Action can also run the Analysis to escalate an alarm that stays unacknowledged.

**Keep the screen honest.** The widget shows the age of the newest record it has received and raises a stale-data alarm after 30 minutes without records. On a real station, count only field telemetry toward that age; an HMI that looks live while the link is down is worse than a blank one.

## Draw your process

Open a Normal or Blueprint dashboard, click the TagoAI icon, paste the prompt, and replace our tags with yours. A tank farm, a chiller plant, a compressor skid, or a packaging line follows the same pattern: variables in, commands out through an Analysis, and a screen described in plain language.

Read the [Custom Widget overview](https://docs.tago.io/docs/tagoio/widgets/custom-widget/), the [SCADA-style HMI guide](https://docs.tago.io/docs/tagoio/widgets/custom-widget/building-scada-style-hmis), and the [TagoAI documentation](https://docs.tago.io/docs/tagoio/tago-ai/) for the details, and post the screen you build in the [TagoIO Community](https://community.tago.io). We want to see it.

[llms.txt](https://tago.io/llms.txt)
