# AI Assistant
Source: https://docs.coreflux.org/hub/ai-assistant
Chat with the built-in Coreflux HUB assistant to get insights from live data, build dashboards, and develop LoT Actions using plain language
## Your Broker, Now With a Teammate
The AI Assistant lives inside the same Coreflux MQTT broker that already carries your sensor, PLC, and cloud traffic. You describe what you want in everyday language—**ask for insights** from the data already flowing through your broker, **build Dashboards** with live widgets and KPIs, or **develop LoT (Language of Things) Actions** that run on the broker—and the assistant uses the broker’s own tools to answer, draft, refine, and deploy. It is not a separate chat app pasted on top; it works with your live system.
**Like a colleague who already sits at your control desk** — they can read the same screens you see, suggest the next step, and (when you allow it) wire up the change for you.
## When to Use the AI Assistant
* **Ship integrations faster** — Describe connections or logic you need; the assistant drafts and deploys Routes, Actions, and panels where your permissions allow.
* **Explore without guesswork** — Ask what topics exist, which Routes are healthy, or how a KPI is calculated before you change anything.
* **Hand off safely** — Use read-only mode for operators or auditors who should see answers but not apply changes.
## Run Models On-Premise or in the Cloud
Where inference runs is an architecture and compliance choice. The AI Assistant supports **edge (on-premise)** models and **cloud** providers so you can keep prompts and payloads on your network when you need to, or use hosted APIs when policy allows.
* **On-premise (edge)** — Run models on hardware you control, for example **Ollama** on the same host or LAN as the broker, so data does not leave your site.
* **Cloud providers** — Connect to hosted models such as **OpenAI**, **Anthropic**, or **Mistral** when your organization allows traffic to leave the network.
**Local AI prerequisite:** [Install Ollama](https://ollama.com/download) and have it running on the broker host (or a machine your broker can reach) before you pick **Local / Private** in the setup wizard. Pull your model in Ollama first (e.g. `ollama pull llama3.2`) — the name you enter in step 2 must match a model already available in Ollama. The model must support **tool calling** (function calling); the Assistant relies on tools to act on your broker, so models without tool-calling support will not work. Check the model's capabilities on the [Ollama model library](https://ollama.com/search?c=tools) before pulling it. Coreflux connects to your Ollama endpoint; it does not install Ollama or download models for you. Lightweight models may work for a quick test, but often are not capable enough for reliable agent work or good answer quality.
If the broker runs in **Docker**, `localhost` inside the container is not the host. With **Docker Compose**, point at the Ollama service (e.g. `http://ollama:11434`). If Ollama runs on the host, use `http://host.docker.internal:11434` (Docker Desktop) or the host's LAN IP. See [AI Routes — Ollama](/latest/lot-language/routes/ai-routes#picking-a-provider) for `BASE_URL` in LoT.
The same Assistant panel and modes work whether you point at an edge endpoint or a cloud API—you are not choosing a different product, only where the model runs.
***
## Set up the AI Assistant
The first time you open the AI Assistant, a short wizard walks you through three choices—**provider**, **model**, and **API key**—and prepares your broker. You only do this once per broker; after that, clicking the Coreflux icon opens the assistant directly.
Click the **Coreflux** icon in the bottom dock. On a fresh broker, the **AI Setup** wizard slides in on the right and asks you to pick where the model will run—a **Cloud** provider (OpenAI, Anthropic, Mistral) or **Local / Private** (Ollama). Pick one and click **Continue**.
Choose a model from the suggested list, or type the exact model name your provider supports (for example `gpt-5.4-mini`) into the custom field. Click **Continue** when you are ready.
Paste the API key from your chosen provider. The key is stored as an **encrypted secret on the broker** and is never sent back to your browser. The wizard also shows a quick summary of what it is about to install so you know what to expect.
If you picked **Ollama** in the earlier step, the wizard skips this screen entirely—your local Ollama endpoint does not need a key.
Sign in to the provider you picked in the previous step and follow their docs to create a key:
* **OpenAI** — [platform.openai.com/api-keys](https://platform.openai.com/api-keys)
* **Anthropic** — [console.anthropic.com](https://console.anthropic.com/)
* **Mistral** — [console.mistral.ai](https://console.mistral.ai/)
If you picked **Ollama**, no key is required—the broker talks to your local Ollama endpoint directly.
Click **Install routes**. The broker provisions everything the assistant needs in a few seconds and then closes the wizard automatically.
Once the wizard closes, the AI Assistant is live on this broker. From now on, clicking the **Coreflux** icon opens the assistant directly—no more setup.
***
## Open the AI Assistant
Use the **Coreflux** icon in the bottom app dock—the first icon on the left. You can peek at the assistant without opening the full side panel, then click when you want the full workspace.
Hover the Coreflux icon. A compact **Coreflux AI** bubble appears above the dock with an **Ask anything…** field and **Insight** / **Agent** mode pills so you can send a quick prompt or pick a mode before you commit to the full panel.
Click the Coreflux icon to pin the **AI Assistant** on the right. You get suggested prompts (for example **Create a dashboard** or **Calculate a new KPI**), a **Sources** area for dragging in panels such as the LoT Editor, and the full conversation thread with edit and close controls in the header.
## Agent vs Insight mode
Pick the mode that matches how much change you want the assistant to make. You can switch at the bottom of the panel at any time.
| Mode | What it does | Use it when |
| ----------- | -------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------- |
| **Agent** | Can read broker state **and** create or update things—Routes, Actions, Models, panels—subject to your account permissions. | You want the assistant to **build or change** integrations, logic, or dashboards. |
| **Insight** | **Read-only**—inspects topics, Routes, payloads, and traces; answers questions; can show tables and charts. | You want **safe exploration**, reporting, or to give users answers **without** deploy rights. |
The mode pills sit below the main input so you always know which behavior you have selected before you send a message.
## What you can do
These are typical starting points—phrase them in your own words; the assistant will ask follow-ups if something is missing.
Ask for live gauges, KPI tiles, or layouts bound to your MQTT topics; the assistant drafts and wires widgets you can refine in the panel. See [Build Your First Dashboard](/latest/quick-start/building-dashboards).
Describe triggers, conditions, and outputs in plain language; the assistant proposes LoT you review and deploy without leaving the conversation.
Describe the metric or inputs you care about; the assistant drafts logic, confirms details with you, and deploys an Action in Agent mode.
Ask what Routes or topics exist, why something errored, or how signals relate—natural in Insight mode when you want answers before any changes.
***
## Example: Build a KPI with Agent mode
Below is one real flow: you ask for a KPI, the assistant requests the missing pieces, then deploys an Action and offers a shortcut to the LoT Editor. Stay in **Agent** mode for this path.
Type a goal such as **Calculate a new KPI**. The assistant loads context and may offer quick actions that match what you said.
When the assistant needs a formula, source topic, or rule, it **pauses and asks you directly**—you reply in the thread instead of guessing what to type up front.
After you supply enough detail, the assistant can draft LoT, deploy an Action, and show a status card. Use **Open in LoT Editor** when you want to inspect or tweak the definition yourself.
When you see **Running** (or equivalent success state) on the deployment card, the new Action is live on the broker—confirm in the **LoT Editor** if you want to read the exact definition.
***
## Example: Explore safely with Insight mode
Switch to **Insight** before you send if you only want answers—not changes. You can still get structured output such as tables and follow-up suggestions.
Try questions like **Create me a list of Routes** or **Which Routes are in a warning state?** The assistant reads broker state and renders the response in the panel without using write tools.
## Built-in safeguards
* **Your permissions apply** — The assistant cannot read topics or change Routes beyond what your HUB user is allowed to do.
* **Insight mode is read-only** — No deploy, publish, or execute path while Insight is selected.
* **Stop at any time** — Long runs can be cancelled from the UI so you are never stuck waiting on a runaway answer.
* **Same deployment path as humans** — Anything the assistant deploys still goes through the broker’s normal validation and permission checks.
***
## Best practices
Use Insight to map topics, Routes, and health first. Switch to Agent only when you are ready for the assistant to create or update broker objects.
Open new Actions or Routes in the **LoT Editor** or **Routes** app to read names, triggers, and bindings before you rely on them in production.
## Next steps
Open and refine Actions, Models, and Rules the assistant creates—same editor, full control.
Add or monitor Routes the assistant can read and help you configure next.
# Connections Manager
Source: https://docs.coreflux.org/hub/connections
Save multiple broker connections, switch between them without logging out, and secure each one with its own TLS certificates
## Why Use the Connections Manager
If you work with more than one broker — a local development instance, a staging cluster, a production site — the HUB remembers each one as a saved connection instead of a single remembered URL. Pick a broker from the list to sign in, probe every saved broker for reachability before you commit, and jump between brokers from the account menu without logging out and re-entering details.
## Adding a Connection
On the login screen, click **New Connection** in the header (or click a saved card's edit action to modify an existing one). The connection form opens in the same card.
Enter a name, the **Protocol** (`mqtt://`, `mqtts://`, `ws://`, or `wss://`), **Host**, and **Port**. For a HUB and broker started together with Docker Compose, use **Host** `broker` rather than `localhost` — the HUB proxies MQTT from inside Docker.
Add at least one identity: a username and password, or an anonymous identity if the broker allows anonymous access. A connection can hold several identities — see [Identities and Switch User](#identities-and-switch-user) below.
Click **Test** to attempt a throwaway connection with the details you entered, without saving anything. Click **Save** to store the connection for later, or **Connect** to save and sign in immediately.
## Saved Connections
Each saved connection appears as a card on the login screen, showing its name, broker URL, and a **Reachable** status that the HUB probes automatically in the background — no need to open the connection to see whether that broker is up.
* **Favorites** — star a connection to pin it; use the **All / Starred** filter above the list to narrow the view.
* **Reorder** — drag a card to change its position in the list.
* **Enter** — pick an identity from the card's dropdown and click **Enter**, or press Enter with a card focused, to sign in.
* **Edit or delete** — open a card to change its details, or remove it entirely.
## Identities and Switch User
A single saved connection can hold more than one identity — for example a day-to-day account and an admin account on the same broker. Mark one identity as the **default** for that connection, and switch between the others from the account menu without leaving the app: open the topbar account menu and choose **Switch user** to reconnect as a different identity on the *current* broker, or **Change Broker** to reconnect to a *different* saved connection (using that connection's default identity). Both close the current session cleanly and reopen it against the new target.
## Securing a Connection with TLS
For `mqtts://` and `wss://` connections, open the connection's detail panel to attach TLS material:
* A **CA certificate**, if the broker's certificate isn't already trusted by your system.
* A **client certificate and private key**, for brokers that require mutual TLS.
Server certificate verification is on by default for secure connections; only turn it off for trusted development brokers.
This is separate from **route** certificate management (trust stores for OPC UA and similar routes running *on* the broker) — see the route's own Certificate Management section when you configure a [Route](/hub/routes/overview).
## Where Credentials Are Stored
* **Desktop app** — passwords and the TLS private key are encrypted at rest using your operating system's secure storage. They are never written to the plain settings file the HUB otherwise syncs to disk.
* **Browser app** — credentials are kept for the session and are not written to disk by the HUB; use your browser's own security practices (a trusted device, a locked profile) when saving connections there.
The broker's TLS **certificate** (the public part) can still be attached to a saved connection either way, since it isn't sensitive.
***
## Next Steps
Install the broker and HUB, and make your first connection.
Configure routes, including per-route TLS trust stores.
# Container Components
Source: https://docs.coreflux.org/hub/dashboards/components/containers
Group dashboard components with cards and tabs to structure larger panels in the Coreflux HUB.
Containers group other components. Use them when a panel grows beyond a simple grid — to cluster a machine's readouts under one title or split a screen into tabs.
**Like trays on a workbench.** The bench grid holds the trays in place; each tray keeps one job's tools together so they move as a unit.
***
Two containers are available today: `card` for grouping, `tabs` for splitting.
## Card
Groups child components under a title.
A titled card with one readout inside:
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
ADD COMPONENT "StatusCard" WITH TYPE "card"
SET TITLE "Zone 1 Status"
ADD COMPONENT "TempReading" WITH TYPE "value"
BIND VALUE TO TOPIC "zone1/temperature"
SET UNIT "°C"
```
A card combining a readout, an indicator, and a navigation button — the two extra children are the new parts:
```lot wrap focus={9-17} theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
ADD COMPONENT "StatusCard" WITH TYPE "card"
SET TITLE "Zone 1 Status"
ADD COMPONENT "TempReading" WITH TYPE "value"
BIND VALUE TO TOPIC "zone1/temperature"
SET UNIT "°C"
ADD COMPONENT "StatusIndicator" WITH TYPE "indicator"
BIND STATE TO TOPIC "zone1/status"
SET ON_VALUE "running" WITH COLOR "green"
SET ON_VALUE "stopped" WITH COLOR "red"
ADD COMPONENT "DetailsButton" WITH TYPE "button"
SET LABEL "Details"
ON CLICK DO NAVIGATE TO PANEL "Zone1Details"
```
| Property | Type | Description |
| -------- | ------ | ------------ |
| `TITLE` | String | Card heading |
## Tabs
Each tab holds its own components — useful for Overview / Analytics / Settings splits on one screen.
In the current HUB build, tab headers show the **inner component name** rather than the `ADD TAB` title. The structure below matches the spec syntax; tab labelling is tracked as a known issue.
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
ADD COMPONENT "MainTabs" WITH TYPE "tabs"
ADD TAB "Overview"
ADD COMPONENT "OvTemp" WITH TYPE "value"
BIND VALUE TO TOPIC "zone1/temperature"
SET UNIT "°C"
ADD TAB "Analytics"
ADD COMPONENT "OvGauge" WITH TYPE "gauge"
SET RANGE FROM 0 TO 100
BIND VALUE TO TOPIC "zone1/temperature"
ADD TAB "Settings"
ADD COMPONENT "OvText" WITH TYPE "text"
SET LABEL "Settings go here"
```
## Card vs. grid position?
Use **grid positioning** (`AT COLUMN/ROW`) when components are independent readouts. Use a **card** when components belong to the same asset and should travel together — a card moves as one unit when you rearrange the panel. See [Mixing layouts with cards](/latest/lot-language/panels/defining-panels#mixing-layouts-with-cards) for a complete panel that combines both.
## Next Steps
The readouts you place inside cards and tabs.
Full panels that combine containers with live data.
# Display Components
Source: https://docs.coreflux.org/hub/dashboards/components/display
Read-only widgets: gauges, values, indicators, charts, tables, and alerts.
Display components show live data without accepting input. Each entry below opens with tabbed examples — the rendered result above the code that produces it — followed by the property reference.
**Like instruments on a car dashboard.** Each one reads a signal and shows it its own way — a dial, a number, a warning light — and none of them lets you steer.
***
## Gauge
Circular (radial) or linear gauge for numeric values.
A radial gauge bound to a temperature topic:
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
ADD COMPONENT "TempGauge" WITH TYPE "gauge"
SET RANGE FROM 0 TO 100
SET UNIT "°C"
BIND VALUE TO TOPIC "sensor/temperature"
```
A gauge with a label, one decimal place, and two configured thresholds:
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
ADD COMPONENT "ProductionGauge" WITH TYPE "gauge"
SET STYLE "radial"
SET RANGE FROM 0 TO 100
SET UNIT "units/hr"
SET LABEL "Production Rate"
SET DECIMALS 1
BIND VALUE TO TOPIC "factory/production/rate"
ADD THRESHOLD AT 75 WITH COLOR "warning" LABEL "High"
ADD THRESHOLD AT 90 WITH COLOR "critical" LABEL "Critical"
```
| Property | Type | Default | Description |
| ------------------- | ----------------------- | ---------- | ---------------- |
| `STYLE` | `"radial"` / `"linear"` | `"radial"` | Gauge appearance |
| `RANGE FROM x TO y` | Numbers | required | Min / max values |
| `UNIT` | String | `""` | Unit label |
| `LABEL` | String | `""` | Display label |
| `DECIMALS` | Integer | `0` | Decimal places |
**Bind targets:** `VALUE`, `MIN`, `MAX`
## Value
The simplest read-only display: a number or string, with unit and formatting.
A pressure readout with two decimals:
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
ADD COMPONENT "PressureDisplay" WITH TYPE "value"
BIND VALUE TO TOPIC "pump/pressure"
SET UNIT "bar"
SET DECIMALS 2
```
Extracting one field from a JSON MQTT payload — `JSON_PATH` and the label are the new parts:
```lot wrap focus={3,5} theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
ADD COMPONENT "EnergyDisplay" WITH TYPE "value"
BIND VALUE TO TOPIC "machines/pump1/data"
SET JSON_PATH "$.energy_wh"
SET UNIT "Wh"
SET LABEL "Energy"
SET DECIMALS 2
```
| Property | Type | Default | Description |
| ----------- | ------- | ------- | -------------------------------- |
| `UNIT` | String | `""` | Unit label |
| `LABEL` | String | `""` | Display label |
| `DECIMALS` | Integer | `0` | Decimal places |
| `JSON_PATH` | String | — | JSONPath to extract from payload |
**Bind targets:** `VALUE`, `MAX`
If your topic carries a JSON object rather than a plain number, set `JSON_PATH` to pick the field — otherwise the widget shows the raw payload. Inspect the payload first in the [Data Viewer](/hub/mqtt/data-viewer).
## Text / Label
Dynamic or static text. `label` is an alias for `text`.
A static heading rendered at large size:
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
ADD COMPONENT "SectionTitle" WITH TYPE "label"
SET LABEL "Zone 1 Overview"
SET SIZE "large"
```
Dynamic text whose content updates live from the broker — the `BIND CONTENT` line is what makes it live:
```lot wrap focus={2} theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
ADD COMPONENT "StatusText" WITH TYPE "text"
BIND CONTENT TO TOPIC "system/message"
SET SIZE "large"
```
| Property | Type | Default | Description |
| -------- | ---------------------------------- | ----------- | ----------- |
| `SIZE` | `"small"` / `"medium"` / `"large"` | `"medium"` | Font size |
| `COLOR` | Color string | `"inherit"` | Text color |
**Bind targets:** `CONTENT`, `COLOR`, `TITLE`, `LABEL`
## Indicator
Maps incoming values to colors — running/stopped/fault at a glance.
A pump-running dot — green when the bound topic reads `true`:
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
ADD COMPONENT "PumpIndicator" WITH TYPE "indicator"
BIND STATE TO TOPIC "pump/running"
SET ON_VALUE "true" WITH COLOR "success"
SET ON_VALUE "false" WITH COLOR "muted"
```
Three states mapped to three colors, rendered large — the extra mapping and size are highlighted:
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
ADD COMPONENT "MachineStatus" WITH TYPE "indicator"
BIND STATE TO TOPIC "machine/status"
SET ON_VALUE "running" WITH COLOR "green"
SET ON_VALUE "stopped" WITH COLOR "red"
SET ON_VALUE "maintenance" WITH COLOR "yellow"
SET SIZE "large"
```
| Property | Type | Default | Description |
| ---------- | ---------------------------------- | ---------- | ----------------------------------- |
| `ON_VALUE` | `"state" WITH COLOR "color"` | required | One or more value-to-color mappings |
| `SIZE` | `"small"` / `"medium"` / `"large"` | `"medium"` | Indicator dot size |
**Bind targets:** `STATE`
## Realtime Chart
Live streaming chart — it receives **individual data points** from a topic and builds the trend itself.
One topic, one trace — every published value becomes a point:
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
ADD COMPONENT "ThroughputTrend" WITH TYPE "realtime-chart"
SET LABEL "Line Throughput"
BIND VALUE TO TOPIC "factory/throughput"
```
The same idea with an explicit chart type, a y-axis unit, and a series label — the additions are highlighted:
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
ADD COMPONENT "MessageCountTrend" WITH TYPE "realtime-chart"
SET LABEL "Message Count Trend"
SET CHART_TYPE "line"
SET UNIT "msg"
BIND VALUE TO TOPIC "kpi/operational/messageCount"
SET SERIES_LABEL "Message Count"
```
**Multi-series is not working in the current HUB build.** Adding a second `BIND VALUE TO TOPIC` (each with its own indented `SET SERIES_LABEL`) is the documented syntax for multiple traces, but the chart currently renders a single trace and no legend. Use one chart per signal for now.
| Property | Type | Description |
| -------------- | ------------------ | ------------------------------------------------------ |
| `CHART_TYPE` | `"line"` / `"bar"` | Trace style (the current build renders both as a line) |
| `UNIT` | String | Y-axis unit label |
| `LABEL` | String | Chart heading |
| `SERIES_LABEL` | String | Series name — set **under the BIND, indented** |
**Bind target:** `VALUE`
## Image
Static, dynamic, or camera-fed image.
A static image loaded from a URL:
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
ADD COMPONENT "ProductPhoto" WITH TYPE "image"
SET SRC "https://picsum.photos/320/240"
SET WIDTH 320
SET HEIGHT 240
```
A live feed whose source URL is published over MQTT — the `BIND SRC` line replaces the static source:
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
ADD COMPONENT "CameraFeed" WITH TYPE "image"
BIND SRC TO TOPIC "camera/1/snapshot"
SET WIDTH 320
SET HEIGHT 240
```
| Property | Type | Description |
| ----------------- | ------------------ | ------------------------------ |
| `SRC` | String / ENV / URL | Image source path or URL |
| `FALLBACK` | String | Fallback image if source fails |
| `WIDTH`, `HEIGHT` | Integer | Dimensions in pixels |
**Bind targets:** `SRC`, `VISIBLE`
## Table
Tabular data from a JSON array topic or a query result.
Each array element becomes a row; `COLUMNS` picks and orders the fields:
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
ADD COMPONENT "ScanHistory" WITH TYPE "table"
BIND DATA TO TOPIC "barcode/history"
SET COLUMNS "time, barcode, product, status"
SET MAX_ROWS 10
```
| Property | Type | Description |
| ---------- | ------- | ---------------------------- |
| `COLUMNS` | String | Comma-separated column names |
| `MAX_ROWS` | Integer | Maximum rows to display |
**Bind targets:** `DATA`
## List
Scrollable list of items from a topic or model.
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
ADD COMPONENT "AlarmLog" WITH TYPE "list"
BIND DATA TO TOPIC "zone1/alarms"
```
**Bind targets:** `DATA`
## Alert
Banner for critical messages — typically bound to an alarm flag.
The banner appears while the alarm flag is true and shows the live message:
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
ADD COMPONENT "AlarmBanner" WITH TYPE "alert"
BIND VISIBLE TO TOPIC "pump/alarm_active"
BIND CONTENT TO TOPIC "pump/alarm_message"
SET VARIANT "critical"
```
In the current HUB build the `critical` variant renders in a neutral/muted style rather than a strong red banner — the message and visibility behaviour work as shown.
| Property | Type | Description |
| ------------------ | ------------------------------------- | -------------- |
| `TYPE` / `VARIANT` | `"critical"` / `"warning"` / `"info"` | Severity style |
**Bind targets:** `CONTENT`, `VISIBLE`
## Common mistakes
Properties use `SET`, never `WITH`. `WITH UNIT "msg"`, `WITH RANGE 0, 200`, and `WITH DECIMALS 2` are silently ignored — write `SET UNIT "msg"`, `SET RANGE FROM 0 TO 200`, `SET DECIMALS 2`.
The topic carries a JSON object — add `SET JSON_PATH "$.field"` to extract the field you want, and confirm the payload shape in the Data Viewer.
## Next Steps
Buttons, toggles, sliders, and fields that write back to the broker.
Topics, models, queries, transformations, and JSON extraction.
# Input & Control Components
Source: https://docs.coreflux.org/hub/dashboards/components/inputs
Buttons, toggles, sliders, and fields that write back to your broker.
Input components let operators act: start a line, set a speed, type a value. One rule applies to all of them: **input components don't publish by themselves** — the `ON DO` handler does.
**Like a steering wheel, not a speedometer.** Display widgets read signals; these widgets send them — every interaction ends in a publish back to the broker.
***
One section per control, each with its rendered result, the code, and then the property reference.
## Button
A button that publishes a command when clicked:
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
ADD COMPONENT "StartButton" WITH TYPE "button"
SET LABEL "Start Production"
ON CLICK DO
PUBLISH TOPIC "factory/commands/start" WITH 1
```
A danger-styled emergency stop — the `STYLE` line is the new part:
```lot wrap focus={3} theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
ADD COMPONENT "StopButton" WITH TYPE "button"
SET LABEL "Emergency Stop"
SET STYLE "danger"
ON CLICK DO
PUBLISH TOPIC "factory/commands/stop" WITH 1
```
| Property | Type | Default | Description |
| ---------- | ---------------------------------------- | ----------- | -------------- |
| `LABEL` | String | required | Button text |
| `STYLE` | `"primary"` / `"secondary"` / `"danger"` | `"primary"` | Visual style |
| `DISABLED` | Boolean | `false` | Disable button |
**Events:** `ON CLICK DO`
## Toggle / Switch
Binary on/off control. `switch` is an alias for `toggle`.
The toggle reflects the bound topic live — publish `true` or `false` to it and the switch follows:
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
ADD COMPONENT "EnableSwitch" WITH TYPE "switch"
SET LABEL "Enable Production"
BIND STATE TO TOPIC "factory/production/enabled"
```
| Property | Type | Description |
| -------- | ------ | ------------ |
| `LABEL` | String | Toggle label |
**Bind targets:** `STATE` — **Events:** `ON TOGGLE DO`
**Write-back is broken in the current HUB build.** `ON TOGGLE DO PUBLISH ... WITH STATE` fires, but the message payload is the literal text `{{STATE}}` instead of the toggle state (`WITH VALUE` behaves the same). Until this is fixed, use the toggle as a live state display and trigger commands from a [button](#button).
## Slider
Range input for numeric values.
The slider tracks its bound topic live — the thumb moves as new values arrive:
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
ADD COMPONENT "SetpointSlider" WITH TYPE "slider"
SET RANGE FROM 0 TO 200
SET UNIT "°C"
BIND VALUE TO TOPIC "control/setpoint"
```
| Property | Type | Description |
| ------------------- | ------- | ----------- |
| `RANGE FROM x TO y` | Numbers | Min / max |
| `UNIT` | String | Unit label |
**Bind targets:** `VALUE` — **Events:** `ON CHANGE DO`
**Write-back is broken in the current HUB build.** `ON CHANGE DO PUBLISH ... WITH VALUE` sends the literal text `{{VALUE}}` instead of the slider position, and dragging is overridden by the bound topic. Until this is fixed, use the slider as a live numeric display and set values from a [number field](#number-field).
## Text Field / Input
Free text entry.
A labeled field pre-filled from its topic; typing publishes the new value:
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
ADD COMPONENT "NameInput" WITH TYPE "text-field"
SET LABEL "Device Name"
SET PLACEHOLDER "Enter name..."
BIND VALUE TO TOPIC "config/device/name"
ON CHANGE DO
PUBLISH TOPIC "config/device/name" WITH VALUE
```
| Property | Type | Description |
| ------------- | ------ | ---------------- |
| `LABEL` | String | Field label |
| `PLACEHOLDER` | String | Placeholder text |
**Bind targets:** `VALUE` — **Events:** `ON CHANGE DO`
## Number Field
Numeric-only entry.
Accepts only numbers and publishes the limit on change:
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
ADD COMPONENT "MaxPressure" WITH TYPE "number-field"
SET LABEL "Max Pressure (bar)"
SET PLACEHOLDER "0.00"
BIND VALUE TO TOPIC "config/pressure/max"
ON CHANGE DO
PUBLISH TOPIC "config/pressure/max" WITH VALUE
```
| Property | Type | Description |
| ------------- | ------ | ---------------- |
| `LABEL` | String | Field label |
| `PLACEHOLDER` | String | Placeholder text |
**Bind targets:** `VALUE` — **Events:** `ON CHANGE DO`
## Common mistakes
Text and number fields fire `ON CHANGE` when the value is **committed** — press Enter (or leave the field) after typing. Each keystroke alone does not publish.
The toggle reflects its `BIND STATE` topic. If nothing updates that topic, the toggle snaps back to the last bound value — make sure something (device, Action, or a test publish) updates the bound state.
## Next Steps
Everything a control can do: publish, navigate, invoke, trigger, and branch.
Group controls and readouts into cards and tabs.
# Dashboard Manager
Source: https://docs.coreflux.org/hub/dashboards/manager
Create, open, share, and manage dashboard panels in the Coreflux HUB.
## Opening the Dashboard Manager
The **Dashboard Manager** is an app inside the Coreflux HUB. It lists your **panels** (each panel is one dashboard you can open, share, or refine). From here you add panels, refresh the list, and open a panel to see it live.
## How Panels Are Defined
The Dashboard Manager and the AI Assistant both produce the same thing: a **LoT panel definition** (`DEFINE PANEL …`). The Manager shows that definition next to its live result so you can edit either by hand or by asking the assistant. The **LoT PANEL** reference documents every panel keyword, component, binding, and event.
The widget tour, the 60-second first panel, and what dashboards can do.
Read and write the `DEFINE PANEL` source behind every dashboard.
## How data updates on a Dashboard
Each component is tied to **live data** on your broker—typically a MQTT topic that already carries the signal from your machines, databases, or Routes. When that topic’s value changes, the widget updates. You do not configure polling or manual refresh for normal operation.
## Viewing a Panel
You can experience the same panel in more than one layout depending on where you need it.
Open the panel as a **floating window** on top of the HUB. That keeps the rest of the HUB—routes, Data Viewer, configuration—one click away while you watch live values or try layout changes.
Open the panel in a **dedicated full view** when you want a clean surface for a plant display, a wall-mounted screen, or a link you share with people who only need the dashboard.
## Managing Panels
Each row in the panel list shows the panel name, a visibility hint (for example **Public**), its publication state (for example **Published**), and when it was last updated. Clicking the row itself opens the panel; hovering reveals a row of quick actions on the right for sharing, inspecting, and deleting.
| Action | How to trigger it | What it does |
| -------------------- | ------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------- |
| **Open as a window** | Click the panel row | Opens the panel as a floating window on top of the HUB so you can keep working while it stays live |
| **Open as a link** | Click the first icon on the right (external link) | Opens the panel in a dedicated shareable view—useful for plant displays, wall screens, or sending a URL to a teammate |
| **View source** | Click the second icon on the right (code) | Shows the panel’s underlying definition so you can inspect or copy it |
| **Delete dashboard** | Click the last icon on the right (trash) | Removes the panel from the list after confirmation |
## Sharing Panels on TVs and External Displays
You can put the same live panel on any screen that runs a browser—control-room wall TVs, lobby KPI boards, shop-floor tablets, or a monitor at a remote site—without installing a separate HMI app or giving everyone full access to the Coreflux HUB. The **shareable panel URL** opens a dedicated view of that dashboard so viewers see only the panel, not routes, configuration, or other HUB apps.
From the **Dashboard Manager**, hover the panel row and click the **external link** icon (first action on the right). That opens the panel in a dedicated tab or window; copy the URL from the address bar and paste it into a browser on a TV stick, kiosk PC, tablet, or any display you want to keep on the loop.
**Same live stream as inside the HUB.** The shareable URL reflects broker data the same way as the floating window—widgets update when topics change, without manual refresh for normal operation.
Typical uses:
* **Control-room and plant walls** — Full-screen dashboards that stay readable from a distance
* **Lobby and office KPI screens** — Management views without exposing operator tools
* **Shop-floor tablets** — Lightweight devices that only need one panel
* **Stakeholders and contractors** — A single URL when someone needs read-only visibility
Deleting a panel from the list removes that dashboard permanently after confirmation.
## Best Practices
Use names that tell you who it is for (for example “Line 3 — Shift Lead”) so the list stays scannable.
Ship a minimal panel with the three signals people ask for most, then add depth once it is in daily use.
Use pumps, valves, and tanks where operators already think in those terms; use charts where trends matter more than mimic layout.
Check labels, ranges, and safety-related controls before handing the panel URL to operators — they will rely on this screen.
***
## Next Steps
Browse topics and payloads so you know what to bind each widget to.
Ask the assistant to suggest layouts, bindings, or improvements to a panel.
# Dashboards in the HUB
Source: https://docs.coreflux.org/hub/dashboards/overview
Live panels bound to your broker data — written in the Dashboard Manager or generated by the AI Assistant.
Dashboards in Coreflux are **panels**: live screens assembled from components — gauges, charts, controls — each wired to data flowing through your broker. You write them in the **Coreflux HUB** Dashboard Manager, where the panel definition and its live result sit side by side, or you ask the **AI Assistant** to build one for you and refine it from there.
**Like a control room sketched in plain text.** You describe a gauge, point it at a signal, and it shows that signal live from the moment you save — no wiring diagram, no deploy step.
## What you can build
* **KPI walls** — large readouts and trends for supervisors
* **Line and area overviews** — at-a-glance status for a process or zone
* **Machine detail** — gauges, indicators, and controls for a single asset
* **Operator stations** — buttons, toggles, and setpoints where people work the process
* **Shift and mobile views** — simplified layouts for tablets and shared screens
***
Panels are authored as **LoT (Language of Things)** definitions — the syntax is documented under [LoT PANEL](/latest/lot-language/panels/defining-panels), which versions with your broker. The rest of this page is a quick tour of what the HUB renders; every widget has a full reference linked at the end.
## Your first panel in 60 seconds
Open **Dashboards** in the HUB, click **Create new panel**, and write the definition below. The panel renders live as you save — point a gauge at a topic and the needle starts moving:
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
DEFINE PANEL SensorMonitor
WITH VISIBILITY PUBLIC
WITH STATE PUBLISHED
WITH TITLE "Sensor Monitor"
WITH LAYOUT "grid" COLUMNS 2
ADD COMPONENT "TempGauge" WITH TYPE "gauge"
SET RANGE FROM 0 TO 100
SET UNIT "°C"
BIND VALUE TO TOPIC "sensor/temperature"
```
Every part of a definition is covered step by step in [Defining a Panel](/latest/lot-language/panels/defining-panels).
Describe what you want in plain language:
> "Build me a panel with a radial temperature gauge on `sensor/temperature`, range 0–100 °C, with a warning threshold at 75."
The assistant creates the panel for you. Open it in the Dashboard Manager to see the definition it wrote — and adjust anything directly. See [AI Assistant](/hub/ai-assistant).
Include `WITH VISIBILITY PUBLIC` **and** `WITH STATE PUBLISHED` in every panel definition — panels saved without them may not appear in the Dashboard Manager list. See [Defining a Panel](/latest/lot-language/panels/defining-panels).
## Tour of the main widgets
A quick taste of each family — every widget has a full reference page.
### Readouts
A value with context: range, unit, and thresholds.
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
ADD COMPONENT "TempGauge" WITH TYPE "gauge"
SET RANGE FROM 0 TO 100
SET UNIT "°C"
BIND VALUE TO TOPIC "sensor/temperature"
```
The big number — unit, decimals, wall-readable.
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
ADD COMPONENT "ErrorRate" WITH TYPE "value"
SET LABEL "Error Rate"
SET UNIT "%"
SET DECIMALS 2
BIND VALUE TO TOPIC "kpi/operational/errorRate"
```
State at a glance — values mapped to colors.
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
ADD COMPONENT "MachineStatus" WITH TYPE "indicator"
BIND STATE TO TOPIC "machine/status"
SET ON_VALUE "running" WITH COLOR "green"
SET ON_VALUE "stopped" WITH COLOR "red"
```
A banner that appears when a condition is true.
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
ADD COMPONENT "AlarmBanner" WITH TYPE "alert"
BIND VISIBLE TO TOPIC "pump/alarm_active"
BIND CONTENT TO TOPIC "pump/alarm_message"
SET VARIANT "critical"
```
### Charts and tables
Builds a live trend from individual data points on a topic.
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
ADD COMPONENT "MessageCountTrend" WITH TYPE "realtime-chart"
SET LABEL "Message Count Trend"
SET CHART_TYPE "line"
SET UNIT "msg"
BIND VALUE TO TOPIC "kpi/operational/messageCount"
SET SERIES_LABEL "Message Count"
```
Rows from a JSON array topic or query result.
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
ADD COMPONENT "ScanHistory" WITH TYPE "table"
BIND DATA TO TOPIC "barcode/history"
SET COLUMNS "time, barcode, product, status"
```
### Controls
On/off switch that tracks the live state of its bound topic.
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
ADD COMPONENT "PumpControl" WITH TYPE "toggle"
SET LABEL "Pump Enable"
BIND STATE TO TOPIC "zone1/pump/state"
```
Click to act — publish a command to any topic, or navigate to another panel.
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
ADD COMPONENT "StartButton" WITH TYPE "button"
SET LABEL "Start Production"
ON CLICK DO
PUBLISH TOPIC "factory/commands/start" WITH 1
```
Numeric value tracked live on a range.
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
ADD COMPONENT "SpeedSlider" WITH TYPE "slider"
SET RANGE FROM 0 TO 100
SET UNIT "%"
BIND VALUE TO TOPIC "zone1/conveyor/speed"
```
## What dashboards can do
* **Live bindings** — bind to MQTT topics, LoT Model fields, or database queries; widgets update the moment values change. See [Data Binding](/latest/lot-language/panels/data-binding).
* **Interactive controls** — buttons and entry fields that publish commands, invoke Actions, and trigger Routes. See [Actions & Events](/latest/lot-language/panels/actions-events).
* **Multi-page navigation** — buttons and cards that jump between panels with `NAVIGATE`, for overview-to-detail drill-downs. See [Defining a Panel](/latest/lot-language/panels/defining-panels#navigation).
* **Share on any screen** — a panel URL runs full-screen on wall TVs, tablets, and kiosks, no app install. See [Dashboards in the HUB](/hub/dashboards/overview).
## Next Steps
A 10-minute guided walk from topic to published panel.
Every widget, every property, every bind target.
# Common Patterns
Source: https://docs.coreflux.org/hub/dashboards/patterns
Ready-to-adapt panel recipes for the most frequent dashboard jobs.
Each recipe is a complete, working panel. Copy it, swap the topics for yours, and adjust ranges and labels.
**Like starter dough.** Each recipe already works as-is — you don't build from scratch, you season: your topics, your ranges, your labels.
***
Five recipes, ordered from the smallest useful panel to data-layer integrations.
## Minimal monitoring panel
**Goal:** one signal, one status — the smallest useful dashboard.
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
DEFINE PANEL SensorMonitor
WITH VISIBILITY PUBLIC
WITH STATE PUBLISHED
WITH TITLE "Sensor Monitor"
WITH LAYOUT "grid" COLUMNS 2
ADD COMPONENT "TempGauge" WITH TYPE "gauge"
AT COLUMN 1 ROW 1
SET RANGE FROM 0 TO 150
SET UNIT "°C"
BIND VALUE TO TOPIC "sensor/temperature"
ADD THRESHOLD AT 100 WITH COLOR "warning"
ADD THRESHOLD AT 130 WITH COLOR "critical"
ADD COMPONENT "StatusIndicator" WITH TYPE "indicator"
AT COLUMN 2 ROW 1
BIND STATE TO TOPIC "machine/status"
SET ON_VALUE "running" WITH COLOR "green"
SET ON_VALUE "stopped" WITH COLOR "red"
SET SIZE "large"
```
**Adapt:** your topic names, the gauge range, the threshold levels.
## Live KPI dashboard
**Goal:** a wall screen with a headline number, its trend, and a secondary KPI.
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
DEFINE PANEL KpiDashboard
WITH VISIBILITY PUBLIC
WITH STATE PUBLISHED
WITH TITLE "Operational KPI Dashboard"
WITH LAYOUT "grid" COLUMNS 3
ADD COMPONENT "MessageCountGauge" WITH TYPE "gauge"
SET LABEL "Message Count"
SET RANGE FROM 0 TO 200
SET UNIT "msg"
BIND VALUE TO TOPIC "kpi/operational/messageCount"
ADD COMPONENT "MessageCountTrend" WITH TYPE "realtime-chart"
SET LABEL "Message Count Trend"
SET CHART_TYPE "line"
SET UNIT "msg"
BIND VALUE TO TOPIC "kpi/operational/messageCount"
SET SERIES_LABEL "Message Count"
ADD COMPONENT "ErrorRate" WITH TYPE "value"
SET LABEL "Error Rate"
SET UNIT "%"
SET DECIMALS 2
BIND VALUE TO TOPIC "kpi/operational/errorRate"
```
**Adapt:** share it on a TV via the panel URL — see [Dashboards in the HUB](/hub/dashboards/overview).
## Control panel
**Goal:** drive the line from the dashboard — buttons publish commands, a number field sets the speed, and a slider tracks the live feedback.
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
DEFINE PANEL Zone1Control
WITH VISIBILITY PUBLIC
WITH STATE PUBLISHED
WITH TITLE "Zone 1 Control"
WITH LAYOUT "grid" COLUMNS 2
ADD COMPONENT "StartButton" WITH TYPE "button"
AT COLUMN 1 ROW 1
SET LABEL "Start Conveyor"
ON CLICK DO
PUBLISH TOPIC "zone1/conveyor/cmd/start" WITH 1
ADD COMPONENT "StopButton" WITH TYPE "button"
AT COLUMN 2 ROW 1
SET LABEL "Stop Conveyor"
SET STYLE "danger"
ON CLICK DO
PUBLISH TOPIC "zone1/conveyor/cmd/stop" WITH 1
ADD COMPONENT "SpeedSetpoint" WITH TYPE "number-field"
AT COLUMN 1 ROW 2
SET LABEL "Speed Setpoint (%)"
SET PLACEHOLDER "0"
ON CHANGE DO
PUBLISH TOPIC "zone1/conveyor/cmd/speed" WITH VALUE
ADD COMPONENT "LiveSpeed" WITH TYPE "slider"
AT COLUMN 2 ROW 2
SET RANGE FROM 0 TO 100
SET UNIT "%"
BIND VALUE TO TOPIC "zone1/conveyor/speed"
```
**Adapt:** the command topics (`cmd/...`) and the feedback topic (`speed`) — keeping them separate means the slider reflects what the line is actually doing, not just the last setpoint sent.
## Panel backed by a LoT Model
**Goal:** when a [Model](/latest/lot-language/models/overview) already aggregates your data layer, bind to its fields instead of raw topics.
**Known issue (current HUB build).** `BIND … TO MODEL` bindings do not receive data yet, and `SET MODEL … WITH VALUE` write-back from panel handlers does not update model fields — so this recipe cannot be shown rendering live values and the setpoint field cannot write back. The syntax below follows the spec; in the meantime, bind widgets to the model's source topics (or its output topic with `JSON_PATH`) and publish setpoints to those topics directly.
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
DEFINE PANEL PumpDashboard
WITH VISIBILITY PUBLIC
WITH STATE PUBLISHED
WITH TITLE "Pump System"
WITH LAYOUT "grid" COLUMNS 3
ADD COMPONENT "FlowGauge" WITH TYPE "gauge"
BIND VALUE TO MODEL "PumpSystem.flow_rate"
SET UNIT "L/min"
SET RANGE FROM 0 TO 500
ADD COMPONENT "PressureDisplay" WITH TYPE "value"
BIND VALUE TO MODEL "PumpSystem.pressure"
SET UNIT "bar"
SET DECIMALS 2
ADD COMPONENT "PumpStatus" WITH TYPE "indicator"
BIND STATE TO MODEL "PumpSystem.running"
SET ON_VALUE "true" WITH COLOR "success"
SET ON_VALUE "false" WITH COLOR "muted"
SET SIZE "large"
ADD COMPONENT "FlowSetpoint" WITH TYPE "number-field"
SET LABEL "Flow Setpoint (L/min)"
SET PLACEHOLDER "0"
BIND VALUE TO MODEL "PumpSystem.setpoint"
```
## Next Steps
Walk through one of these panels end to end with live data.
Publish, share, and put any of these on a wall screen.
# LoT Editor
Source: https://docs.coreflux.org/hub/lot-editor
Author, deploy, and inspect LoT and Python as notebooks and projects in the Coreflux HUB
## Why Use the LoT Editor?
The LoT Editor brings the **LoT Notebooks** experience from VS Code into the Coreflux HUB. You author LoT and Python in notebook cells, deploy any cell on its own, and see per-cell whether your copy matches what the broker is running — with the broker's live entity list right beside your code, without leaving the browser.
This makes the HUB a single place to develop, manage, and operate your Coreflux solutions.
**This is parity, not a replacement.** Editing LoT from the [VS Code LoT Notebooks extension](/lot-notebooks/usage) remains fully supported, and you never have to choose between the two. The broker is the source of truth: either editor deploys to it, and both read the same project. `Import .lotnb`, `Push to broker`, and `Pull from broker` are what keep the two workflows one.
**Like a lab notebook wired to the machine it describes.** The prose, tables, and code live in one document — and each block of code is the thing that is actually running.
## When to Use It
* **Deploying LoT without another tool installed** — Any HUB install, Docker or desktop app, can author and deploy
* **Checking what is really on the broker** — **Broker runtime** is the authoritative list, next to your code
* **Iterating on one definition** — Run a single cell instead of redeploying a whole project
* **Auditing drift** — Per-cell badges and a broker diff show exactly which definitions have moved out of sync
* **Documenting a solution where it lives** — Markdown cells render inline, so the topic conventions and device maps sit beside the code that implements them
* **Putting live visuals next to the LoT that drives them** — Drag Library components into a markdown cell and watch topics such as `demo/sensor/value` update in the notebook
* **Picking up someone else's project** — Open it from the broker and read the notebook top to bottom
* **Publishing or recovering a project** — Push a local draft, Pull or Inspect one already on the broker, or restore an earlier commit
* **Removing without surprises** — A local delete, an entity remove, and a project remove from the broker are three different operations
***
## Opening the Editor
Select **LoT Editor** in the HUB dock — the notebook-and-pen icon, after **Data Viewer** and **Connections**. An orange dot under the icon marks the editor as the active app.
The window header carries the current project and notebook plus the controls that act on the whole notebook:
| Control | What it does |
| ------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **Breadcrumb** (`Sample › notebook.lotnb`) | The open project and file. Click either part to rename it |
| **`Deployed`** | Every comparable cell in the open project matches the broker. Drift shows as an orange count such as `N differs` or `N not running` — on a live project, click it to Push |
| **Branch chip** (`main`) | Git branch of the open project on the broker. Missing git is silence — there is no `Local workspace` chip |
| **`Deploy All`** | Deploys every LoT and Python cell in the **open notebook**. The split control also offers **Deploy project**, which stores and loads the whole project |
| **Action Console** (pulse) | Opens the right-hand **Action Console** rail for running and tracing Actions — see [Action Console](#action-console-traces-and-run-once) |
| **Remove entities** | Removes the entities *this notebook* defines from the broker |
| **Undo** / **Redo** | Step cell changes back and forward |
| **Attach notebook to AI chat** | Hands the open notebook to the HUB's AI assistant as context |
| **`…`** | `Import .lotnb file` and `Export open notebook as .lotnb` |
## A Notebook Is Both Document and Deployment
A notebook is an ordered list of cells, and the cell type dropdown decides what each one is:
| Cell type | Contents | Deployed? |
| ------------ | ------------------------------------------------------------------------------------------ | ------------------------------------------------------- |
| **LoT** | One complete definition — `DEFINE ACTION`, `DEFINE MODEL`, `DEFINE ROUTE`, `DEFINE RULE` | Yes, to the broker |
| **Python** | A Python script your Actions call, named in the cell's **Script name** field | Yes, to the broker |
| **Markdown** | Headings, prose, lists, tables, **live visual components**, and links to project documents | Visuals are published as panels; prose is documentation |
Markdown cells render in place between the executable ones. That is not documentation-only: you can drop a gauge, chart, or card from **Library → Components** into a markdown cell and it stays live, bound to MQTT. The Sample notebook uses this for a **live document preview** — widgets on `demo/sensor/value` sit in the notebook itself, so you deploy the cells above and watch the gauge, status, and trend update without leaving the editor.
**Adding visuals in the editor is a first-class authoring step**, not a Dashboards-only feature. Drag from Library, or write the component in the markdown cell — the notebook is allowed to *be* the live view. See [Library](#library-visual-components-and-documents).
To add a cell, use the **`+ Code`** and **`+ Markdown`** buttons that appear in the gutter between cells — so a new cell always lands exactly where you point at it.
## Working With Cells
Every cell has its own toolbar, left to right. Hover the cell to reveal the right-hand controls — **Move up** / **Move down**, **Duplicate**, and **Delete** stay hidden until then.
| Control | What it does |
| ------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------- |
| **Drag handle** | Drag to reorder the cell |
| **Run cell** (`Shift`+`Enter`) | Deploys just this cell to the broker |
| **Cell type** | Switch between `LoT`, `Python`, and `Markdown` |
| **Sync badge** | This cell's state against the broker — see below |
| **Run or trace** (pulse, Action cells only) | Pin this Action in the [Action Console](#action-console-traces-and-run-once). While a capture is armed it reads as tracing |
| **Compare with broker** | Shown when the cell is not `DEPLOYED` — see [Comparing a cell with the broker](#comparing-a-cell-with-the-broker). Appears on hover |
| **Move up** / **Move down** | Reorder without dragging. Appear on hover |
| **Duplicate** | Copy the cell and its contents. Appears on hover |
| **Delete cell** | Remove the cell from the notebook. Appears on hover |
Action cells also carry a strip under the code. Here it reads **`SummarizeF2Hvac · Not yet run · Tracing`** — expand it for Traces and Run, or choose **Open in Action Console** to pin the same panel in the right-hand rail. **Tracing** (green) means a five-minute capture is already armed for that Action.
Deleting a cell asks **"Delete this cell?"** and warns that the cell and its contents are removed from the notebook and that it cannot be undone. It changes the notebook only — the entity that cell deployed stays on the broker until you remove it there. See [Removal](#removal-four-different-operations).
## Action Console: Traces and Run Once
**Run cell** deploys the definition. The console underneath — or pinned from the header pulse — is where you watch and trigger execution: **Traces** records every run for five minutes, and **Run once** fires the Action with a topic and payload you choose.
Every Action cell has the same console in two places — expanded under the cell, or pinned in the header pulse rail. Both have **Traces** and **Run**.
| Control | What it does |
| -------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------- |
| **Traces** tab | A five-minute capture of every execution, including runs triggered elsewhere. Playback sits on the timeline; a single run's result stays on **Run** |
| **Run** tab | Topic and payload (or JSON parameters for callable Actions), then **Run once** |
| **`Use`** | Fill the payload from the last message on that topic, or from the last run |
| **Run once** | Live run — (re)deploys the version in the cell, then executes it. There is no sandbox: anything the Action publishes is published |
| **Open in Action Console** | Pin this Action in the right-hand rail so you can switch Actions without leaving the strip |
**Run once is a live run.** It (re)deploys the Action from the cell and executes it on the broker. Downstream topics, databases, and devices see the same publishes they would from a real trigger.
## Is This Cell Running on the Broker?
Each LoT and Python cell reports its own state against the broker, which is the fastest way to tell whether the code you are reading is the code that is running:
| Badge | Meaning | What to do |
| ----------------- | ------------------------------------------------------- | ------------------------------------------ |
| **`DEPLOYED`** | The cell matches an entity running on the broker | Nothing |
| **`DIFFERS`** | The entity is running but the source here has changed | Run the cell to update it |
| **`NOT RUNNING`** | Nothing of that name is running on the broker yet | Run the cell to deploy it |
| **`RUNNING`** | Something is running that this project does not declare | Adopt it, or remove it from Broker runtime |
`NOT RUNNING` and `DIFFERS` are how the editor answers "why is my Action missing from [LoT Flow](/hub/lot-flow)?" — the graph is drawn from Broker runtime, so a cell that has never been deployed has nothing to draw.
### Comparing a cell with the broker
Whenever a cell is not `DEPLOYED`, a **Compare with broker (Diff)** control appears on its toolbar. It replaces the editor with a line-by-line comparison of the running definition against your local copy, headed `BROKER · ⟷ LOCAL` with an added/removed line count:
For a `NOT RUNNING` cell the comparison is against nothing — every line shows as an addition, which is a quick way to review a whole definition before it goes live.
## Deploying
Two granularities, and the difference matters when a project is large:
* **Run cell** deploys that one definition. Use it while iterating — you get the broker's response for that entity alone.
* **Deploy All** deploys every LoT and Python cell in the **open notebook**. Use it while iterating.
* **Deploy project** (the other half of the split button) stores the whole project and loads it, replacing the broker's entity set. It always confirms first.
Either way the cell's badge flips to `DEPLOYED` once the broker confirms it, and the entity appears under **Broker runtime**.
Here is a complete Model as it appears in a cell — the fields the Action fills in are the ones worth reading first:
```lot wrap focus={2-4} theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
DEFINE MODEL SampleTableRow COLLAPSED
ADD DOUBLE "value"
ADD STRING "status"
ADD STRING "timestamp"
```
Running a cell writes to the broker immediately. There is no staging step and no separate save — the definition in the cell replaces whatever the broker was running under that name.
***
## The Sidebar: Projects and Broker Runtime
There is **one Projects list**, and **Broker runtime** sits under it as its own section — always present, even when nothing is running.
| Section | What it holds |
| ------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **Projects** | Every project you can open — on the broker, local-only drafts, and the one currently **LIVE**. `+ New` creates a draft. Hover a name for placement (`On broker · live`, `On broker · idle`, `Local draft`). The row plug is **Activate** or **Deactivate** |
| **Broker runtime** | What is actually running. The group reads **`Project: Sample`** (or whatever project is live), or **`Standalone`** when none is. Click the name to open the inspector table |
| **Configuration** | `Variables`, `Secrets`, and `Internal Rules` — the broker's locked built-in permission rules |
| **Library** | Visual components, documents, and images — see [Library](#library-visual-components-and-documents) |
Editing a notebook changes your local copy. Only deploying, Push, Activate, or Deploy project writes to the broker. A colleague on another machine sees the same Broker runtime and the same on-broker projects, not your unpublished local edits — which is why you Push and Pull.
### Broker runtime
Expanding **Broker runtime** groups everything running by type — `Actions`, `Models`, `Rules`, `Routes`, `Python`, `Themes`, `Panels` — each with a count and names beneath. Click **`Project: {name}`** or **Standalone** to open the inspector:
The inspector is where mixed ownership is obvious: Sample-owned rows point at `notebook.lotnb`; a standalone route such as `BrokerAgent` leaves **Project** and **Notebook** blank. Select a row to read the running source and use **Open notebook**, **Copy to project**, or **Remove**.
A project's own **Entities** folder (under the project in **Projects**) is what that project's notebooks *declare*. Broker runtime is what is *running*. In the Sample notebook those counts can disagree — **Routes (1)** under the project versus **Routes (2)** on the broker — because a leftover such as `BrokerAgent` is running but not declared in the notebook.
An idle project's declarations can match the running set and still read `NOT RUNNING` on cells — Activate that project before treating those badges as a deploy to-do.
**Routes and Python scripts deploy from the editor too.** Routes still have their own [Routes Manager](/hub/routes/overview) for connection status and templates.
Two counts are easy to misread:
* **`Rules`** under a project counts rules that project owns. Locked built-ins live under **Configuration → Internal Rules**, not here.
* **`Python`** counts *scripts*. [LoT Flow](/hub/lot-flow)'s Python group counts the individual *functions* those scripts expose.
## Library: Visual Components and Documents
**Library** is the bottom of the LoT Editor sidebar. It is how you put *views* and *files* into a project — not a second editor.
### Components
**Components** is the palette of live widgets you can drop onto a **markdown** cell. Each row is a type (Gauge, Chart, Button, Map, …) with its group on the right. Drag a row onto a markdown cell to embed a viewer bound to an MQTT topic. The Sample notebook's live preview is this capability in use: a card of gauge, status, and trend sitting in the document.
Types you can drop today, by group:
| Group | Components |
| -------------------- | ------------------------------------------------------------------------------------ |
| **Display** | Gauge, Indicator, Value, Text, Header |
| **Containers** | Card, Frame |
| **Data viz** | Chart, Realtime Chart, Table, List |
| **Input** | Button, Switch, Toggle, Slider, Input, Text Field, Number Field, Select, Date Picker |
| **Geo / spatial** | Map, Markers, Trace |
| **Media / graphics** | SVG, Image, Video, Camera, Audio, Barcode |
Widgets in the notebook publish as panels when the document is saved, so they are the same component types Dashboards uses — authored next to the LoT that feeds them.
### Resources: documents and images
**Resources** nests **Documents** and **Images**. These are files in the project, not a separate scrapbook you have to remember to Push.
| Item | What it holds | How you add it | What drop onto markdown does |
| ------------- | ------------------------------------------------------- | ----------------------------------------------------------------------------- | --------------------------------- |
| **Documents** | `.txt`, `.md`, and `.pdf` files in the **open project** | **Add file…**, or project / folder **Import file…** | Inserts a relative markdown link |
| **Images** | Project images (PNG, JPEG, SVG, GIF, WebP, and similar) | **Add file…** under Images, **Import file…**, or drag an image onto a project | Inserts a relative markdown image |
Click a document or image in Library to open it in the main pane (it replaces the notebook until you go back). Hover **Remove** on a Library leaf to delete that file from the list.
Right-click a project or folder for **Import file…** when you want the file to live under a specific folder rather than the Library picker. Dropping a Library image onto a project copies it into that project.
## Standalone Deployments vs Projects
The broker can run LoT **with** a live project, or **without** one. Broker runtime tells you which:
| Mode | Broker runtime label | What it means |
| ---------------- | ----------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| **Live project** | **`Project: Sample`** (the live project's name) | At most one project is **LIVE**. Deployments from that project auto-commit. **Activate** / **Deactivate** on the project row choose which stored project is active |
| **Standalone** | **`Standalone`** | No project is loaded. Entities can still be running — you deployed cells with nothing live, or leftovers that no project owns. Auto-commit is off |
Standalone is a normal state: a scratchpad, the view after **Deactivate**, or entities deployed outside a project (another editor, an older session, or a cell run with nothing LIVE).
How the two interact:
* **Run cell / Deploy All** with nothing LIVE writes straight into standalone runtime. The toolbar stays a direct action.
* **Deploy All** from an idle project on the broker asks you to **Activate** first — it does not silently load that project.
* **Deploy All** from a local-only draft while another project is LIVE is refused (the toast points you at **Push**). Deploying those cells would write into the other live project.
* **Activate** loads a stored project's declarations. If the broker publishes entity ownership, only that project's entities stop; standalone leftovers stay. Without ownership, the confirm says the current project's entities are replaced.
* **Deactivate** unloads the live project and **removes its entities from running**. Auto-commit stops until you Activate again. Broker runtime then reads **Standalone** (plus anything that was never owned by that project). The confirm is explicit: *Deactivating **BMS** unloads it from the broker and **removes its entities from running**.*
* **Adopt into project** (Broker runtime context menu) writes selected running entities into a notebook and Pushes — that is how you turn a standalone set into a project.
## Moving Work Between Projects and the Broker
Right-click a project under **Projects** for operations on that row. A **LIVE** project and an idle on-broker project do not offer the same items (idle has **Inspect** and **Activate**; live has **Deactivate** and the full create/import set).
| Item | What it does |
| ---------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| **`New notebook`** / **`New folder`** | Add a `.lotnb` file or a folder inside the project |
| **`Push to broker`** | Send the whole project — every notebook and folder — to the broker. This is what links a local project to a broker project and puts it under version control |
| **`Pull from broker`** | Replace your local copy with the broker's. If you have unsaved local changes, a **"Pull from broker?"** prompt warns you first |
| **`Version history`** | Open the broker's git history for this project |
| **`Import .lotnb`** / **`Import file…`** | Bring a notebook or other file from outside into this project |
| **`Inspect`** | Download the broker's copy of an idle on-broker project into the editor (does not Activate) |
| **`Activate`** / **`Deactivate`** | Load or unload the stored project as the live one — see [Standalone vs projects](#standalone-deployments-vs-projects) |
| **`Rename`** | Rename the local project |
| **`Delete local copy`** | Delete the HUB notebooks only |
| **`Remove project from broker`** | Permanently delete the project from the broker |
Click **`Project: {name}`** to open the inspector table (status, type, name, owning project, notebook). A row with empty **Project** / **Notebook** is a standalone leftover — see [Standalone vs projects](#standalone-deployments-vs-projects). Right-click **Broker runtime** for operations on the running set:
| Item | What it does |
| ---------------------------------------- | ----------------------------------------------------------------------- |
| **`Adopt into project`** | Write the selected (or all) running entities into a notebook, then Push |
| **`Open in LoT Flow`** | Open the live graph of this runtime |
| **`Download all entities to notebook…`** | Copy running definitions into a `.lotnb` file |
| **`Remove all entities from broker…`** | Undeploy the running set. Internal Rules stay |
## Version History
Broker projects are git repositories, and the editor exposes enough of that to recover from mistakes without a terminal:
The dialog reports the project's branch and current commit, lists recent commits with their messages, and shows any uncommitted changes on the broker. From here you can:
* **Restore a commit** — checks that commit out on the broker and reloads the workspace if the project is active
* **Checkout a branch, tag, or commit** — type a ref and check it out
* **Pull** and **Push** — synchronise with the broker's configured git remote
**Entity changes auto-commit while the project is active.** You do not commit by hand — deploying an entity produces a commit like `Update route: BacnetPortoHq`, which is why the history reads as a log of deployments.
Restoring a commit can discard uncommitted changes on the broker. Check the **Uncommitted changes** list in the same dialog before restoring.
## Pulling Entities Back Into a Notebook
Deployment is not one-way. Anything running on the broker can be brought back into a notebook, which is how you adopt LoT that was deployed by other means.
Right-click an entity **type group** under **Broker runtime**:
Every type group carries its own pair, and the `Entities` group itself carries **`Download all to notebook…`** and **`Remove all from broker…`** for the whole set. Right-clicking a **single entity** offers **`Open in code editor`**, **`Download to notebook…`**, and **`Remove from broker…`**.
Any download asks **"Choose notebook"** — pick an existing `.lotnb` file in the project, or create a new one — and the definitions arrive as cells you can then edit and redeploy.
## Removal: Four Different Operations
Deleting locally and removing from the broker are different operations with different consequences, and the HUB guards them differently on purpose. Read the dialog: it tells you which one you are in.
| Trigger | Dialog | What it destroys | Guard |
| ------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------- | ------------------------------------------------------------------------------- |
| Workspace project → **`Delete local copy`** | **"Delete project?"** — local notebooks and folders only | Your local copy only | Plain confirm |
| Single entity → **`Remove from broker…`** | **"Remove from broker?"** — *Remove action "SummarizeF2Hvac" from the broker. This cannot be undone.* | One running entity | Plain confirm |
| `Entities` → **`Remove all from broker…`** | **"Remove all from broker?"** — *Remove 3 action(s), 3 model(s), 1 route(s), 1 python script(s), 0 theme(s), and 0 panel(s) from the broker. Project-owned rules are also cleared; built-in and operator custom rules are preserved.* | Everything loaded on the broker | Plain confirm, but it enumerates live counts — read them |
| Broker project → **`Remove from broker…`** | **"Remove project from broker"** — *This permanently deletes **BMS** from the broker — all its files **and its entire git history**. This cannot be undone.* | The project, its files, and every commit | **Type-to-confirm** — the button stays disabled until you type the project name |
**Deleting a workspace project does not touch the broker, and removing a project from the broker does not touch your workspace.** A local delete leaves every entity running and every commit intact; a broker removal leaves your notebooks on disk but destroys the project's git history. Neither is a substitute for the other, and only the broker removal is guarded by type-to-confirm.
The bulk-remove wording also settles a question the sidebar raises: **project-owned rules are cleared, while built-in and operator custom rules are preserved.** That is why the `Rules` count under **Broker runtime** can be small while **Configuration → Internal Rules** lists the broker's locked built-ins separately.
A `.lotnb` file is the safety net worth having before any removal. **`Download all to notebook…`** on the `Entities` group captures everything currently deployed, and **`Export open notebook as .lotnb`** in the header `…` menu writes it out as a file.
## Editing the Same Project From VS Code
The in-HUB editor and VS Code share the same broker-held project and git history: a project pushed from the HUB opens in the VS Code extension and vice versa, and `.lotnb` files move between them directly:
* **`Import .lotnb`** on a project, or **`Import .lotnb file`** in the header `…` menu, brings a notebook from outside into that project
* **`Export open notebook as .lotnb`** takes the open notebook back out
* **`Push to broker`** and **`Pull from broker`** move the whole project, which is how two people using two editors stay on one project
Whichever tool deployed an entity, it appears the same way under **Broker runtime** and is drawn the same way in [LoT Flow](/hub/lot-flow).
***
## Next Steps
See the entities you just deployed as a live dependency graph.
Edit the same broker-held project from VS Code, then Push and Pull to stay in sync.
# LoT Flow
Source: https://docs.coreflux.org/hub/lot-flow
See the Actions, Models, Routes, and topics deployed on your broker as a live dependency graph
## Why Use LoT Flow?
Once you have more than a handful of Actions, Models, and Routes on a broker, the question "what happens when this topic changes?" stops having an obvious answer. Tracing it means opening each definition and following topic names by hand. LoT Flow reads the LoT entities deployed on your broker and draws how they connect — what triggers what, and which topics each one reads and writes — so you can follow the data flow instead of reconstructing it from source.
LoT Flow carries a **`Beta`** badge in its window header. It is available for everyday use, but its controls and layout may still change between releases.
**Like a wiring diagram that draws itself.** You never maintain the diagram — it is rebuilt from whatever is actually running on the broker.
## When to Use It
* **Understanding an inherited project** — See the whole entity graph before changing any of it
* **Answering "what does this topic affect?"** — Follow trigger edges from a topic to the Actions it fires
* **Confirming a deployment** — Check that a newly deployed Action appears, and that it is wired to the topics you intended
* **Finding orphans** — Spot entities that nothing triggers and topics that nothing consumes
* **Reviewing an integration** — See which topics a Route publishes into your namespace and which it writes back out
* **Debugging execution order** — Follow trigger and event edges to understand what runs before what
***
## LoT Flow Reads the Broker, Not Your Workspace
This is the single most important thing to know about LoT Flow: **the graph is built entirely from entities deployed on the connected broker.**
The LoT Editor's sidebar is split into two halves that are easy to conflate:
| Sidebar section | What it holds |
| --------------- | ---------------------------------------------------------------------------------------- |
| **Workspace** | Notebook files local to your browser profile. Editing them changes nothing on the broker |
| **Broker** | `Projects`, `Entities`, `Configuration`, and `Library` — shared server state |
A notebook cell you have written but not yet deployed is marked **`NOT RUNNING`** in the editor, and it will **not** appear in LoT Flow. Deploy it — with **Deploy All**, or by running the individual cell — and it appears. Cells already live on the broker are marked **`DEPLOYED`**.
If an entity you just wrote is missing from LoT Flow, check that it was deployed. **Broker runtime** in the LoT Editor is the authoritative list of what is actually running on the broker, and the graph matches that list.
Because the graph reflects the broker rather than any one editor, it does not matter which tool produced the LoT. Entities deployed from the [LoT Editor in the HUB](/hub/lot-editor) and entities deployed from VS Code appear the same way.
***
## Accessing LoT Flow
Select **LoT Flow** in the HUB dock. The window header carries a **`Beta`** badge. The canvas top-right cluster is the layout chrome:
| Control | What it does |
| ------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **Freshness** (`3m ago`) | How long ago the graph was last built from the broker |
| **Reset layout** | Auto-arranges every node from scratch — use it after collapsing topics, after dragging nodes out of place, or whenever the graph looks tangled. Tooltip: *Auto-arrange LoT Flow from scratch* |
| **Open LoT Editor** | Opens the editor so you can change the entities this graph is drawn from |
Zoom, pan, **Fit View**, and **Download image** sit at the bottom-left of the canvas. Fit the view after Reset layout or after changing grouping — a freshly laid-out graph is often off-centre.
## The Entity Rail
The **Entities** tab groups everything deployed, with a count per group and a **Find entity** search for jumping straight to one by name:
| Group | Contents |
| ----------- | ------------------------------------------------------------ |
| **Actions** | Event-driven logic, each badged with its trigger type |
| **Models** | Data schemas that map topic values into structured documents |
| **Routes** | Connections to external systems, badged with the route type |
| **Python** | The individual Python functions your Actions call |
Selecting an entity focuses it on the canvas and opens its detail panel.
The **Python** group lists the individual *functions* invoked from Actions, so its count is usually higher than the number of Python *scripts* deployed. A single script exposing four functions appears as one entity under **Broker runtime** in the LoT Editor and as four nodes here.
## Reading the Graph
The canvas draws your entities, the topics that wire them together, and the Python they call:
| Node | Meaning |
| --------------- | -------------------------------------------------------------------- |
| **Action** | Event-driven logic, badged with its trigger (for example `ON TOPIC`) |
| **Model** | A data schema, badged `Model` |
| **Route** | An external connection, badged with its type and a health indicator |
| **Python** | A Python function, badged with the script it belongs to |
| **Topic** | An MQTT topic, showing its current value |
| **Topic group** | Many topics sharing a prefix, collapsed into one node with a count |
Edges are colour-coded by relationship — solid lines are live data flow, muted dashed lines are structure. The vocabulary covers `publish`/`sub`, `trigger`, `write`, `event`, `python call`, `dependency` (a value read but not triggered on), `internal state`, `extends`, and grouping links. Turn on the **Legend** from the Settings tab while you are still learning it.
LoT **Rules** are not drawn on the canvas. Rules govern access control rather than moving data, so the graph focuses on Actions, Models, Routes, and Python. Rules deployed on your broker still appear under **Broker runtime** in the LoT Editor.
### How a single Action becomes a subgraph
The Action below reads four topics, calls two Python functions, and publishes a Model to a status topic:
```lot wrap focus={7-12} theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
DEFINE ACTION SummarizeF2Hvac
ON TOPIC "uns/porto/hq/f2/hvac/temp" DO
SET "temp" WITH (GET TOPIC "uns/porto/hq/f2/hvac/temp" AS DOUBLE)
SET "humidity" WITH (GET TOPIC "uns/porto/hq/f2/hvac/humidity" AS DOUBLE)
SET "occupied" WITH (GET TOPIC "uns/porto/hq/f2/hvac/occupied" AS BOOL)
SET "setpoint" WITH (GET TOPIC "uns/porto/hq/f2/hvac/setpoint" AS DOUBLE)
CALL PYTHON "BuildingInsights.comfort_score"
WITH ({temp}, {humidity}, {setpoint})
RETURN AS {score}
CALL PYTHON "BuildingInsights.comfort_label"
WITH ({score})
RETURN AS {label}
PUBLISH MODEL F2HvacModel TO "uns/porto/hq/f2/hvac/status" WITH
temp = {temp}
humidity = {humidity}
occupied = {occupied}
setpoint = {setpoint}
comfort_score = {score}
comfort_label = {label}
timestamp = TIMESTAMP "ISO"
```
On the canvas that becomes one Action node with a trigger edge from `temp`, dependency edges from the three topics it reads, `python call` edges to both `BuildingInsights` functions, and an edge to the Model that publishes the result. You can read the same relationships from the other direction in the [UNS Viewer](/hub/mqtt/uns-viewer), where each topic reports the Actions and Routes that produce and consume it.
## Inspecting an Entity
Select any node, or any entity in the rail, to open its detail panel.
Depending on the entity type, the panel reports:
* **Its trigger** and a live status such as `Receiving data`
* **Inputs and outputs** — the topics it consumes and produces, with counts
* **The deployed LoT source**, syntax-highlighted, so you can read what is actually running
* **Route status** for Route entities, including connection health
* **Trace** — capture this Action's executions to see it run. Traces auto-expire after five minutes
* **Publish a test value** — send a value to one of the entity's topics to exercise it
**Trace answers "did it actually run?"** Rendering a graph proves an Action is deployed and wired up; a trace proves it fired.
## Keeping Large Graphs Readable
The **Settings** tab is the Canvas menu — grouping, look, and collapse live here. Layout itself is the **Reset layout** control at the top-right of the canvas, not in this list.
| Option | What it does |
| --------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **Legend** | Puts a key for node and edge types on the canvas |
| **Minimap** | An overview map. Leave it off on a wide graph — it parks in the bottom-right and can cover real nodes |
| **Topic update flash** | Pulses a topic node when a new value arrives |
| **Edge style** | Smooth step, Bezier, Step, or Straight. This is a persisted preference, not a fixed default |
| **Spacing** | Compact, Comfortable, or Spacious. Also persisted |
| **Group large branches** | Fold sibling topics that share a prefix into one topic-group node with a count |
| **Collapse over** | How many children a branch needs before it groups. `1` groups every sibling set, which is the most readable hero view on a busy namespace. The field accepts `1`–`50` |
| **Collapse all** / **Expand all** | Collapse or expand every grouped branch at once |
After you change grouping, click **Reset layout** (top-right of the canvas) so the remaining nodes are placed again, then **Fit View**. Collapsing without resetting leaves the old positions and a lot of empty space.
Topic grouping is the most effective control on a busy namespace: **Collapse over** `1` folds each floor/device prefix — `…/f2/hvac/` `4/4`, `…/f2/energy/` `2/2` — into one node. The relationships stay on the graph; only the leaf clutter goes.
***
## Next Steps
Explore the topic namespace those entities read from and write to.
Create and deploy the entities that appear in this graph, cell by cell.
# Connected Clients
Source: https://docs.coreflux.org/hub/mqtt/connected-clients
View active MQTT client connections and their status in the Coreflux HUB
## Why View Connected Clients?
You need to know which clients are connected to your broker—devices, applications, or the Coreflux HUB itself. The Connected Clients view shows all active MQTT connections with detailed connection info, traffic statistics, session data, and last activity so you can verify connectivity and troubleshoot disconnections.
## When to Use It
* **Verifying device connections** — Confirm that sensors or gateways are connected
* **Troubleshooting** — Check if a client has disconnected or failed to connect
* **Monitoring** — See how many clients are using the broker and their traffic
* **Debugging** — Inspect session state, pending messages, and last activity timestamps
***
## Connected Clients Interface
The Connected Clients view has two panels: a client list on the left and a detail panel on the right. Select a client to view its connection information.
***
## Client List (Left Panel)
The left panel lists all clients currently connected to the broker. For each client you see:
| Field | Description |
| ----------------------- | --------------------------------------------------------------------------- |
| **Client ID** | Unique identifier for the connection (e.g., `coreflux-controller-0e1e882c`) |
| **Endpoint** | Client IP and port (e.g., `127.0.0.1:62983`) |
| **Connection Duration** | How long the client has been connected (e.g., `40m`) |
| **Total Traffic** | Estimated bytes received and sent combined (e.g., `15.16 MB`) |
| **Protocol** | MQTT version (e.g., `MQTT v4`) |
Select a client to view its detailed information in the right panel.
## Client Details (Right Panel)
When you select a client from the list, the right panel shows four sections:
### Connection Information
| Field | Description |
| -------------------- | ----------------------------------- |
| **Protocol Version** | MQTT version (e.g., MQTT v4) |
| **Source Port** | Client's source port |
| **Connected** | Timestamp when the client connected |
| **Client IP** | Client IP address |
| **Full Endpoint** | IP:port (e.g., `127.0.0.1:62983`) |
| **Duration** | Connection duration (e.g., `40m`) |
### Traffic Statistics
| Direction | Fields |
| ------------ | ------------------------ |
| **Received** | Bytes, Packets, Messages |
| **Sent** | Bytes, Packets, Messages |
Use these to see how much data the client has exchanged with the broker.
### Session Information
| Field | Description |
| -------------------- | --------------------------------------------------- |
| **Session ID** | Session identifier (typically matches Client ID) |
| **Created** | Timestamp when the session was created |
| **Expiry Interval** | Session expiry (e.g., `0s` for no expiry) |
| **Pending Messages** | Number of messages awaiting delivery to this client |
| **Status** | Connection status (e.g., Connected) |
### Last Activity
| Field | Description |
| ------------------------ | ----------------------------------------------------- |
| **Last Packet Received** | Timestamp of the last packet received from the client |
| **Last Packet Sent** | Timestamp of the last packet sent to the client |
| **Last Non-KeepAlive** | Timestamp of the last non-keepalive packet |
## Refreshing the List
The view updates as clients connect and disconnect. Use the refresh control if you need to force an update of the displayed list. The detail panel shows an "Updated" timestamp indicating when the data was last refreshed.
***
## Next Steps
Explore topics and publish messages.
Understand MQTT client connections and sessions.
# Data Viewer
Source: https://docs.coreflux.org/hub/mqtt/data-viewer
Subscribe to topics, view the topic tree, and publish messages from the Coreflux HUB
## Why Use the Data Viewer?
You need to see what data is flowing through your broker—which topics are active, what payloads look like, and how to send test messages. The built-in Data Viewer in the Coreflux HUB lets you subscribe to topics, browse the topic tree, and publish messages without installing an external MQTT client.
**An MQTT data viewer built into your Coreflux.** Designed to feel as intuitive as the popular MQTT Explorer desktop app — but with zero installation, right inside the HUB.
## When to Use It
* **Viewing device data** — Subscribe to sensor or device topics to see live readings (temperature, status, etc.) as they arrive
* **Exploring your topic structure** — Browse the Topic Tree to understand which topics exist and how they are organized
* **Verifying Actions** — Subscribe to output topics to confirm your LoT Actions are publishing correctly
* **Debugging** — Inspect payloads and topic structure in real time
* **Testing** — Publish messages to trigger Actions, test Routes, or simulate device data
* **Monitoring broker activity** — Watch `$SYS` topics to check broker health, connected clients, and message throughput
***
## Accessing the Data Viewer
1. In the HUB top navigation, select **MQTT**, then **Data Viewer**.
2. The Topic Tree appears on the left, showing topics as they receive messages.
3. Select a topic to view its payloads in the right panel.
## Topic Tree
The Topic Tree shows the hierarchical structure of topics. Topics appear as messages are received. Expand branches to navigate and select a topic to view its payload history in the payload viewer.
## Exploring Live Data
The Data Viewer is laid out as three vertical panels: **Publish** on the left, the **Topic Tree** in the middle, and a **Detail area** on the right where you build a personal view of the topics you care about right now. You can drag the divider between the tree and the detail area, and collapse the Publish panel into a thin rail to give yourself more room.
**Like an instrument panel for your broker.** Instead of reading rows of raw messages, you pin the topics that matter to the right side and watch them update — values, charts, lamps, even maps — in real time.
### Previewing a topic
Get a feel for what is happening on the broker without committing to a layout. Click any topic in the tree and its latest payload appears in the Detail area as a quick preview.
Each row in the tree carries a small **type icon** that tells you at a glance what kind of value the topic currently holds:
| Icon | Meaning |
| ------------------ | --------------- |
| Glowing orange dot | Boolean `true` |
| Empty dot | Boolean `false` |
| `#` | Number |
| `{}` | JSON object |
| `[]` | JSON array |
| `"` | Plain string |
Rows briefly **flash orange** whenever a new message arrives, so you can see broker activity without staring at every value. To narrow the list, type any part of a topic path into the **Filter** box at the top of the tree — the filter matches anywhere in the path, and the usual display cap is lifted while you are filtering. For broader monitoring, you can also subscribe with a wildcard pattern such as `sensors/#`, and every matching topic appears in the tree as messages come in.
### Tracking what matters
Once a topic is worth watching, **pin it**: double-click the topic, or drag it from the tree onto the Detail area. A card appears for it, and the card shape adapts to the payload so you don't have to mentally decode raw values:
* **Numbers** render as a large value with a **sparkline** of recent history — perfect for sensor readings like temperature or RPM.
* **Booleans** render as a glowing **lamp** — green when `true`, dim when `false`.
* **JSON and text** render with colored keys and an expandable tree, so nested objects stay readable.
* **Geolocation** payloads (latitude/longitude) render as a small **map** with the current point and a trail of recent positions, plus the raw payload underneath.
Cards can be reordered by dragging them up or down, copied to the clipboard with one click, and removed with the **X** in their corner. Add more topics any time from the dashed **+ Add topic** button at the bottom of the cards, which opens a searchable list of every known topic.
### Replaying history with the timeline
As soon as you are tracking topics with more than one message of history, a **timeline** appears at the top of the Detail area. Every tracked topic gets its own color, and every message it has received is drawn as a colored mark on a horizontal track.
From there you can:
* **Click or drag** anywhere on the track to scrub to a moment in time — every card below jumps to the values they had then.
* Step message-by-message with **First / Previous / Next / Last**.
* Hit **Play** to replay the message history like a video, with cards animating through every value.
* Tap **Live** to snap back to the latest message and resume real-time updates (the label turns green when you are caught up).
* Open the gear icon to change the **history buffer** per topic, from 50 up to 2 000 messages — larger buffers give you more scrub range.
While the viewer is focused, the arrow keys step through messages, **Home** jumps to the first, and **End** jumps back to live.
### Drilling into payloads
When a payload is too big to read inline, click its preview in the tree to open the **payload inspector** — a floating window that parses the full payload and lets you expand JSON to any depth. If the payload contains coordinates, the inspector also draws a small map at the top.
For payloads where you only care about one number, drag a single value out of the inspector (or out of a card's expanded JSON tree) onto the Detail area. That value becomes its own **extraction card** — a small card that tracks just that one field, sparkline included if it's numeric. It's the fastest way to follow `temperature` from a 30-field sensor payload without the surrounding noise.
### Drag-and-drop cheatsheet
Most of the viewer is built around dragging. While you are dragging, valid drop zones light up so you can't miss them.
| Drag this | Onto this | Result |
| --------------------------- | -------------------------------- | ------------------------------------------------ |
| A topic in the tree | The Detail area | Adds a tracked card for that topic |
| A topic in the tree | The timeline's topic chips | Adds the topic to the timeline only |
| A topic in the tree | The Publish panel's Topic input | Pre-fills the publish topic |
| A topic in the tree | The Publish panel's Payload area | Pastes the topic's current value |
| A JSON value from a payload | The Detail area | Creates an extraction card for that single field |
| A JSON value from a payload | The Publish panel's Payload area | Pastes just that value |
## Publishing Messages
To publish a test message:
1. Select or enter the target topic.
2. Enter the payload (plain text or JSON).
3. Click **Publish** to send the message.
Use this to trigger Actions, test route configurations, or simulate device data.
***
## Next Steps
See which clients are connected to your broker.
Learn how MQTT topics and wildcards work.
# MQTT Overview
Source: https://docs.coreflux.org/hub/mqtt/overview
Browse topics, publish messages, and see who's connected — all from your Coreflux HUB
## Your MQTT Dashboard
Welcome to your built-in MQTT toolbox. From here you can browse every topic on your broker, publish test messages, and check which clients are online — all without leaving the Coreflux HUB. Two widgets are ready for you: **Data Viewer** for topics and messages, and **Connected Clients** for monitoring connections.
**No need to install external tools** — the Data Viewer is built right into the Coreflux HUB.
## Pick the Right Widget
| Widget | You want to... |
| --------------------- | --------------------------------------------------------- |
| **Data Viewer** | Browse topics, inspect payloads, or publish test messages |
| **Connected Clients** | See who's connected and check connection health |
***
## Next Steps
Subscribe to topics, browse the topic tree, and publish messages.
View active MQTT client connections and their status.
# UNS Viewer
Source: https://docs.coreflux.org/hub/mqtt/uns-viewer
Explore your broker's topic namespace as a live hierarchy graph in the Coreflux HUB
## Why Use the UNS Viewer?
You have hundreds or thousands of topics on your broker and you need to understand their *shape* — how the namespace is organised, which levels exist, and where a given device or metric sits in the hierarchy. Reading that structure from a flat list of topic strings is slow and error-prone. The UNS Viewer draws your broker's namespace as a hierarchy graph, so you can see the structure at a glance instead of reconstructing it in your head.
The UNS Viewer carries a **`BETA`** badge in its window header. It is available for everyday use, but its controls and layout may still change between releases.
**Like a map of your factory instead of a list of street names.** A topic list tells you what exists; the namespace graph shows you how it all fits together.
## When to Use It
* **Understanding an unfamiliar broker** — Open the namespace and read the hierarchy top-down instead of guessing at topic conventions
* **Reviewing your namespace design** — Check that a new site, line, or device was published under the level you intended
* **Finding where a metric lives** — Search for a name and see which branch of the hierarchy it belongs to
* **Tracing a topic back to your logic** — Each leaf reports which Actions and Routes produce or consume it
* **Comparing several topics at once** — Select multiple topics and watch their values and history side by side
* **Spotting activity** — Live pulses show which parts of the namespace are actually receiving traffic
***
## Accessing the UNS Viewer
Select **UNS Viewer** in the HUB dock. The window opens with the namespace already built from the topics the HUB has seen, and a `N topics` badge in the top-right corner reports how many leaf topics are currently in scope.
## Reading the Graph
The canvas draws two kinds of node:
| Node | Shape | Meaning |
| ------------------ | ------- | -------------------------------------------------------------------------------------------------------------------- |
| **Namespace node** | Square | A level in the hierarchy. Carries a count of everything beneath it, and a chevron to expand or collapse its children |
| **Leaf topic** | Rounded | An actual MQTT topic. Shows its current value inline |
Colour and motion carry meaning too, and the **Legend** toggle puts a key on the canvas while you are learning the vocabulary:
* **Live value** — a green pulse means a message just arrived on that topic
* **Activity pulse** — a green glow on a namespace node shows aggregated traffic somewhere below it
* **Search match** — an amber ring marks topics matching your current search
Only leaf topics carry payloads. Selecting a namespace node reports `No message at this time`, because the values live on its children.
## Navigating the Namespace
The **NAMESPACE** section of the left rail controls how much of the tree is drawn:
| Control | What it does |
| --------------------------------- | -------------------------------------------------------------------------------------------------- |
| **Root filter** | An MQTT filter that scopes the whole view, `#` by default. The `N topics` badge follows the filter |
| **Include \$ topics** | Brings broker-internal `$SYS` and `$Visu` topics into the graph, which are hidden by default |
| **Expand depth** | How many levels are expanded when the graph is built. Defaults to `3` |
| **Collapse all** / **Expand all** | Collapse the whole tree, or expand every level at once |
The **Root filter** applies when you press **Enter** — typing a filter alone does not redraw the graph.
On a large namespace, narrowing the root filter to a subtree such as `uns/porto/hq/f1/#` is the fastest way to keep the graph readable.
The **Search topics** box highlights matching topics in place with an amber ring rather than filtering the graph, so you keep the surrounding structure as context while you locate something.
### Canvas options
The **CANVAS** section changes how the graph is drawn without changing what it contains:
| Option | Choices |
| -------------- | ----------------------------------------------------------------- |
| **Legend** | Show or hide the node and edge key |
| **Minimap** | Show or hide an overview map for staying oriented in large graphs |
| **Edge style** | Smooth step, Bezier, Step, or Straight |
| **Spacing** | Compact, Comfortable, or Spacious |
Zoom, pan, and a **fit-view** control sit at the bottom-left of the canvas. Fit the view after expanding or filtering — a freshly laid-out graph is often off-centre.
***
## Inspecting a Topic
Select a leaf topic to open the inspector on the right. It reports the topic's breadcrumb path, its live value, its history, and how it relates to the LoT running on your broker.
The inspector gives you:
* **A value card** with the current value and a **sparkline** of recent history
* **A message histogram** with the start and end timestamps of the buffered history
* **Playback controls** — step to first, previous, next, or last message, or play the history back
* **A `Live` toggle** that snaps to the latest message and resumes real-time updates
* **LoT cross-references** — which Actions the topic triggers, which reference it, and which Route produces it
Those cross-references are the bridge between your namespace and your logic. A topic written by a Route and consumed by an Action shows all three relationships, and a topic that nothing uses reports `Not referenced by any action or route.`
Cross-references answer "is anything actually using this topic?" — useful when cleaning up a namespace, or when confirming that a newly deployed Action really is wired to the topic you intended.
### Comparing several topics
Turn on **multi-select** in the top-right corner to keep more than one topic selected. Each selection becomes a removable chip carrying its own message count, the inspector header counts them, and the histogram merges every selected topic onto one timeline so you can line their behaviour up against each other. Press `Esc` to leave multi-select.
With multi-select off, a plain click replaces the current selection, while `Shift`, `Ctrl`, or `⌘` click adds to it.
***
## Namespace Best Practices
The UNS Viewer makes the consequences of namespace design visible, and the shape it draws is only as good as the topics you publish:
* **Keep the hierarchy consistent** — model your physical or organisational structure, so the graph reads like your plant
* **Use lowercase, stable, descriptive segment names** — and avoid renaming a level once it is in use
* **Publish state as retained messages** so late subscribers, including this viewer, get the current value
* **Prefer descriptive leaf metrics** over opaque blobs that have to be parsed downstream
* **Narrow the root filter** on large namespaces rather than fighting a graph with thousands of nodes
The viewer builds its tree from the topics the HUB has observed. A topic that has never published, and holds no retained value, cannot appear in the graph.
***
## Next Steps
See the Actions, Models, and Routes behind those topics as a dependency graph.
Read individual payloads, track topics as cards, and publish test messages.
# Coreflux HUB Overview
Source: https://docs.coreflux.org/hub/overview
Manage your Coreflux broker—explore MQTT topics, edit LoT code, configure routes, and monitor system health from the browser or the standalone desktop app
## Why Use the Coreflux HUB?
You need a single place to manage your broker without installing extra tools or connecting via the command line. The Coreflux HUB gives you an interface to explore topics, create and edit LoT (Language of Things) definitions, configure routes, and monitor system status—all from one dashboard. It is available as a browser app (its own container, via Docker Compose) or as a standalone desktop application for Windows and Linux.
The Coreflux HUB is a **separate container** from the broker — it is not bundled in the broker image. Run it with **Docker** or **Docker Compose** (open `http://localhost:3000`) or as a **standalone desktop application** for Windows x64, Linux x64, and Linux ARM64. See [Installation](/latest/quick-start/installation) for Docker commands, Compose profiles, the HUB desktop app, and the login flow.
**Like a control panel for your broker.** The HUB puts Data Viewer, LoT Editor, route management, and system tools in one place so you can deploy and debug without extra tooling.
## When to Use the HUB
* **Manage everything from the HUB** — Configure your broker, deploy logic, and set up data routes without installing desktop tools
* **Go from idea to deployment in minutes** — Write and deploy Actions, Models, and Rules directly in the built-in editor
* **Monitor your system in real time** — See device connections, data flow, and system health at a glance
* **Diagnose issues fast** — Inspect live MQTT messages and run broker commands from one interface
## Prerequisites
Install the HUB (and the broker, if you do not already have one) from [Installation](/latest/quick-start/installation). After `docker compose --profile full up`, open `http://localhost:3000`, click **New Connection**, and set **Host** to `broker` (**Protocol** `mqtt://`, **Port** `1883`). The desktop app is on the **HUB desktop** tab of that page.
## How the HUB Is Organized
The HUB is built around five apps. Each app groups related tools so you can switch between tasks without leaving the browser.
Explore topics, publish messages, and view connected clients.
Create and edit Actions, Models, and Rules in the browser.
Build live panels with gauges, charts, and controls bound to broker data.
Add, configure, and monitor routes with templates or LoT code.
Broker configuration, user management, trace logs, and command console.
### MQTT
Browse live data flowing through your broker. The MQTT app includes a **Data Viewer** for subscribing to topics and publishing test messages, and a **Connected Clients** view to see which devices are online.
### LoT Editor
Write and deploy logic directly in the HUB. The LoT Editor is a notebook editor: cells hold LoT, Python, or Markdown, each cell deploys on its own, and the sidebar lists everything currently running on the broker — no external tools required.
### Dashboards
Design operator-facing screens from a component library—gauges, charts, industrial symbols, and controls—and bind each widget to live data on your broker. The **Dashboard Manager** lists your panels; open one to preview or use it full screen for walls and shared links.
### Routes
Connect your broker to databases, PLCs, and external systems. The Routes app shows each route as a card with a live **status indicator**, and lets you configure routes visually or in LoT code.
### System
Administer the broker itself. The System app provides **Broker Configuration**, **User Management**, a **Command Console** for direct broker commands, and **Broker Logs** for real-time diagnostics.
***
## Next Steps
Run the HUB with Docker, Compose, or the desktop app — then connect from New Connection.
Save multiple brokers, secure them with TLS, and switch between them.
Subscribe to topics and publish messages from the browser.
# Route Configuration
Source: https://docs.coreflux.org/hub/routes/configuration
Edit route configuration, events, mappings, and tags in Visual or LoT mode
## Why Configure Routes in the Coreflux HUB?
You need to adjust route settings—connection parameters, events, mappings, and tags—without editing files or publishing MQTT commands. The Route Configuration in the HUB lets you edit routes with a form-based Visual Mode or switch to LoT Mode to modify the raw LoT code by hand.
## When to Use It
* **Updating connection settings** — Change IP, port, credentials, or other config fields
* **Adding events or mappings** — Extend a route with new events, mappings, or tags
* **Switching modes** — Use Visual Mode for guided editing or LoT Mode for full control
For an overview of how to add routes, select route types, and use templates, see [Routes Overview](/hub/routes/overview).
***
## Visual Mode vs LoT Mode
| Mode | Best For |
| --------------- | ----------------------------------------------------------------- |
| **Visual Mode** | Form-based editing with add/remove fields, modals, and validation |
| **LoT Mode** | Direct LoT code editing for power users who prefer hand-editing |
Toggle between modes using the **Visual Mode** / **LoT Mode** toggle at the top of the route configuration panel. Changes in either mode are applied when you click **Save Changes**.
### LoT Mode
Switch to **LoT Mode** to view and edit the full route definition as LoT code. This gives you direct control over the syntax and is useful for copying configurations or making bulk changes.
### Renaming Elements
You can rename routes, events, mappings, and tags directly from the configuration panel. Click the **pencil icon** next to any element name to turn it into an editable text field, type the new name, and press **Enter** to confirm. A **Renamed** badge appears briefly to confirm the change.
This works the same way for all element types:
* **Route name** — Displayed at the top of the configuration panel
* **Event name** — Displayed in the Events tab header for each event
* **Mapping name** — Displayed in the Mappings tab header for each mapping
* **Tag name** — Displayed in the tag header within a mapping
Renaming takes effect when you click **Save Changes**. If you cancel without saving, the original names are restored.
***
## Configuration Tab
The **Configuration** tab shows the route's connection and protocol settings (e.g., `S7_CONFIG`, `SQL_CONFIG`, `MODBUS_CONFIG`). Each field displays its name, whether it is required or optional, its data type, and a description.
* **View fields** — See all configuration fields with their types and descriptions
* **Edit field** — Change values in the input fields
* **Add field** — Click **+ Add Field** to add optional fields from a list of available parameters
* **Delete field** — Use the delete icon on a field to remove it
### Adding a Configuration Field
Click **+ Add Field** to open the **Add Configuration Field** modal. This shows all optional fields available for the route type, along with their data types and descriptions.
***
## Events Tab
The **Events** tab manages event definitions for the route. Events define how the route reacts to MQTT messages—typically by reading from or writing to the connected device or service.
* **Add Event** — Click **+ Add Event** to create a new event with a name and fields
* **Rename event** — Click the pencil icon next to the event name to rename it
* **Edit event** — Expand an event to view and edit its fields (e.g., `SOURCE_TOPIC`, `DESTINATION_TOPIC`, `QUERY`)
* **Add field** — Use **+ Add Field** within an event to add optional fields
### Adding an Event Field
Click **+ Add Field** inside an event to open the **Add Event Field** modal. This shows the optional fields available for the event.
***
## Mappings Tab
The **Mappings** tab manages mapping definitions and tags. Mappings define periodic data polling from a connected device—reading tags at a set interval and publishing the data to MQTT topics.
* **Add Mapping** — Click **+ Add Mapping** to create a new mapping with a name, mapping fields, and tags
* **Rename mapping** — Click the pencil icon next to the mapping name to rename it
* **Edit mapping** — Expand a mapping to edit its configuration (e.g., `EVERY`, `SOURCE_TOPIC`) and tags
* **Add Tag** — Use **+ Add Tag** within a mapping to add tags with address, data type, and other properties
### Adding a New Mapping
Click **+ Add Mapping** to open the **Add New Mapping** modal. Enter a mapping name, then use **+ Add Field** and **+ Add Tag** to build the mapping.
### Adding a Mapping Field
Click **+ Add Field** inside a mapping to open the **Add Mapping Field** modal. This shows the optional fields available for the mapping, such as `DESTINATION_TOPIC` and `QUERY`.
#### Tags
Tags are defined inside mappings. Each tag represents a data point to read from the connected device. You can add, edit, rename, and remove tags and their properties (address, address type, data type, scaling, unit, etc.) from the Mappings tab. To rename a tag, click the pencil icon next to the tag name.
#### Saving and Canceling
* **Save Changes** — Applies all edits to the broker
* **Cancel** — Discards unsaved changes and closes the configuration panel
***
## Next Steps
Create new routes with templates or LoT code.
View and export route-specific logs and status.
# Routes Monitoring
Source: https://docs.coreflux.org/hub/routes/monitoring
View real-time route status and logs — monitor connection health, message counts, and export logs
## Why Monitor Routes?
You need to know whether a route is running correctly — whether it is connected, how many messages it has processed, and whether there are errors. The Monitor window gives you live status metrics and real-time logs filtered to a single route, so you can troubleshoot without searching through broker-wide output.
## When to Use It
* **Verifying connectivity** — Confirm that a route is connected to its target system after creation or configuration changes
* **Tracking activity** — Check message counts and error rates to validate that events and mappings are executing
* **Troubleshooting connections** — See why a route failed to connect using status details and log entries
* **Exporting for analysis** — Download filtered logs as JSON, CSV, or LOG for offline review
***
## Accessing the Monitor
Open the Routes Manager, click on a route card, and select **Monitor**. The Monitor window opens with two tabs: **Status** and **Logs**.
***
## Status Tab
The Status tab displays live metrics from the broker for the selected route.
| Field | Description |
| -------------- | ---------------------------------------------------------- |
| **Connection** | Whether the route is currently connected to its target |
| **Health** | Health indicator (Green, Yellow, Red) based on error rates |
| **Messages** | Total messages processed, with success and failure counts |
| **Errors** | Total and consecutive error counts |
The **Activity Timeline** shows timestamps for last activity, last success, and last error. The **Advanced: Raw JSON** section shows the full status payload from the broker, including uptime and connection timestamps.
***
## Logs Tab
The Logs tab shows real-time log entries filtered to the selected route.
### Filter by Level
Use the filter buttons to show only certain log levels:
| Filter | Description |
| --------- | --------------------------- |
| **ALL** | Show all log entries |
| **FATAL** | Fatal errors only |
| **ERROR** | Errors only |
| **WARN** | Warnings only |
| **INFO** | Informational messages only |
### Log Entry Fields
Each log entry displays:
| Field | Description |
| --------------- | ------------------------------------ |
| **Timestamp** | When the event occurred |
| **Log level** | FATAL, ERROR, WARN, or INFO |
| **Category** | Event category (e.g., Action, Event) |
| **Name** | Event or action name |
| **Outcome** | Success or failure |
| **Description** | Detailed message or structured data |
### Export
Use the export dropdown to choose a format:
* **JSON** — Structured export for parsing or integration
* **CSV** — Spreadsheet-friendly format
* **LOG** — Plain text log file
Click **Export** to download the filtered logs.
### Clear Logs
Use **Clear Logs** to remove displayed entries from the view. This does not affect the broker — it only clears the HUB display.
***
## Next Steps
Edit route settings if status or logs show connection or configuration issues.
View broker-wide logs for system-level troubleshooting.
# Routes Overview
Source: https://docs.coreflux.org/hub/routes/overview
Add, configure, and monitor Routes in the Coreflux HUB—view status, edit configuration, and inspect route logs
## Why Use the Routes Section?
You need to manage Routes from the browser—add new routes, edit configuration, check connection status, and troubleshoot with logs. The Routes section of the Coreflux HUB lets you configure routes with a visual editor or LoT code, view connected and disconnected routes, and inspect route-specific logs.
## When to Use It
* **Adding routes** — Create routes from templates (PostgreSQL, MongoDB, Modbus, etc.) or write LoT code directly
* **Configuring routes** — Edit configuration, events, mappings, and tags in Visual or LoT mode
* **Monitoring** — See which routes are connected and which are not, and view real-time status metrics
* **Troubleshooting** — View route logs, filter by level, and export for analysis
***
## Routes Tools
Edit routes, view status (connected/not connected), and delete routes.
View real-time status metrics and logs filtered by route, export as JSON/CSV/LOG.
***
## Route List and Status
The Routes Manager displays all defined routes as cards. Each route card shows the route name, type (e.g., `SIEMENS_S7`), and a **status indicator** dot in the top-right corner. The dot color tells you the current connection state at a glance:
| Color | Status |
| ---------- | -------------- |
| **Green** | Connected |
| **Red** | Not connected |
| **Gray** | Unknown status |
| **Yellow** | Reconnecting |
Use the controls at the top to **sort** routes by name and **filter** by type. The **Delete All** button removes all routes, and the **Add a route +** button opens the route creation options.
## Route Card Actions
Click on a route card to open its context menu with three options:
| Action | Description |
| ----------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **Configuration** | Opens the [Routes Configuration](/hub/routes/configuration) panel to edit settings, events, mappings, and tags |
| **Monitor** | Opens the [Routes Monitoring](/hub/routes/monitoring) window with a **Status** tab (connection, health, messages, errors) and a **Logs** tab (real-time route logs) |
| **Documentation** | Opens the Coreflux documentation for the route type directly inside the HUB |
### Route Documentation
The Documentation option opens the Coreflux docs page for the route type in an embedded viewer inside the HUB. This gives you quick access to syntax reference, configuration parameters, and examples without leaving the interface.
***
## Adding a Route
Click **Add a route +** in the Routes Manager. The **Create a New Route** modal appears with two options:
* **Start from Scratch** — Opens the LoT code editor where you write your route definition from scratch
* **Start from Template** — Opens the template list where you pick a route type and configure it visually
| Method | Best For |
| ----------------------- | ------------------------------------------------------------------- |
| **Start from Scratch** | Custom routes, full control over syntax, copying from documentation |
| **Start from Template** | Quick setup for common route types with a visual configuration form |
### Start from Scratch
Select **Start from Scratch** to open the **Create Route with Code Editor** modal. Write your route definition in LoT language and click **Create Route** to deploy.
Example:
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
DEFINE ROUTE MyPostgres WITH TYPE POSTGRESQL
ADD SQL_CONFIG
WITH SERVER "localhost"
WITH PORT '5432'
WITH DATABASE "iot_data"
WITH USERNAME "admin"
WITH PASSWORD "your_password"
ADD EVENT StoreSensor
WITH SOURCE_TOPIC "sensors/#"
WITH QUERY "INSERT INTO readings (topic, value) VALUES ('{topic}', '{value.json}')"
```
### Start from Template
Select **Start from Template** to open the templates list. Choose a route type to configure it visually.
1. Select **Start from Template** from the Create a New Route modal.
2. The **Templates** list opens with available route types.
3. Use **Search by type...** to filter (e.g., "PostgreSQL", "Modbus").
4. Select a template. The route creation view opens with a configuration form.
When you select a template, a configuration tab opens with **Visual Mode** and **LoT Mode**. Visual Mode displays all configuration fields and events as a form. LoT Mode shows the raw LoT code.
* **Configuration** — Fill in the connection fields (server, port, database, username, password). Required fields are marked with a badge. Use **+ Add Field** to include optional parameters.
* **Events** — Define events for the route (e.g., queries triggered by MQTT messages).
* **LoT Mode** — Switch to LoT Mode to view or edit the generated LoT code directly.
5. Fill in the configuration fields and add events as needed.
6. Click **Create Route** to deploy. Click **Cancel** to discard.
Templates are available for all supported route types. See the [Routes Overview](/latest/lot-language/routes/overview) for the full list of available types and their documentation.
***
## Next Steps
Edit routes with Visual or LoT mode.
Learn route syntax and available route types.
# Welcome
Source: https://docs.coreflux.org/latest/index
Get, centralize, process, and send IoT data - all from one MQTT Broker with built-in LoT (Language of Things)
# Welcome to Coreflux
Tired of stitching together MQTT brokers, message queues, transformation scripts, and database connectors? Coreflux handles it all in one place. **Get** data from devices, **centralize** it in your broker, **process** it with simple scripts, and **send** it wherever it needs to go.
**"You can do a LoT"** - The complete IoT data pipeline for developers who want to ship faster.
## Quick Navigation
Go from zero to your first LoT Action in under 5 minutes.
Install Coreflux with Docker, Docker Compose, or a native binary.
Manage your broker from the browser—explore topics, edit LoT, configure routes.
Learn the Language of Things - Actions, Models, Rules, and Routes.
Download the MQTT Broker from the Coreflux website — use **Start Free**.
## What is LoT?
Think of LoT as **SQL for real-time IoT data**.
**Just like SQL lets you query databases without writing low-level code**, LoT lets you process MQTT messages without deploying external services. Write simple scripts, get instant results.
Instead of deploying separate microservices to process, transform, and route your IoT data, write simple scripts that run inside the broker. No containers to orchestrate. No message queues to manage. Just logic that executes the moment data arrives.
Learn the fundamentals of the Language of Things and how it works.
React to events and publish data. "When temperature exceeds 80°, send an alert."
Transform and structure data. Convert raw bytes to clean JSON with context.
Connect systems. Stream data to PostgreSQL, MongoDB, REST APIs, and more.
## Why Developers Choose Coreflux
No microservices, containers, or orchestration for basic automation. One binary, full functionality.
Logic executes on publish, not on poll. Sub-millisecond response times.
Everything runs inside the MQTT broker. No Redis, no Kafka, no Node.js.
Write automation in plain English-like syntax anyone can understand.
***
Ready to see what this looks like? Here's a working example you can deploy in seconds.
## See It In Action
**The problem:** You want to see your broker run logic at the edge without external hardware or sensors.
**The solution:** A temperature simulator action that publishes readings every 5 seconds—deploy it and see data on a topic right away:
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
DEFINE ACTION TemperatureSimulator
ON EVERY 5 SECONDS DO
PUBLISH TOPIC "sensors/zone1/temperature" WITH "{\"temperature\": 72}"
```
This action runs every 5 seconds and publishes a JSON temperature reading to `sensors/zone1/temperature`. Subscribe to that topic with MQTT Explorer or any MQTT client to see output immediately after deployment.
***
## Resources
Explore examples and community projects.
Join the community for support and discussions.
Download Coreflux and start your free trial.
***
## Next Steps
Follow the 5-minute tutorial to create a working LoT action.
A beginner’s guide to building IIoT solutions with LoT, by yourself or with the help of AI.
# Callable ACTIONs
Source: https://docs.coreflux.org/latest/lot-language/actions/callable-actions
Reuse LoT logic across ACTIONs with callable utilities—shared calculations, validations, and on-demand scripts invoked via CALL ACTION.
## Overview
Most ACTIONs start automatically—on a schedule, when an MQTT message arrives, or at broker startup. A **callable ACTION** has no trigger. It runs when another ACTION invokes it with `CALL ACTION`, or when you publish `-runAction` to `$SYS/Coreflux/Command`.
Callable ACTIONs let you extract logic into named, reusable blocks: unit conversions, threshold checks, aggregations, or short on-demand scripts (refresh a cache, recalculate totals) that run at a specific point in a workflow—not on their own timer or topic.
**Think of callable ACTIONs as functions inside the broker.** Triggered ACTIONs decide *when* something happens; callables hold *how* to compute, validate, or transform—and run only when called.
***
## When to Use Callable ACTIONs
Before splitting logic out, understand where callables fit:
| Use Callable ACTIONs For | Use Triggered ACTIONs For |
| ------------------------------------------------------- | ---------------------------------------------------------- |
| Shared calculations used in multiple places | Reacting to MQTT messages (`ON TOPIC`, `ON CHANGE`) |
| Validations and transformations with typed inputs | Scheduled polling and reports (`ON EVERY`, `ON TIMESTAMP`) |
| On-demand steps inside a workflow (cache refresh, prep) | One-time bootstrap at startup (`ON START`) |
| Keeping main ACTION bodies short and readable | Logic that must run on its own when an event occurs |
If an ACTION must run by itself when data arrives or on a clock, give it a trigger. Use a callable when the logic is shared or should run only as part of another ACTION's flow. To invoke a callable from MQTT with JSON `INPUT` values, see [Broker Commands — run an Action on demand](/latest/mqtt-broker/commands#run-an-action-on-demand).
***
## Defining a Callable ACTION
A callable is a normal `DEFINE ACTION` **without** an `ON …` clause. Optionally declare `INPUT` parameters and `RETURN` one or more `OUTPUT` values.
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
DEFINE ACTION
[INPUT AS ...]
DO
-- logic
[RETURN
OUTPUT ...]
```
### Supported INPUT Types
| Type | Typical use |
| -------- | ------------------------------ |
| `STRING` | IDs, labels, status text |
| `INT` | Counts, discrete setpoints |
| `DOUBLE` | Sensor readings, analog values |
| `BOOL` | Flags and enable/disable state |
### Parameterless Callable
Use when the block has no inputs—common for on-demand scripts that read from topics or update cached state:
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
DEFINE ACTION RefreshDashboardCache
DO
KEEP TOPIC "cache/dashboard/summary" WITH (GET TOPIC "data/live/summary")
PUBLISH TOPIC "cache/dashboard/updated" WITH TIMESTAMP "UTC"
```
### Callable with Inputs and Return Value
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
DEFINE ACTION ConvertCelsiusToFahrenheit
INPUT celsius AS DOUBLE
DO
SET "fahrenheit" WITH ({celsius} * 9 / 5 + 32)
RETURN
OUTPUT fahrenheit
```
***
## CALL ACTION Syntax
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
CALL ACTION
WITH = , ...
RETURN [, , ...]
```
* `WITH` argument names must match the callee's `INPUT` names.
* `RETURN` variables are local to the caller and receive `OUTPUT` values in declaration order.
## Parameter Passing
Pass one or more values with `WITH`. Each name must match an `INPUT` on the callable:
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
CALL ACTION CalculateAverage
WITH value1 = {temp1}, value2 = {temp2}
RETURN result
```
Define the callable with a matching `INPUT` for each parameter:
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
DEFINE ACTION CalculateAverage
INPUT value1 AS DOUBLE
INPUT value2 AS DOUBLE
DO
SET "avg" WITH (({value1} + {value2}) / 2)
RETURN
OUTPUT avg
```
## Return Value Handling
The `RETURN` clause on the caller captures values from the callable's `OUTPUT` declarations.
### Single Return Value
Declare one `OUTPUT` on the callable and one variable on the caller:
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
DEFINE ACTION ConvertCelsiusToFahrenheit
INPUT celsius AS DOUBLE
DO
SET "fahrenheit" WITH ({celsius} * 9 / 5 + 32)
RETURN
OUTPUT fahrenheit
```
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
CALL ACTION ConvertCelsiusToFahrenheit
WITH celsius = {temp_c}
RETURN temp_f
```
### Multiple Return Values
Declare one `OUTPUT` per value. On the caller, list return variables in the **same order** as the `OUTPUT` lines:
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
DEFINE ACTION AnalyzeValue
INPUT value AS DOUBLE
INPUT threshold AS DOUBLE
DO
SET "is_above" WITH ({value} > {threshold})
SET "difference" WITH ({value} - {threshold})
SET "percentage" WITH ({value} / {threshold} * 100)
RETURN
OUTPUT is_above
OUTPUT difference
OUTPUT percentage
```
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
CALL ACTION AnalyzeValue
WITH value = {reading}, threshold = {limit}
RETURN above, diff, pct
```
The first `OUTPUT` (`is_above`) maps to `above`, the second to `diff`, and the third to `pct`.
***
## Usage Examples
Call a shared conversion from a topic-triggered ACTION instead of repeating the formula inline:
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
DEFINE ACTION ProcessTemperatureReading
ON TOPIC "sensors/+/celsius" DO
SET "sensor_id" WITH TOPIC POSITION 2
SET "temp_c" WITH PAYLOAD AS DOUBLE
CALL ACTION ConvertCelsiusToFahrenheit
WITH celsius = {temp_c}
RETURN temp_f
PUBLISH TOPIC "sensors/" + {sensor_id} + "/fahrenheit" WITH {temp_f}
```
A parameterless callable runs only when a triggered ACTION calls it—not on its own schedule or topic:
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
DEFINE ACTION HourlyDashboardReport
ON EVERY 1 HOUR DO
CALL ACTION RefreshDashboardCache
SET "summary" WITH (GET TOPIC "cache/dashboard/summary")
PUBLISH TOPIC "reports/dashboard/hourly" WITH {summary}
```
Chain callables in a triggered ACTION so the main body stays focused on orchestration:
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
DEFINE ACTION ProcessTankLevel
ON TOPIC "tanks/+/level" DO
SET "tank_id" WITH TOPIC POSITION 2
SET "level" WITH PAYLOAD AS DOUBLE
SET "capacity" WITH (GET TOPIC "config/" + {tank_id} + "/capacity" AS DOUBLE)
CALL ACTION CalculatePercentage
WITH value = {level}, total = {capacity}
RETURN fill_pct
CALL ACTION AnalyzeReading
WITH current = {level}, threshold = ({capacity} * 0.9)
RETURN above, diff, pct_of_max
PUBLISH TOPIC "processed/" + {tank_id} + "/fill_percent" WITH {fill_pct}
PUBLISH TOPIC "processed/" + {tank_id} + "/over_threshold" WITH {above}
```
***
## Best Practices
Each callable should do one clear thing—convert units, validate a range, compute an average. If a block grows large or handles unrelated steps, split it into separate callables and compose them with `CALL ACTION`.
The moment the same expression appears in two ACTIONs, consider a callable. Shared rules (thresholds, conversions, KPI formulas) belong in one place so fixes propagate to every caller on deploy.
Use PascalCase, function-like names: `ConvertCelsiusToFahrenheit`, `CalculateAverage`, `ValidateReading`. See [Naming conventions](/latest/lot-language/usage-patterns/naming-conventions).
Triggered ACTIONs should read as workflows: read data, call utilities, publish results. Push calculations and validations into callables so topic and schedule handlers stay easy to scan and maintain.
***
## Next Steps
Review all available LoT operations.
Time-based, topic-based, and initialization triggers.
# ACTION Triggers
Source: https://docs.coreflux.org/latest/lot-language/actions/events
Define when actions execute using time-based schedules (ON EVERY, ON TIMESTAMP), topic-based events, or manual invocation
## Overview
Every Action needs a trigger—the event that starts its execution. Choosing the right trigger type determines how your automation responds to real-world conditions.
**Think of triggers like different ways to start your car.** A time-based trigger is like an auto-start that warms up your car every morning at 7 AM. A topic-based trigger is like pushing the start button when you get in. A callable action is like asking someone else to start it for you.
## When to Use Each Type
| Trigger Type | Use When | Example |
| -------------------------------------------------------------------- | --------------------------------------------------------- | -------------------------------------------------------------------- |
| **Time-Based** (`ON EVERY`, `ON TIMESTAMP`) | You need a clock-driven schedule (interval or wall-clock) | Heartbeats, polling, shift windows, one-shot cutovers |
| **Topic-Based** (`ON TOPIC`, `ON CHANGE`) | You need to react when MQTT data arrives | Sensor processing, commands; `ON CHANGE` when only new values matter |
| **Initialization** (`ON START`) | You need to run logic once when the broker starts | Bootstrap UNS structure, set default config values |
| **Client lifecycle** (`ON CONNECT`, `ON DISCONNECT`, `ON SUBSCRIBE`) | You need to react to MQTT session events | Connection audit, session dashboards, subscription tracking |
| **Callable** (no trigger) | You need reusable logic invoked by other actions | Utility functions, shared calculations |
***
## Time-Based Triggers
LoT supports several time-based trigger forms. All of them use the `ON` keyword and run the action's `DO` block automatically—no MQTT message is required to fire them. Together they cover recurring intervals, calendar rules (a specific weekday, Monday–Friday, or Saturday–Sunday), daily wall-clock times, and single one-shot executions.
### Quick reference
| Syntax | Pattern | Recurs? |
| -------------------------------------- | -------------------------------------------- | ------------------- |
| `ON EVERY` *N* *UNIT* | Every *N* seconds, minutes, hours, and so on | Yes — indefinitely |
| `ON EVERY` *DAY* `AT` `"HH:mm"` | Named weekday at a fixed local time | Yes — every week |
| `ON EVERY WEEKDAY AT` `"HH:mm"` | Monday–Friday at a fixed local time | Yes — every weekday |
| `ON EVERY WEEKEND AT` `"HH:mm"` | Saturday–Sunday at a fixed local time | Yes — every weekend |
| `ON TIMESTAMP` `"HH:mm:ss"` | Every day at a fixed local time | Yes — every day |
| `ON TIMESTAMP` `"dd-MM-yyyy HH:mm:ss"` | Exact date and time | No — one-shot |
**Time zone:** For `HH:mm` and `HH:mm:ss` in `ON EVERY … AT` and in daily `ON TIMESTAMP`, values are interpreted in the **local time of the machine running the broker**, then converted to UTC internally. The action is scheduled for the correct wall-clock time across daylight saving changes. The one-shot form `dd-MM-yyyy HH:mm:ss` is interpreted as **UTC**, as described below.
**Structured payloads:** `PUBLISH TOPIC` does not support inline JSON object literals. For multi-field records, define a **COLLAPSED** model and use `PUBLISH MODEL … TO … WITH` (see [Publishing Models](/latest/lot-language/models/publish-model)). The timed-action examples below use that pattern.
### 1. `ON EVERY` *N* *UNIT* — fixed interval
Fires repeatedly every *N* units of time. Common for monitoring, polling, and heartbeats.
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
DEFINE ACTION
ON EVERY DO
-- body
```
#### Supported units
| Keyword(s) | Example |
| -------------------- | --------------------- |
| `MILLISECOND` / `MS` | `ON EVERY 500 MS` |
| `SECOND` / `SECONDS` | `ON EVERY 30 SECONDS` |
| `MINUTE` / `MINUTES` | `ON EVERY 5 MINUTES` |
| `HOUR` / `HOURS` | `ON EVERY 1 HOUR` |
| `DAY` / `DAYS` | `ON EVERY 1 DAY` |
| `WEEK` / `WEEKS` | `ON EVERY 2 WEEKS` |
| `MONTH` / `MONTHS` | `ON EVERY 1 MONTH` |
| `YEAR` / `YEARS` | `ON EVERY 1 YEAR` |
Singular and plural unit names are interchangeable (for example, `1 SECOND` and `1 SECONDS`). `ON EVERY 1 DAY` fires every 24 hours from the last run; for the same clock time every calendar day, use daily `ON TIMESTAMP` instead.
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
DEFINE ACTION SystemHeartbeat
ON EVERY 5 SECONDS DO
PUBLISH TOPIC "system/heartbeat" WITH "alive"
PUBLISH TOPIC "system/heartbeat/timestamp" WITH TIMESTAMP "UTC"
```
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
DEFINE ACTION PollMachineTemperature
ON EVERY 30 SECONDS DO
SET "temp" WITH GET TOPIC "machine/sensor/temperature"
IF {temp} > 85 THEN
PUBLISH TOPIC "alerts/temperature/high" WITH {temp}
PUBLISH TOPIC "machine/temperature/last_check" WITH TIMESTAMP "UTC"
```
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
DEFINE ACTION FastSensorSample
ON EVERY 500 MS DO
PUBLISH TOPIC "sensors/vibration/sample" WITH GET TOPIC "sensors/vibration/raw"
```
### 2. `ON EVERY` *DAY* `AT` — weekly, specific day
Fires once per week on the named weekday at the given **local** time. Use `"HH:mm"` or `"HH:mm:ss"`; seconds are optional.
| Keyword | Fires on |
| ------------------- | ---------------------- |
| `MONDAY` … `SUNDAY` | That weekday each week |
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
DEFINE ACTION
ON EVERY MONDAY AT "08:00:00" DO
-- body
```
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
DEFINE MODEL ProductionWeeklyReport COLLAPSED
ADD STRING "week"
ADD INT "total_parts"
ADD INT "total_rejects"
DEFINE ACTION WeeklyProductionReport
ON EVERY MONDAY AT "08:00:00" DO
SET "week_total" WITH GET TOPIC "factory/line1/parts_produced"
SET "week_rejects" WITH GET TOPIC "factory/line1/rejects"
PUBLISH MODEL ProductionWeeklyReport TO "reports/weekly/production" WITH
week = TIMESTAMP "UTC"
total_parts = {week_total}
total_rejects = {week_rejects}
PUBLISH TOPIC "factory/line1/parts_produced" WITH 0
PUBLISH TOPIC "factory/line1/rejects" WITH 0
```
### 3. `ON EVERY WEEKDAY AT` — Monday–Friday
Fires Monday through Friday at the same **local** time—one action instead of five separate daily schedules.
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
DEFINE MODEL ShiftMorningBriefing COLLAPSED
ADD STRING "line_status"
ADD STRING "pending_orders"
ADD STRING "shift_start"
DEFINE ACTION ShiftStartBriefing
ON EVERY WEEKDAY AT "07:30" DO
SET "line_status" WITH GET TOPIC "factory/line1/status"
SET "pending_orders" WITH GET TOPIC "erp/orders/pending"
PUBLISH MODEL ShiftMorningBriefing TO "shift/briefing/morning" WITH
line_status = {line_status}
pending_orders = {pending_orders}
shift_start = TIMESTAMP "UTC"
```
### 4. `ON EVERY WEEKEND AT` — Saturday and Sunday
Fires on both weekend days at the specified **local** time—useful for maintenance windows or weekend-only reporting.
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
DEFINE ACTION WeekendMaintenanceWindow
ON EVERY WEEKEND AT "06:00:00" DO
PUBLISH TOPIC "maintenance/window/open" WITH "true"
PUBLISH TOPIC "maintenance/window/start" WITH TIMESTAMP "UTC"
PUBLISH TOPIC "notifications/maintenance" WITH "Weekend maintenance window is now open"
```
### 5. `ON TIMESTAMP` — daily fixed time
Fires **every day** at the given **local** wall-clock time. Unlike `ON EVERY 1 DAY`, this tracks clock time, not elapsed time since the broker or last run started.
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
DEFINE ACTION
ON TIMESTAMP "08:00:00" DO
-- body
```
`"HH:mm"` without seconds is accepted.
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
DEFINE ACTION DailyCounterReset
ON TIMESTAMP "00:00:00" DO
PUBLISH TOPIC "factory/line1/parts_produced" WITH 0
PUBLISH TOPIC "factory/line1/rejects" WITH 0
PUBLISH TOPIC "factory/line1/day_start" WITH TIMESTAMP "UTC"
```
### 6. `ON TIMESTAMP` — one-shot, exact date and time
Fires **once** at the exact instant given, then never again (the scheduler moves the next run far into the future). Format is strictly **`dd-MM-yyyy HH:mm:ss`** (day–month–year). The timestamp is treated as **UTC**.
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
DEFINE ACTION SystemCutoverNotice
ON TIMESTAMP "01-04-2026 06:00:00" DO
PUBLISH TOPIC "system/cutover/notice" WITH "New firmware version going live in 1 hour"
PUBLISH TOPIC "notifications/all" WITH "Scheduled maintenance begins at 07:00 UTC"
```
### Combination patterns
Real systems often split schedules across several actions—one trigger per concern.
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
DEFINE MODEL DailyTemperatureReport COLLAPSED
ADD DOUBLE "max"
ADD DOUBLE "min"
ADD STRING "date"
DEFINE ACTION SensorPoll
ON EVERY 10 SECONDS DO
SET "value" WITH GET TOPIC "sensors/temperature/raw"
PUBLISH TOPIC "sensors/temperature/current" WITH {value}
DEFINE ACTION DailySensorSummary
ON TIMESTAMP "00:00:00" DO
SET "max" WITH GET TOPIC "sensors/temperature/max"
SET "min" WITH GET TOPIC "sensors/temperature/min"
PUBLISH MODEL DailyTemperatureReport TO "reports/daily/temperature" WITH
max = {max}
min = {min}
date = TIMESTAMP "UTC"
PUBLISH TOPIC "sensors/temperature/max" WITH 0
PUBLISH TOPIC "sensors/temperature/min" WITH 0
```
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
DEFINE ACTION WeekdayProductionStart
ON EVERY WEEKDAY AT "06:00" DO
PUBLISH TOPIC "factory/line1/mode" WITH "production"
PUBLISH TOPIC "factory/line1/shift_start" WITH TIMESTAMP "UTC"
DEFINE ACTION WeekendMaintenance
ON EVERY WEEKEND AT "07:00" DO
PUBLISH TOPIC "factory/line1/mode" WITH "maintenance"
PUBLISH TOPIC "notifications/maintenance/start" WITH TIMESTAMP "UTC"
```
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
DEFINE MODEL DashboardThroughput COLLAPSED
ADD INT "parts"
ADD DOUBLE "rate_per_min"
ADD INT "updated"
DEFINE ACTION WeeklyKpiReset
ON EVERY MONDAY AT "00:00:00" DO
PUBLISH TOPIC "kpi/weekly/parts_produced" WITH 0
PUBLISH TOPIC "kpi/weekly/reset_time" WITH TIMESTAMP "UTC"
DEFINE ACTION DailyKpiSnapshot
ON TIMESTAMP "23:59:00" DO
SET "daily_parts" WITH GET TOPIC "factory/line1/parts_produced"
PUBLISH TOPIC "kpi/daily/snapshot" WITH {daily_parts}
PUBLISH TOPIC "kpi/daily/snapshot_time" WITH TIMESTAMP "UTC"
DEFINE ACTION LiveThroughput
ON EVERY 1 MINUTE DO
SET "parts" WITH GET TOPIC "factory/line1/parts_produced"
SET "rate" WITH GET TOPIC "factory/line1/production_rate"
PUBLISH MODEL DashboardThroughput TO "dashboard/live/throughput" WITH
parts = {parts}
rate_per_min = {rate}
updated = TIMESTAMP "UNIX"
```
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
DEFINE ACTION CutoverActivation
ON TIMESTAMP "01-07-2026 06:00:00" DO
PUBLISH TOPIC "system/version" WITH "2.0"
PUBLISH TOPIC "system/cutover/complete" WITH TIMESTAMP "UTC"
DEFINE ACTION V2Heartbeat
ON EVERY 30 SECONDS DO
SET "ver" WITH GET TOPIC "system/version"
IF {ver} EQUALS "2.0" THEN
PUBLISH TOPIC "system/v2/heartbeat" WITH "running"
```
### Error handling
If a time string does not match an expected format, the broker logs an error similar to:
`Invalid DATE_TIME format: . Expected formats:`
* `dd-MM-yyyy HH:mm:ss` (one-time)
* `HH:mm:ss` / `HH:mm` (daily)
* Weekly forms using a day name with `AT` and a time string
Errors are also published (retained) to:
`$SYS/Coreflux/Actions//Error`
Subscribe to `$SYS/Coreflux/Actions/+/Error` to observe failures from any action.
### At a glance — valid time triggers
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
ON EVERY 500 MS DO
ON EVERY 10 SECONDS DO
ON EVERY 5 MINUTES DO
ON EVERY 1 HOUR DO
ON EVERY 1 DAY DO
ON EVERY 1 WEEK DO
ON EVERY 1 MONTH DO
ON EVERY MONDAY AT "08:00:00" DO
ON EVERY WEEKDAY AT "07:30" DO
ON EVERY WEEKEND AT "06:00" DO
ON TIMESTAMP "07:00" DO
ON TIMESTAMP "00:00:00" DO
ON TIMESTAMP "25-12-2026 09:00:00" DO
```
### More examples
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
DEFINE ACTION SystemHeartbeat
ON EVERY 5 SECONDS DO
PUBLISH TOPIC "system/status" WITH "online"
PUBLISH TOPIC "system/timestamp" WITH TIMESTAMP "UTC"
```
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
DEFINE ACTION IncrementCounter
ON EVERY 10 SECONDS DO
IF GET TOPIC "counter/value" == EMPTY THEN
PUBLISH TOPIC "counter/value" WITH 0
ELSE
PUBLISH TOPIC "counter/value" WITH (GET TOPIC "counter/value" + 1)
```
***
## Topic-Based Triggers
MQTT-driven actions use a **topic trigger** — the condition that tells the broker *when* to run the action body. Both triggers share the same topic patterns and wildcards; the difference is whether repeated identical payloads should be ignored.
| Trigger | When the action runs |
| --------------- | ------------------------------------------------------------------------------------------------------------------ |
| **`ON TOPIC`** | On **every** message that matches the topic pattern |
| **`ON CHANGE`** | When the message matches the pattern **and** the payload is **different** from the last message on that same topic |
### `ON TOPIC` — react to every message
Use `ON TOPIC` when each publish matters: commands, alarms, counters, or any flow where duplicate payloads still need processing.
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
DEFINE ACTION ProcessTemperature
ON TOPIC "sensors/+/temperature" DO
SET "sensor_id" WITH TOPIC POSITION 2
SET "temp" WITH PAYLOAD AS DOUBLE
PUBLISH TOPIC "processed/" + {sensor_id} + "/temperature" WITH {temp}
```
**Key characteristics:**
* Any client (or route) that publishes to a matching topic runs the action.
* Publishing the same value twice runs the action **twice**.
* Wildcard `+` matches one topic level (same rules as [MQTT wildcards](/latest/mqtt-broker/protocol#wildcards)).
### `ON CHANGE` — react only when the value changes
Use `ON CHANGE` for telemetry, status fields, or setpoints that are published often but only need logic when the reading actually changes. This reduces CPU load, database writes, and downstream noise.
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
DEFINE ACTION ProcessTemperatureOnChange
ON CHANGE "sensors/+/temperature" DO
SET "sensor_id" WITH TOPIC POSITION 2
SET "temp" WITH PAYLOAD AS DOUBLE
PUBLISH TOPIC "processed/" + {sensor_id} + "/temperature" WITH {temp}
```
**Key characteristics:**
* The **first** message on a given topic always runs the action (there is no previous value yet).
* Later messages run the action only if the **payload bytes** are not identical to the last message on that **exact** topic name.
* Example: `sensors/line1/temperature` and `sensors/line2/temperature` are tracked separately.
* Wildcard patterns work the same as `ON TOPIC`; change is evaluated per real topic, not per pattern.
**What counts as “changed”?** Comparison is on the **raw message body** (bytes), not on parsed numbers or JSON. `25` and `25.0` are different payloads. `{"temp":25}` and `{"temp": 25}` (different spacing) are different payloads. If you need semantic equality (for example, treat `25` and `25.0` as the same), handle that inside an `ON TOPIC` action with your own logic.
### Choosing between `ON TOPIC` and `ON CHANGE`
| Situation | Recommended trigger |
| --------------------------------------------------------------------------- | ---------------------------------------------- |
| Commands, events, “do something” signals | `ON TOPIC` |
| Alarms that must fire on every notification | `ON TOPIC` |
| High-frequency sensor values (temperature, level, status) | `ON CHANGE` |
| Writing to a database or historian only when value moves | `ON CHANGE` |
| Forwarding data only when it differs | `ON CHANGE` |
| Same pattern, but some actions need every message and others only on change | Use **both** (two actions on the same pattern) |
You can deploy two actions on the same topic pattern: one with `ON TOPIC` and one with `ON CHANGE`. The broker runs `ON TOPIC` every time and skips the `ON CHANGE` action when the payload is unchanged.
### Side-by-side example
Two devices publish temperature every second. Only `22.5` → `23.1` should trigger heavy processing.
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
DEFINE ACTION LogEveryReading
ON TOPIC "sensors/+/temperature" DO
PUBLISH TOPIC "logs/all" WITH PAYLOAD
```
On value change only:
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
DEFINE ACTION ProcessWhenDifferent
ON CHANGE "sensors/+/temperature" DO
SET "temp" WITH PAYLOAD AS DOUBLE
PUBLISH TOPIC "analytics/updated" WITH {temp}
```
| Publish to | Payload | `LogEveryReading` | `ProcessWhenDifferent` |
| ----------------------- | ------- | ----------------- | ---------------------- |
| `sensors/a/temperature` | `22.5` | Runs | Runs (first value) |
| `sensors/a/temperature` | `22.5` | Runs | Skipped (unchanged) |
| `sensors/a/temperature` | `23.1` | Runs | Runs (changed) |
Inside the action body, **`PAYLOAD`** is the content of the message that fired the trigger, and **`TOPIC POSITION N`** is segment *N* of the actual topic (1-based), useful with `+`.
### Wildcard Patterns
LoT supports MQTT wildcards for flexible topic matching:
The `+` wildcard matches exactly one topic level:
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
DEFINE ACTION SensorRouter
ON TOPIC "sensors/+/temperature" DO
// POSITION 1="sensors", POSITION 2=wildcard (sensor_id), POSITION 3="temperature"
SET "sensor_id" WITH TOPIC POSITION 2
PUBLISH TOPIC "processed/" + {sensor_id} + "/temp" WITH PAYLOAD
```
**Matches:** `sensors/room1/temperature`, `sensors/room2/temperature`
**Does NOT match:** `sensors/building1/floor2/temperature` (too many levels)
Use multiple single-level wildcards for deeper hierarchies:
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
DEFINE ACTION FactoryMonitor
ON TOPIC "factory/+/+/status" DO
// POSITION 1="factory", POSITION 2=line_id, POSITION 3=machine_id, POSITION 4="status"
SET "line_id" WITH TOPIC POSITION 2
SET "machine_id" WITH TOPIC POSITION 3
PUBLISH TOPIC "monitoring/" + {line_id} + "/" + {machine_id} WITH PAYLOAD
```
**Matches:** `factory/line1/machine1/status`, `factory/line2/press3/status`
**Does NOT match:** `factory/line1/status` (missing one level), `factory/status` (missing two levels)
***
## Initialization Triggers
Use `ON START` to execute logic exactly once when the broker starts **or when the action is first deployed on a live broker**. This is the right place to bootstrap a Unified Namespace (UNS) structure, publish retained default configurations, and log startup events.
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
DEFINE ACTION InitUNSRoot
ON START DO
KEEP TOPIC "config/uns_root" WITH "factory/lisbon"
KEEP TOPIC "factory/info/name" WITH "Lisbon Plant"
KEEP TOPIC "factory/info/timezone" WITH "Europe/Lisbon"
PUBLISH TOPIC "system/log" WITH "UNS root initialised"
```
**Key characteristics:**
* Runs once when the broker starts, and again when you first deploy (or redeploy) the action while the broker is already up — you do not need a restart to seed bootstrap topics
* Cold-start behaviour is unchanged: every `ON START` action still runs once after the MQTT broker is ready. Deploying before the broker is ready defers until startup so the body is not double-run
* A failing `ON START` body is logged but does not fail the deploy
* `KEEP TOPIC` publishes with the retain flag so the value is delivered to any future subscriber immediately on connection
* Use it to set default configuration values and retained state that must survive restarts
* There is no `ON STOP` equivalent — `ON START` is the correct place for setup logic
Action `PUBLISH` and `KEEP` are ordinary MQTT publishes: the payload is on the topic and any client subscribed to that topic receives it. What they skip is the **publish ACL** that applies to connected MQTT users — a restrictive rule on that topic does not stop the action from publishing, so bootstrap topics from `ON START` (and other actions) are not silently dropped. Clients still need subscribe permission to receive those messages, and anything a client publishes itself remains fully subject to rules.
***
## Client Lifecycle Triggers
Use **`ON CONNECT`**, **`ON DISCONNECT`**, and **`ON SUBSCRIBE`** when automation must react to MQTT session events instead of publishes. Typical uses include audit trails, connection dashboards, onboarding retained topics when a UI client connects, or tracking which topics clients subscribe to.
| Trigger | When the action runs |
| --------------------------------- | ------------------------------------------------------------------------------------------------------------------------ |
| **`ON CONNECT DO`** | After a client passes authentication and the broker accepts the MQTT CONNECT packet |
| **`ON DISCONNECT DO`** | When the broker detects a client disconnection (clean, unclean, or session takeover) |
| **`ON SUBSCRIBE ["pattern"] DO`** | After the broker accepts a subscription (RBAC check passed); optional pattern limits which subscriptions fire the action |
If no action in the broker uses `ON CONNECT`, `ON DISCONNECT`, or `ON SUBSCRIBE`, the broker does not dispatch those lifecycle hooks — there is no overhead when the triggers are unused.
### `ON CONNECT DO`
Runs once after a client successfully authenticates and the broker accepts the MQTT CONNECT packet.
```lot wrap focus={2} theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
DEFINE ACTION AuditConnect
ON CONNECT DO
PUBLISH TOPIC "audit/connect/" + CLIENTID WITH USER + "@" + ENDPOINT
KEEP TOPIC "sessions/" + CLIENTID + "/clean" WITH CLEANSESSION
```
**Key characteristics:**
* Fires for every successful client connection, including routes and HUB clients.
* Session context variables (`CLIENTID`, `USER`, `ENDPOINT`, `CLEANSESSION`) reflect the connecting client.
* `USER IN GROUP`, `USER EQUALS`, and `USER HAS` evaluate against the session user, the same way they do for `ON TOPIC`.
### `ON DISCONNECT DO`
Runs when the broker detects a client disconnection — clean disconnect, unclean disconnect, or session takeover.
```lot wrap focus={2} theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
DEFINE ACTION AuditDisconnect
ON DISCONNECT DO
PUBLISH TOPIC "audit/disconnect/" + CLIENTID WITH DISCONNECTREASON
```
**Key characteristics:**
* `DISCONNECTREASON` carries the disconnect type or MQTT reason code for the session that ended.
* `CLIENTID`, `USER`, and `ENDPOINT` reflect the disconnecting session when available from the connect handshake.
### `ON SUBSCRIBE DO`
Runs after the broker accepts a subscription and RBAC allows it. Use a topic pattern to limit which subscriptions fire the action; omit the pattern to run on **every** allowed subscription.
```lot wrap focus={2} theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
DEFINE ACTION TrackSensorSubscriptions
ON SUBSCRIBE "sensors/#" DO
PUBLISH TOPIC "audit/subscriptions" WITH CLIENTID + " -> " + SUBSCRIBETOPIC + " qos=" + SUBSCRIBEQOS
```
Track every allowed subscription:
```lot wrap focus={2} theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
DEFINE ACTION TrackAllSubscriptions
ON SUBSCRIBE DO
KEEP TOPIC "audit/last/subscribe" WITH SUBSCRIBETOPIC
```
**Key characteristics:**
* Supports the same MQTT wildcards as `ON TOPIC`: `+` (single level) and `#` (multi-level).
* `SUBSCRIBETOPIC` is the topic filter the client subscribed to; `SUBSCRIBEQOS` is the requested QoS (`0`, `1`, or `2`).
* Lifecycle subscribe dispatch runs for all allowed subscriptions, including Visu and system topics, unless you filter them in the action body.
### Session context variables
Available inside lifecycle-triggered actions (and still available on `ON TOPIC` / `ON CHANGE` for the **publisher**):
| Variable | `ON CONNECT` | `ON DISCONNECT` | `ON SUBSCRIBE` | `ON TOPIC` / `ON CHANGE` |
| ------------------ | -------------------------- | ----------------------------- | ---------------------------- | ------------------------ |
| `CLIENTID` | Connecting client | Disconnecting client | Subscribing client | Publishing client |
| `USER` | Authenticated username | Session username | Session username | Publisher username |
| `ENDPOINT` | Client address (`ip:port`) | Last known address | Address from connect | Empty |
| `CLEANSESSION` | `"true"` / `"false"` | Value from connect | Value from connect | Empty |
| `DISCONNECTREASON` | Empty | Disconnect type / MQTT reason | Empty | Empty |
| `SUBSCRIBETOPIC` | Empty | Empty | Subscribed topic filter | Empty |
| `SUBSCRIBEQOS` | `-1` | `-1` | QoS level (`0`, `1`, or `2`) | `-1` |
### Not the same as Visu `ON CHANGE`
In **dashboard / Visu (LoTV)** panels, `ON CHANGE DO` on a slider or text field is a **UI event** in the browser (user edits a control). That is separate from broker **`ON CHANGE "topic" DO`**, which reacts to **MQTT publishes** inside the broker.
Lifecycle triggers (`ON CONNECT`, `ON DISCONNECT`, `ON SUBSCRIBE`) are also broker-side only. The broker still publishes `$SYS/Coreflux/Comms/Logins` on connect for observability, but LoT lifecycle actions give you programmatic automation with session context variables.
| Context | Syntax | What fires it |
| ----------------- | ----------------------------- | -------------------------------------- |
| Broker LoT action | `ON CHANGE "sensors/temp" DO` | MQTT message with changed payload |
| Broker LoT action | `ON CONNECT DO` | MQTT client connects |
| Broker LoT action | `ON DISCONNECT DO` | MQTT client disconnects |
| Broker LoT action | `ON SUBSCRIBE "pattern" DO` | Client subscribes to a matching filter |
| Visu component | `ON CHANGE DO PUBLISH …` | User changing a control in the UI |
***
## Callable Actions
Actions **without** a trigger clause (`ON TOPIC`, `ON EVERY`, `ON START`, `ON CONNECT`, and so on) are **callable**—they run only when another ACTION calls them with `CALL ACTION`.
Use callables for reusable logic (conversions, validation, shared math) that several trigger-based Actions need. They accept typed `INPUT` parameters and can `RETURN` one or more `OUTPUT` values.
**Full guide:** [Callable ACTIONs](./callable-actions)—reusable logic, on-demand scripts, and `CALL ACTION` syntax.
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
DEFINE ACTION CalculateAverage
INPUT value1 AS DOUBLE
INPUT value2 AS DOUBLE
DO
SET "avg" WITH (({value1} + {value2}) / 2)
RETURN
OUTPUT avg
DEFINE ACTION ProcessSensorPair
ON TOPIC "sensors/pair/data" DO
SET "temp1" WITH (GET JSON "sensor1" IN PAYLOAD AS DOUBLE)
SET "temp2" WITH (GET JSON "sensor2" IN PAYLOAD AS DOUBLE)
CALL ACTION CalculateAverage
WITH value1 = {temp1}, value2 = {temp2}
RETURN result
PUBLISH TOPIC "sensors/average" WITH {result}
```
***
## Comparing Trigger Types
| Aspect | Time-Based | Topic-Based | ON START | Client lifecycle | Callable |
| ------------------ | -------------------------------- | --------------- | ------------------------------ | --------------------------------------- | ------------------ |
| **Trigger** | Clock interval or schedule | MQTT message | Broker startup or first deploy | Connect, disconnect, subscribe | Manual command |
| **Frequency** | Interval, daily, weekly, or once | Event-driven | Once | Per session event | On-demand |
| **Payload Access** | No | Yes (`PAYLOAD`) | No | Session context (`CLIENTID`, `USER`, …) | Via parameters |
| **Best For** | Monitoring, reporting | Data processing | Initialization | Audit, session automation | Reusable utilities |
***
## Next Steps
Reusable logic, on-demand scripts, and `CALL ACTION`.
Learn about SET, GET, PUBLISH, and conditional logic.
# ACTION Operations Reference
Source: https://docs.coreflux.org/latest/lot-language/actions/operations
Complete reference for LoT operations: variables, arithmetic, strings, RANDOM, transformations, and control flow
## Overview
Complete reference for all LoT operations available within actions. These operations allow you to manipulate variables, perform calculations, transform strings, read and write MQTT topics, and implement conditional logic.
**Quick Reference:** Use the sidebar to jump to specific operations.
***
## Variable Operations
SET, GET TOPIC, GET JSON, and GET PROTO are the core operations for working with data in actions. They allow you to store values, read from MQTT topics, extract fields from JSON payloads, and decode protobuf fields.
### SET
Creates or assigns a value to an internal variable:
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
SET "variable_name" WITH
```
Variables exist only during action execution and are referenced using `{variable_name}`.
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
SET "threshold" WITH 100
SET "status" WITH "active"
SET "enabled" WITH TRUE
```
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
SET "current_temp" WITH (GET TOPIC "sensors/temperature")
// For topic "sensors/temp001/data": POSITION 2 gets the sensor ID
SET "sensor_id" WITH TOPIC POSITION 2
```
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
SET "total" WITH (GET TOPIC "count/a" + GET TOPIC "count/b")
SET "average" WITH ({sum} / {count})
```
Variable names must be enclosed in double quotes when using SET, but referenced with curly braces: `SET "myVar" WITH 10` then use `{myVar}`.
### GET TOPIC
Retrieves the last known value from a topic. If the topic has no value, the broker returns a default based on the requested type.
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
GET TOPIC "topic/path"
GET TOPIC "topic/path" AS
```
#### Default Values for Missing Topics
| Type | Default Value |
| -------- | ------------------- |
| `STRING` | `""` (empty string) |
| `DOUBLE` | `0.0` |
| `INT` | `0` |
| `BOOL` | `FALSE` |
#### Type Casting
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
GET TOPIC "sensors/temp" AS DOUBLE
GET TOPIC "config/count" AS INT
GET TOPIC "flags/enabled" AS BOOL
```
#### Wildcard Inheritance
When an action is triggered by a wildcard topic pattern, GET TOPIC inherits the matched wildcard values:
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
DEFINE ACTION ProcessWithContext
ON TOPIC "sensors/+/temperature" DO
// If triggered by "sensors/temp001/temperature"
// The + wildcard matches "temp001"
// This GET TOPIC uses the same wildcard value:
SET "humidity" WITH (GET TOPIC "sensors/+/humidity" AS DOUBLE)
// Reads from "sensors/temp001/humidity"
```
#### Example Usage
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
// Read and process
SET "last_temp" WITH (GET TOPIC "sensors/temperature" AS DOUBLE)
// Check before using (EMPTY check)
IF GET TOPIC "config/threshold" == EMPTY THEN
SET "threshold" WITH 80
ELSE
SET "threshold" WITH (GET TOPIC "config/threshold" AS DOUBLE)
// Combine multiple topic values
SET "total" WITH (GET TOPIC "counters/a" AS INT + GET TOPIC "counters/b" AS INT)
```
### GET JSON
Extracts fields from JSON payloads with support for nested paths and arrays.
For narrative patterns, missing-field workflows, and end-to-end `PUBLISH MODEL` examples, see [Working with JSON](/latest/lot-language/usage-patterns/working-with-json).
#### Basic Syntax
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
GET JSON "field_name" IN PAYLOAD AS
```
#### Supported Types
| Type | Use For | Example |
| -------- | ----------------- | --------------------------------------------- |
| `STRING` | Text values | `GET JSON "name" IN PAYLOAD AS STRING` |
| `DOUBLE` | Decimal numbers | `GET JSON "temperature" IN PAYLOAD AS DOUBLE` |
| `INT` | Whole numbers | `GET JSON "count" IN PAYLOAD AS INT` |
| `BOOL` | True/false values | `GET JSON "enabled" IN PAYLOAD AS BOOL` |
#### Nested Path Extraction
Use dot notation to access nested objects and bracket notation for arrays:
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
// Nested object access
SET "nested" WITH (GET JSON "data.sensors.temperature" IN PAYLOAD AS DOUBLE)
SET "deep" WITH (GET JSON "metadata.config.threshold" IN PAYLOAD AS INT)
// Array access (0-indexed)
SET "first_reading" WITH (GET JSON "readings[0].value" IN PAYLOAD AS DOUBLE)
SET "second_sensor" WITH (GET JSON "data.sensors[1].id" IN PAYLOAD AS STRING)
```
For a payload like:
```json wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
{
"data": {
"sensors": [
{ "id": "temp001", "value": 23.5 },
{ "id": "temp002", "value": 24.1 }
]
},
"metadata": { "config": { "threshold": 80 } }
}
```
#### Error Handling
If a field doesn't exist, the result depends on the requested type:
| Type | Missing Field Result |
| -------- | -------------------- |
| `STRING` | `""` (empty string) |
| `DOUBLE` | `0.0` |
| `INT` | `0` |
| `BOOL` | `FALSE` |
Use EMPTY check for conditional handling:
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
SET "value" WITH (GET JSON "optional_field" IN PAYLOAD AS DOUBLE)
IF {value} == EMPTY THEN
SET "value" WITH 100 // Default fallback
```
#### Complete Example
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
DEFINE ACTION ProcessComplexJSON
ON TOPIC "sensors/data" DO
// Top-level fields
SET "sensor_id" WITH (GET JSON "id" IN PAYLOAD AS STRING)
SET "timestamp" WITH (GET JSON "timestamp" IN PAYLOAD AS STRING)
// Nested fields
SET "temp" WITH (GET JSON "readings.temperature" IN PAYLOAD AS DOUBLE)
SET "humidity" WITH (GET JSON "readings.humidity" IN PAYLOAD AS DOUBLE)
// Array access
SET "latest" WITH (GET JSON "history[0].value" IN PAYLOAD AS DOUBLE)
PUBLISH TOPIC "processed/" + {sensor_id} + "/temp" WITH {temp}
PUBLISH TOPIC "processed/" + {sensor_id} + "/humidity" WITH {humidity}
```
### GET PROTO
Extracts fields from **binary protobuf** MQTT payloads. `GET PROTOBUF` is the same verb. You need a matching `DEFINE MODEL` with `WITH FORMAT PROTOBUF` (or `BOTH`) and `PROTO_TAG` values that follow the vendor `.proto`. Full models, type mapping, and pitfalls: [Protobuf Models](/latest/lot-language/models/protobuf).
#### Basic Syntax
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
GET PROTO "field_name" IN PAYLOAD AS USING MODEL "ModelName"
```
| Source | Bytes come from |
| ------------------- | -------------------------------------- |
| `IN PAYLOAD` | The MQTT payload that fired the Action |
| `IN GET TOPIC "…"` | Last payload stored for that topic |
| `IN` a LoT variable | A Base64 string in that variable |
`USING MODEL` names the model that describes the payload. Supported `AS` types: `INT`, `DOUBLE`, `STRING`, `BOOL`, `BYTES`.
A missing or mismatched field returns `null` (not a type default).
Declare the parent message (unused arms stay as `ADD OBJECT` so wire numbers do not shift):
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
DEFINE MODEL DeviceCommand WITH FORMAT PROTOBUF COLLAPSED
ADD INT "command_type" PROTO_TAG 1
ADD OBJECT "header" PROTO_TAG 2
```
Then read a field from the triggering payload:
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
DEFINE ACTION OnPlcCommand
ON TOPIC "plant/+/+/command" DO
SET "cmd" WITH (GET PROTO "command_type" IN PAYLOAD AS INT USING MODEL "DeviceCommand")
KEEP TOPIC "plant/pending_command" WITH {cmd}
```
***
## Environment Variables & Secrets
**Version Requirement:** `GET ENV`, `GET SECRET`, `KEEP ENV`, `KEEP SECRET`, `DELETE ENV`, and `DELETE SECRET` are available from **Coreflux Broker v1.9.3** and above.
LoT Actions can read, write, and delete environment variables and encrypted secrets at runtime. Environment variables store non-sensitive configuration (hostnames, ports, URLs). Secrets store sensitive credentials encrypted with AES-256-GCM.
Use environment variables for configuration that can safely appear in logs (hostnames, ports, feature flags). Use secrets for anything sensitive (passwords, API keys, tokens).
### GET ENV
Reads an environment variable. The broker checks the managed `.env` file first, then falls back to process, user, and machine environment variables. Returns an empty string if not found.
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
GET ENV "NAME"
```
#### Examples
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
-- Read connection settings
SET "host" WITH (GET ENV "DB_HOST")
SET "port" WITH (GET ENV "DB_PORT")
SET "apiUrl" WITH (GET ENV "API_BASE_URL")
-- Use in conditional logic
IF GET ENV "FEATURE_FLAG" == "enabled" THEN
PUBLISH TOPIC "features/new" WITH "active"
```
### KEEP ENV
Persists a new environment variable to the `.env` file on disk:
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
KEEP ENV "NAME" WITH
```
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
-- Save a timestamp
KEEP ENV "LAST_RUN" WITH TIMESTAMP "UTC"
-- Save a computed value
KEEP ENV "DEVICE_COUNT" WITH {count}
```
### DELETE ENV
Removes an environment variable from the managed `.env` file:
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
DELETE ENV "NAME"
```
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
DELETE ENV "OLD_CONFIG"
```
### GET SECRET
Reads an encrypted secret from `secrets.json`, decrypting it at runtime. Returns an empty string if the secret does not exist.
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
GET SECRET "NAME"
```
#### Examples
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
-- Read credentials
SET "dbPass" WITH (GET SECRET "DB_PASSWORD")
SET "apiKey" WITH (GET SECRET "API_KEY")
-- Use in conditional logic
IF GET SECRET "LICENSE_KEY" == EMPTY THEN
PUBLISH TOPIC "system/warnings" WITH "No license key configured"
```
### KEEP SECRET
Encrypts and persists a new secret to `secrets.json`:
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
KEEP SECRET "NAME" WITH "value"
```
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
KEEP SECRET "API_KEY" WITH "sk-abc123"
```
### DELETE SECRET
Removes a secret from `secrets.json`:
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
DELETE SECRET "NAME"
```
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
DELETE SECRET "OLD_API_KEY"
```
### Complete Example
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
DEFINE ACTION SecureDataForwarder
ON TOPIC "sensors/+/data" DO
SET "sensor_id" WITH TOPIC POSITION 2
-- Read configuration from environment
SET "apiUrl" WITH (GET ENV "API_BASE_URL")
SET "region" WITH (GET ENV "DEPLOY_REGION")
-- Read credentials from secrets
SET "apiKey" WITH (GET SECRET "API_KEY")
IF {apiUrl} == EMPTY THEN
PUBLISH TOPIC "system/errors" WITH "API_BASE_URL not configured"
ELSE
-- Forward sensor data with region tag
PUBLISH TOPIC "forwarded/" + {region} + "/" + {sensor_id} WITH PAYLOAD
PUBLISH TOPIC "forwarded/" + {sensor_id} + "/timestamp" WITH TIMESTAMP "UTC"
-- Track last forwarded sensor
KEEP ENV "LAST_FORWARDED_SENSOR" WITH {sensor_id}
```
For full details on environment variable resolution order, encryption key management, and deployment configuration, see [Environment Variables & Secrets](/latest/mqtt-broker/secrets-and-env).
***
## Arithmetic Operators
LoT supports standard arithmetic operations that work with numeric types (INT, DOUBLE) and can be combined with GET TOPIC, PAYLOAD, and variables.
### Supported Operators
| Operator | Symbol | Description |
| -------------- | ------ | ------------------------------- |
| Addition | `+` | Adds two values together |
| Subtraction | `-` | Subtracts right value from left |
| Multiplication | `*` | Multiplies two values |
| Division | `/` | Divides left value by right |
| Modulo | `%` | Returns remainder of division |
| Unary Negative | `-` | Negates a value |
### Syntax
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
+
-
*
/
%
-
```
### Basic Examples
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
// Simple arithmetic with literals
SET "sum" WITH (10 + 5)
SET "difference" WITH (100 - 25)
SET "product" WITH (6 * 7)
SET "quotient" WITH (100 / 4)
SET "remainder" WITH (17 % 5)
SET "negative" WITH (-42)
```
### Using with Variables
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
SET "a" WITH 100
SET "b" WITH 25
SET "result" WITH ({a} + {b})
SET "average" WITH (({a} + {b}) / 2)
```
### Using with GET TOPIC
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
// Add values from two different topics
SET "total" WITH (GET TOPIC "sensors/count/a" AS INT + GET TOPIC "sensors/count/b" AS INT)
// Calculate percentage
SET "percentage" WITH (GET TOPIC "current/value" AS DOUBLE / GET TOPIC "max/value" AS DOUBLE * 100)
// Temperature conversion (Celsius to Fahrenheit)
SET "fahrenheit" WITH (GET TOPIC "sensors/temp/celsius" AS DOUBLE * 9 / 5 + 32)
```
### Using with PAYLOAD
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
DEFINE ACTION CalculateWithPayload
ON TOPIC "input/value" DO
SET "doubled" WITH (PAYLOAD AS DOUBLE * 2)
SET "incremented" WITH (PAYLOAD AS INT + 1)
PUBLISH TOPIC "output/doubled" WITH {doubled}
```
### Operator Precedence
Operations follow standard mathematical precedence:
1. Parentheses `()` - highest priority
2. Unary negation `-`
3. Multiplication `*`, Division `/`, Modulo `%`
4. Addition `+`, Subtraction `-`
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
// Without parentheses: 2 + 3 * 4 = 14 (multiplication first)
SET "result1" WITH (2 + 3 * 4)
// With parentheses: (2 + 3) * 4 = 20
SET "result2" WITH ((2 + 3) * 4)
```
### Complete Example
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
DEFINE ACTION TemperatureProcessor
ON TOPIC "sensors/+/temperature" DO
// POSITION 1="sensors", POSITION 2=sensor_id (wildcard), POSITION 3="temperature"
SET "sensor_id" WITH TOPIC POSITION 2
SET "celsius" WITH PAYLOAD AS DOUBLE
// Convert to Fahrenheit
SET "fahrenheit" WITH ({celsius} * 9 / 5 + 32)
// Calculate deviation from setpoint
SET "setpoint" WITH (GET TOPIC "config/setpoint" AS DOUBLE)
SET "deviation" WITH ({celsius} - {setpoint})
SET "deviation_percent" WITH ({deviation} / {setpoint} * 100)
PUBLISH TOPIC "processed/" + {sensor_id} + "/fahrenheit" WITH {fahrenheit}
PUBLISH TOPIC "processed/" + {sensor_id} + "/deviation" WITH {deviation_percent}
```
***
## String Concatenation
The `+` operator concatenates strings, allowing dynamic construction of topics, messages, and values.
### Syntax
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
+
```
### Basic Examples
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
SET "greeting" WITH ("Hello, " + "World!")
SET "fullname" WITH ({firstname} + " " + {lastname})
```
### Dynamic Topic Construction
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
// Build topic paths dynamically
PUBLISH TOPIC "devices/" + {device_id} + "/status" WITH "online"
PUBLISH TOPIC "sensors/" + {sensor_type} + "/" + {location} + "/reading" WITH {value}
// Using with KEEP
KEEP TOPIC "cache/" + {sensor_id} + "/last_reading" WITH PAYLOAD
```
### Combining Strings with Other Types
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
// String + Number (number is converted to string)
SET "message" WITH ("Temperature is: " + {temp})
PUBLISH TOPIC "alerts/temperature" WITH ("High temp: " + {celsius} + "°C")
```
### Complete Example
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
DEFINE ACTION DynamicPublisher
ON TOPIC "input/+/+/data" DO
// POSITION 1="input", POSITION 2=region, POSITION 3=device, POSITION 4="data"
SET "region" WITH TOPIC POSITION 2
SET "device" WITH TOPIC POSITION 3
SET "timestamp" WITH TIMESTAMP "UTC"
// Build dynamic output topic
SET "output_topic" WITH ("processed/" + {region} + "/" + {device})
// Build descriptive message
SET "message" WITH ("Device " + {device} + " in " + {region} + " reported at " + {timestamp})
PUBLISH TOPIC {output_topic} WITH {message}
```
***
## RANDOM Function
Generates random values of various types including numeric ranges and unique identifiers.
### Syntax
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
// Random integer in range (inclusive)
RANDOM BETWEEN AND
RANDOM INT BETWEEN AND
// Random double/float in range
RANDOM DOUBLE BETWEEN AND
// Unique identifiers
RANDOM UUID
RANDOM GUID
RANDOM ULID
```
### Random Integers
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
// Random integer between 1 and 100
SET "dice_roll" WITH (RANDOM BETWEEN 1 AND 6)
SET "random_percent" WITH (RANDOM INT BETWEEN 0 AND 100)
// Using expressions for range
SET "random_in_range" WITH (RANDOM INT BETWEEN {min_value} AND {max_value})
```
### Random Doubles
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
// Random double between 0.0 and 1.0
SET "probability" WITH (RANDOM DOUBLE BETWEEN 0.0 AND 1.0)
// Random temperature for simulation
SET "simulated_temp" WITH (RANDOM DOUBLE BETWEEN 18.5 AND 25.5)
```
### Unique Identifiers
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
// Generate UUID (e.g., "550e8400-e29b-41d4-a716-446655440000")
SET "transaction_id" WITH (RANDOM UUID)
// Generate GUID (same format as UUID)
SET "session_id" WITH (RANDOM GUID)
// Generate ULID (e.g., "01ARZ3NDEKTSV4RRFFQ69G5FAV")
// ULIDs are sortable by time
SET "event_id" WITH (RANDOM ULID)
```
### Use Cases
Generate test data for sensors:
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
DEFINE ACTION SimulateSensor
ON EVERY 5 SECONDS DO
SET "temperature" WITH (RANDOM DOUBLE BETWEEN 20.0 AND 30.0)
SET "humidity" WITH (RANDOM INT BETWEEN 40 AND 80)
SET "pressure" WITH (RANDOM DOUBLE BETWEEN 1000.0 AND 1020.0)
PUBLISH TOPIC "simulated/sensor/temperature" WITH {temperature}
PUBLISH TOPIC "simulated/sensor/humidity" WITH {humidity}
PUBLISH TOPIC "simulated/sensor/pressure" WITH {pressure}
```
Generate unique record identifiers:
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
DEFINE ACTION CreateRecord
ON TOPIC "records/create" DO
SET "record_id" WITH (RANDOM UUID)
SET "timestamp" WITH TIMESTAMP "UTC"
PUBLISH TOPIC "records/" + {record_id} WITH PAYLOAD
PUBLISH TOPIC "records/index/latest" WITH {record_id}
```
Distribute work across workers:
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
DEFINE ACTION RandomRouter
ON TOPIC "incoming/requests" DO
SET "worker" WITH (RANDOM INT BETWEEN 1 AND 4)
PUBLISH TOPIC "workers/" + {worker} + "/queue" WITH PAYLOAD
```
***
## String Transformations
LoT provides string transformation operations for filtering, replacing, and parsing text data.
### FILTER
Extracts content from a string using a regular expression pattern.
#### Syntax
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
FILTER USING REGEX ""
```
#### Supported Sources
* `PAYLOAD` - The incoming message payload
* `GET TOPIC "path"` - Value from an MQTT topic
* `{variable}` - A previously set variable
* `"string literal"` - A hardcoded string
#### Examples
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
// Extract all digits from payload
SET "numbers" WITH FILTER PAYLOAD USING REGEX "[0-9]+"
// Extract from a topic value
SET "extracted" WITH FILTER (GET TOPIC "data/raw") USING REGEX "[a-zA-Z]+"
// Extract email pattern
SET "email" WITH FILTER PAYLOAD USING REGEX "[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}"
// Extract numbers from mixed content
SET "value" WITH FILTER "Temperature: 23.5°C" USING REGEX "[0-9]+\\.?[0-9]*"
```
#### Common Regex Patterns
| Pattern | Description | Example Match |
| ------------------ | ------------------- | ---------------- |
| `[0-9]+` | One or more digits | "123", "4567" |
| `[a-zA-Z]+` | One or more letters | "Hello", "World" |
| `[0-9]+\\.?[0-9]*` | Decimal numbers | "23.5", "100" |
| `[^\\s]+` | Non-whitespace | "word" |
| `\\w+` | Word characters | "test\_123" |
### REPLACE
Replaces occurrences of a pattern with a new value.
#### Syntax
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
REPLACE "" WITH "" IN
```
#### Examples
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
// Replace in payload
SET "cleaned" WITH REPLACE "old" WITH "new" IN PAYLOAD
// Replace in topic value
SET "modified" WITH REPLACE "error" WITH "warning" IN (GET TOPIC "logs/latest")
// Replace in variable
SET "formatted" WITH REPLACE " " WITH "_" IN {sensor_name}
// Remove characters by replacing with empty string
SET "digits_only" WITH REPLACE "[^0-9]" WITH "" IN {mixed_content}
```
#### Multiple Replacements
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
SET "step1" WITH REPLACE "," WITH ";" IN PAYLOAD
SET "step2" WITH REPLACE " " WITH "_" IN {step1}
SET "final" WITH REPLACE "'" WITH "" IN {step2}
```
### SPLIT CSV
Parses CSV (Comma-Separated Values) data from the payload into a structured format.
#### Syntax
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
SPLIT PAYLOAD CSV [SEPARATOR ""] [HEADERS PRESENT|NONE]
```
#### Parameters
| Parameter | Description | Default |
| ----------------- | ------------------------------- | ----------- |
| `SEPARATOR` | The delimiter character | `,` (comma) |
| `HEADERS PRESENT` | First row contains column names | - |
| `HEADERS NONE` | No header row | - |
#### Example
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
DEFINE ACTION ProcessCSVData
ON TOPIC "import/csv" DO
// Payload: "name,value,unit\nTemperature,23.5,C\nHumidity,65,percent"
SET "parsed" WITH SPLIT PAYLOAD CSV SEPARATOR "," HEADERS PRESENT
PUBLISH TOPIC "import/parsed" WITH {parsed}
```
SPLIT CSV currently only supports PAYLOAD as input. Support for GET TOPIC and variables is planned for future releases.
***
## Topic Operations
### PUBLISH vs KEEP — When to Use Which
| Operation | Retained | Typical use |
| --------------- | -------- | --------------------------------------------------------------------------- |
| `PUBLISH TOPIC` | No | Fire to current subscribers, drive routes, dashboards, and external systems |
| `KEEP TOPIC` | Yes | Broker-side state shared between actions — readable with `GET TOPIC` |
Use `KEEP TOPIC` for inter-action state (counters, mode flags, last-known values). Use `PUBLISH TOPIC` when routes, UIs, or other subscribers must react to the message.
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
KEEP TOPIC "state/mode" WITH "running"
KEEP TOPIC "state/lastTemp" WITH {temp}
SET "mode" WITH (GET TOPIC "state/mode" AS STRING)
PUBLISH TOPIC "sensors/data" WITH {temp}
PUBLISH TOPIC "alerts/zone1" WITH "CRITICAL"
```
Do **not** use `KEEP TOPIC` to drive route events — that path is unreliable. Use `PUBLISH TOPIC` when a route or external consumer must react to the message.
### PUBLISH TOPIC
Sends a **non-retained** message to an MQTT topic. Current subscribers receive it immediately; the message can trigger other actions or routes listening on that topic.
#### Syntax
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
PUBLISH TOPIC "" WITH
```
#### Value Types
You can publish various value types:
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
// String
PUBLISH TOPIC "system/status" WITH "online"
// Number
PUBLISH TOPIC "sensors/reading" WITH 23.5
// Variable
PUBLISH TOPIC "output/value" WITH {calculated_result}
// Payload passthrough (string path — may corrupt non-UTF-8 data)
PUBLISH TOPIC "forwarded/data" WITH PAYLOAD
// Timestamp
PUBLISH TOPIC "events/timestamp" WITH TIMESTAMP "UTC"
```
#### Binary passthrough — `PAYLOAD AS BYTES`
Use `AS BYTES` when the incoming MQTT payload must be forwarded or retained bit-for-bit — for example protobuf messages, Modbus frames, or image chunks. Untyped `WITH PAYLOAD` still uses the string publish path and may corrupt non-UTF-8 data. To **decode or encode** protobuf fields rather than forward the blob, use [Protobuf Models](/latest/lot-language/models/protobuf) (`GET PROTO` / `PUBLISH MODEL`).
```lot wrap focus={3} theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
DEFINE ACTION ForwardBinary
ON TOPIC "binary/in" DO
PUBLISH TOPIC "binary/out" WITH PAYLOAD AS BYTES
```
`KEEP TOPIC` and `SET` accept the same binary form, so you can store raw frames for later reads with `GET TOPIC`:
```lot wrap focus={3-4} theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
DEFINE ACTION KeepBinaryState
ON TOPIC "binary/in" DO
KEEP TOPIC "state/raw" WITH PAYLOAD AS BYTES
SET "frame" WITH PAYLOAD AS BYTES
```
**Binary path limitations**
* Do **not** chain string transforms (`TRIM`, `FILTER`, `REPLACE`, `SPLIT`) after `PAYLOAD AS BYTES` on publish or keep — the parser rejects that combination.
* Empty payloads are not published or stored on the binary path.
**Cannot Use Inline JSON Objects**
`PUBLISH TOPIC` does **not** support inline JSON object literals. This will NOT work:
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
// ❌ WRONG - This does not work
PUBLISH TOPIC "output/data" WITH {"temperature": 23.5, "humidity": 65}
```
**Why?** `PUBLISH TOPIC` is designed for simple scalar values (strings, numbers, timestamps, variables). It doesn't have the type system or schema validation needed for structured JSON data.
**Solution:** For structured JSON output, use [Models](/latest/lot-language/models/overview) with `PUBLISH MODEL`:
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
// ✅ CORRECT - Define a model and publish it
DEFINE MODEL SensorData COLLAPSED
ADD DOUBLE "temperature"
ADD DOUBLE "humidity"
DEFINE ACTION PublishStructuredData
ON TOPIC "sensors/input" DO
PUBLISH MODEL SensorData TO "output/data" WITH
temperature = 23.5
humidity = 65
```
Models provide type safety, schema validation, and proper JSON serialization. See [Publishing Models](/latest/lot-language/models/publish-model) for complete guidance.
#### Dynamic Topics
Build topic paths dynamically using string concatenation:
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
PUBLISH TOPIC "devices/" + {device_id} + "/status" WITH "active"
PUBLISH TOPIC "sensors/" + {sensor_type} + "/" + {location} + "/reading" WITH {value}
```
#### Multiple Publishes
Actions can publish to multiple topics:
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
DEFINE ACTION MultiPublish
ON TOPIC "input/data" DO
PUBLISH TOPIC "output/raw" WITH PAYLOAD
PUBLISH TOPIC "output/timestamp" WITH TIMESTAMP "UTC"
PUBLISH TOPIC "output/source" WITH "sensor_001"
```
**Triggering Other Actions:** When you publish to a topic, any action with `ON TOPIC` matching that path will execute. This enables chaining actions together for complex workflows.
### KEEP TOPIC
Stores a retained value on a topic that persists between action executions. Unlike `SET` (which exists only during one execution), `KEEP` values survive restarts and are readable with `GET TOPIC` from other actions or routes.
**KEEP vs PUBLISH:** `PUBLISH TOPIC` is non-retained and drives routes and external subscribers. `KEEP TOPIC` is retained and is the right choice for broker-side state you read back with `GET TOPIC`.
#### When to Use KEEP
| Use Case | Example |
| ------------------------------ | ------------------------------------------------- |
| Cache last known value | `KEEP TOPIC "cache/last_temp" WITH {temperature}` |
| Store state between runs | `KEEP TOPIC "state/counter" WITH {count}` |
| Save intermediate calculations | `KEEP TOPIC "calc/running_avg" WITH {avg}` |
#### Syntax
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
KEEP TOPIC "" WITH
KEEP TOPIC "" WITH PAYLOAD AS BYTES
```
Retain string or variable state, including dynamic topic paths:
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
KEEP TOPIC "state/mode" WITH "running"
KEEP TOPIC "device/" + {id} + "/config" WITH {config}
```
#### Example: Persistent Counter
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
DEFINE ACTION PersistentCounter
ON EVERY 10 SECONDS DO
SET "current" WITH (GET TOPIC "state/counter")
IF {current} == EMPTY THEN
KEEP TOPIC "state/counter" WITH 1
ELSE
KEEP TOPIC "state/counter" WITH ({current} + 1)
PUBLISH TOPIC "stats/count" WITH (GET TOPIC "state/counter")
```
#### KEEP vs PUBLISH Comparison
| Operation | Retained | Readable with `GET TOPIC` | Typical use |
| --------------- | -------- | ------------------------- | ---------------------------------------- |
| `PUBLISH TOPIC` | No | Yes (until overwritten) | Routes, dashboards, external subscribers |
| `KEEP TOPIC` | Yes | Yes | Shared state between actions |
***
## Control Flow
### IF ... THEN ... ELSE
Execute different logic based on conditions.
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
IF THEN
ELSE
```
#### Comparison Operators
| Operator | Description | Example |
| ---------------- | ---------------- | ------------------------ |
| `EQUALS` or `==` | Equality | `IF {value} EQUALS 100` |
| `!=` | Not equal | `IF {status} != "error"` |
| `>` | Greater than | `IF {temp} > 80` |
| `<` | Less than | `IF {count} < 10` |
| `>=` | Greater or equal | `IF {level} >= 50` |
| `<=` | Less or equal | `IF {pressure} <= 100` |
#### Basic Conditionals
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
IF {temperature} > 80 THEN
PUBLISH TOPIC "alerts/high_temp" WITH {temperature}
ELSE
PUBLISH TOPIC "status/normal" WITH "OK"
```
#### Nested Conditionals
For multiple conditions, nest IF statements:
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
IF {temperature} > 90 THEN
PUBLISH TOPIC "alerts/critical" WITH "CRITICAL: " + {temperature}
ELSE
IF {temperature} > 80 THEN
PUBLISH TOPIC "alerts/warning" WITH "WARNING: " + {temperature}
ELSE
IF {temperature} > 70 THEN
PUBLISH TOPIC "alerts/info" WITH "Elevated: " + {temperature}
ELSE
PUBLISH TOPIC "status/normal" WITH "OK"
```
#### Checking for Empty Topics
Use `EMPTY` to check if a topic has no value:
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
IF GET TOPIC "data/value" == EMPTY THEN
PUBLISH TOPIC "data/value" WITH 0
ELSE
PUBLISH TOPIC "data/value" WITH (GET TOPIC "data/value" + 1)
```
#### Combining with Expressions
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
// Check calculated values
SET "average" WITH (({temp1} + {temp2}) / 2)
IF {average} > {threshold} THEN
PUBLISH TOPIC "alerts/average_high" WITH {average}
// String comparisons
IF {status} EQUALS "active" THEN
PUBLISH TOPIC "devices/active_count" WITH (GET TOPIC "devices/active_count" + 1)
```
***
## Timestamps
Add timestamps to your messages for tracking, logging, and debugging. LoT supports two timestamp formats.
### Supported Formats
| Format | Output | Use Case |
| ------------------ | ---------------------- | ----------------------------------------------------- |
| `TIMESTAMP "UTC"` | `2025-10-25T14:30:15Z` | Human-readable logs, APIs, ISO 8601 compliance |
| `TIMESTAMP "UNIX"` | `1729864215` | Databases, calculations, cross-platform compatibility |
### Syntax
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
TIMESTAMP "UTC" // ISO 8601 format
TIMESTAMP "UNIX" // Unix epoch (seconds since 1970-01-01)
```
### Using with PUBLISH and KEEP
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
// Add timestamp to published messages
PUBLISH TOPIC "events/created" WITH TIMESTAMP "UTC"
PUBLISH TOPIC "sensors/reading/time" WITH TIMESTAMP "UNIX"
// Store timestamp for later reference
KEEP TOPIC "cache/last_update" WITH TIMESTAMP "UNIX"
```
### Using with SET
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
// Store timestamp in a variable
SET "event_time" WITH TIMESTAMP "UTC"
SET "process_start" WITH TIMESTAMP "UNIX"
// Use in string concatenation
SET "log_message" WITH ("Event occurred at " + TIMESTAMP "UTC")
PUBLISH TOPIC "logs/events" WITH {log_message}
```
### Complete Example
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
DEFINE ACTION AuditLogger
ON TOPIC "actions/+/executed" DO
SET "action_name" WITH TOPIC POSITION 2
SET "utc_time" WITH TIMESTAMP "UTC"
SET "unix_time" WITH TIMESTAMP "UNIX"
// Human-readable log
PUBLISH TOPIC "audit/" + {action_name} + "/log"
WITH ("Action " + {action_name} + " executed at " + {utc_time})
// Machine-readable timestamp for database storage
PUBLISH TOPIC "audit/" + {action_name} + "/timestamp" WITH {unix_time}
```
***
## Callable Actions
LoT supports callable actions - actions that can be invoked by other actions with input parameters and return values. This enables modular, reusable logic blocks.
For reusable logic, on-demand scripts, and composition patterns, see [Callable ACTIONs](./callable-actions).
### Defining a Callable Action
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
DEFINE ACTION ActionName
INPUT param1 AS
INPUT param2 AS
DO
RETURN
OUTPUT variable1
OUTPUT variable2
```
#### Supported Input Types
* `STRING`
* `INT`
* `DOUBLE`
* `BOOL`
* `JSON`
### Calling an Action
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
CALL ACTION ActionName
WITH param1 = , param2 =
RETURN var1, var2
```
### Basic Example
Create a reusable calculation:
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
DEFINE ACTION CalculateAverage
INPUT value1 AS DOUBLE
INPUT value2 AS DOUBLE
DO
SET "sum" WITH ({value1} + {value2})
SET "avg" WITH ({sum} / 2)
RETURN
OUTPUT avg
```
Use the callable action:
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
DEFINE ACTION ProcessSensorPair
ON TOPIC "sensors/pair/data" DO
SET "temp1" WITH (GET JSON "sensor1" IN PAYLOAD AS DOUBLE)
SET "temp2" WITH (GET JSON "sensor2" IN PAYLOAD AS DOUBLE)
CALL ACTION CalculateAverage
WITH value1 = {temp1}, value2 = {temp2}
RETURN result
PUBLISH TOPIC "sensors/average" WITH {result}
```
### Multiple Outputs
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
DEFINE ACTION AnalyzeValue
INPUT value AS DOUBLE
INPUT threshold AS DOUBLE
DO
SET "is_above" WITH ({value} > {threshold})
SET "difference" WITH ({value} - {threshold})
SET "percentage" WITH ({value} / {threshold} * 100)
RETURN
OUTPUT is_above
OUTPUT difference
OUTPUT percentage
```
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
DEFINE ACTION CheckThreshold
ON TOPIC "sensors/+/value" DO
SET "sensor_id" WITH TOPIC POSITION 2
SET "current_value" WITH PAYLOAD AS DOUBLE
SET "threshold" WITH (GET TOPIC "config/threshold" AS DOUBLE)
CALL ACTION AnalyzeValue
WITH value = {current_value}, threshold = {threshold}
RETURN above, diff, pct
PUBLISH TOPIC "analysis/" + {sensor_id} + "/above_threshold" WITH {above}
PUBLISH TOPIC "analysis/" + {sensor_id} + "/difference" WITH {diff}
PUBLISH TOPIC "analysis/" + {sensor_id} + "/percentage" WITH {pct}
```
### Utility Action Library
Build reusable utility actions for common operations:
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
// Reusable temperature conversion
DEFINE ACTION ConvertCelsiusToFahrenheit
INPUT celsius AS DOUBLE
DO
SET "fahrenheit" WITH ({celsius} * 9 / 5 + 32)
RETURN
OUTPUT fahrenheit
// Reusable percentage calculation
DEFINE ACTION CalculatePercentage
INPUT value AS DOUBLE
INPUT total AS DOUBLE
DO
SET "pct" WITH ({value} / {total} * 100)
RETURN
OUTPUT pct
// Main processing action using utilities
DEFINE ACTION ProcessTemperatureReading
ON TOPIC "sensors/+/celsius" DO
SET "sensor_id" WITH TOPIC POSITION 2
SET "temp_c" WITH PAYLOAD AS DOUBLE
SET "max_temp" WITH (GET TOPIC "config/max_temperature" AS DOUBLE)
CALL ACTION ConvertCelsiusToFahrenheit
WITH celsius = {temp_c}
RETURN temp_f
CALL ACTION CalculatePercentage
WITH value = {temp_c}, total = {max_temp}
RETURN capacity
PUBLISH TOPIC "sensors/" + {sensor_id} + "/fahrenheit" WITH {temp_f}
PUBLISH TOPIC "sensors/" + {sensor_id} + "/capacity_percent" WITH {capacity}
```
***
## TRIGGER Statement
The TRIGGER statement allows actions to programmatically trigger route events or mappings. This is useful for initiating data pipeline operations or triggering external integrations.
### Syntax
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
TRIGGER "" TO FOR
```
### Components
| Component | Description |
| -------------- | --------------------------------------------------- |
| `route_name` | The name of the route to trigger |
| `data` | The data to send (PAYLOAD, variable, or expression) |
| `mapping_name` | The specific mapping or event within the route |
### Basic Examples
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
// Trigger a route with payload data
TRIGGER "EmailRoute" TO PAYLOAD FOR "SendAlert"
// Trigger with a variable
SET "message" WITH ("Alert: " + {sensor_id} + " exceeded threshold")
TRIGGER "NotificationRoute" TO {message} FOR "EmailMapping"
// Trigger with a literal value
TRIGGER "DatabaseRoute" TO "backup_requested" FOR "BackupEvent"
```
### Complete Example
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
DEFINE ACTION AlertHandler
ON TOPIC "alerts/+/critical" DO
SET "alert_type" WITH TOPIC POSITION 2
SET "alert_data" WITH PAYLOAD
SET "timestamp" WITH TIMESTAMP "UTC"
// Build alert message
SET "message" WITH ("Critical alert [" + {alert_type} + "] at " + {timestamp})
// Trigger email notification route
TRIGGER "EmailNotification" TO {message} FOR "CriticalAlertEmail"
// Also trigger database storage
TRIGGER "DatabaseStorage" TO {alert_data} FOR "StoreAlert"
PUBLISH TOPIC "alerts/processed" WITH {alert_type}
```
### Use Cases
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
DEFINE ACTION SendDailyReport
ON EVERY 24 HOURS DO
SET "report_data" WITH (GET TOPIC "reports/daily/summary")
TRIGGER "EmailRoute" TO {report_data} FOR "DailyReportEmail"
```
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
DEFINE ACTION ArchiveOldData
ON EVERY 1 HOUR DO
SET "cutoff_time" WITH TIMESTAMP "UNIX"
TRIGGER "DatabaseRoute" TO {cutoff_time} FOR "ArchiveEvent"
```
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
DEFINE ACTION NotifyExternalSystem
ON TOPIC "events/important" DO
TRIGGER "RestAPIRoute" TO PAYLOAD FOR "WebhookEvent"
```
***
## CALL MCP
CALL MCP allows actions to invoke tools provided by MCP (Model Context Protocol) routes. This enables integration with AI assistants and external tools.
### Prerequisites
* An MCP route must be defined and active
* The route must expose the tool you want to call
### Syntax
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
CALL MCP "RouteName.tool_name"
WITH (arg1 = , arg2 = , ...)
RETURN AS {variable_name}
```
### Example MCP Route
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
DEFINE ROUTE FileSystem WITH TYPE MCP
ADD MCP_CONFIG
WITH SERVER_COMMAND "npx"
WITH SERVER_ARGUMENTS "-y, @modelcontextprotocol/server-filesystem, /data"
WITH CLIENT_NAME "CorefluxBroker"
```
### Basic Examples
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
// Call an MCP tool with arguments
CALL MCP "FileSystem.read_file"
WITH (path = "/data/config.json")
RETURN AS {file_content}
// Call without arguments
CALL MCP "MyMCPRoute.list_resources"
RETURN AS {resources}
// Call with multiple arguments
CALL MCP "MyMCPRoute.echo"
WITH (message = "Hello", count = 3)
RETURN AS {response}
```
### Complete Example
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
DEFINE ACTION ReadDeviceConfig
ON TOPIC "devices/+/request_config" DO
SET "device_id" WITH TOPIC POSITION 2
SET "config_path" WITH "/configs/" + {device_id} + ".json"
CALL MCP "FileSystem.read_file"
WITH (path = {config_path})
RETURN AS {config_data}
IF {config_data} == EMPTY THEN
PUBLISH TOPIC "devices/" + {device_id} + "/config/error" WITH "Config not found"
ELSE
PUBLISH TOPIC "devices/" + {device_id} + "/config" WITH {config_data}
```
***
## Complete Example
This example combines the key operations covered on this page—SET, GET TOPIC, GET JSON, arithmetic, IF/THEN, PUBLISH, and KEEP—in a realistic sensor processing action.
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
DEFINE ACTION ComprehensiveProcessor
ON TOPIC "sensors/+/data" DO
// Extract sensor ID from topic path (POSITION 2 = the wildcard)
SET "sensor_id" WITH TOPIC POSITION 2
// Parse JSON payload
SET "temperature" WITH (GET JSON "temp" IN PAYLOAD AS DOUBLE)
// Convert to Fahrenheit
SET "fahrenheit" WITH ({temperature} * 9 / 5 + 32)
// Conditional alerting
IF {temperature} > 80 THEN
PUBLISH TOPIC "alerts/" + {sensor_id} WITH "High temp: " + {temperature}
// Publish processed data
PUBLISH TOPIC "processed/" + {sensor_id} + "/temperature" WITH {temperature}
PUBLISH TOPIC "processed/" + {sensor_id} + "/fahrenheit" WITH {fahrenheit}
PUBLISH TOPIC "processed/" + {sensor_id} + "/timestamp" WITH TIMESTAMP "UTC"
// Cache for later reference
KEEP TOPIC "cache/" + {sensor_id} + "/last_reading" WITH PAYLOAD
```
### What This Action Does
1. **Triggers** on any message to `sensors/+/data` topics
2. **Extracts** the sensor ID from the topic path
3. **Parses** the JSON payload to get temperature value
4. **Converts** Celsius to Fahrenheit using arithmetic
5. **Alerts** if temperature exceeds threshold
6. **Publishes** formatted data to processed topics
7. **Caches** the raw payload for future reference
***
## Quick Reference
| Operation | Category | Description |
| ----------------------------------------- | -------------- | ----------------------------------------------------------- |
| `+, -, *, /, %` | Arithmetic | Mathematical operations |
| `+ (strings)` | String | Concatenation |
| `RANDOM BETWEEN` | Generation | Random numbers |
| `RANDOM UUID/GUID/ULID` | Generation | Unique identifiers |
| `FILTER ... USING REGEX` | Transformation | Extract with regex |
| `REPLACE ... WITH ... IN` | Transformation | String replacement |
| `SPLIT ... CSV` | Transformation | Parse CSV data |
| `GET ENV` | Configuration | Read environment variable |
| `GET SECRET` | Configuration | Read encrypted secret |
| `KEEP ENV` | Configuration | Persist environment variable |
| `KEEP SECRET` | Configuration | Persist encrypted secret |
| `DELETE ENV` | Configuration | Remove environment variable |
| `DELETE SECRET` | Configuration | Remove secret |
| `CALL PYTHON` | Integration | Execute Python functions |
| `CALL MCP` | Integration | Invoke MCP tools |
| `CALL ACTION` | Control | Call other actions |
| `TRIGGER` | Control | Trigger route events |
| `PUBLISH TOPIC` | Topic | Non-retained publish to routes, dashboards, and subscribers |
| `PUBLISH TOPIC ... WITH PAYLOAD AS BYTES` | Topic | Forward binary payloads bit-for-bit |
| `KEEP TOPIC` | Topic | Retained broker-side state readable with `GET TOPIC` |
| `KEEP TOPIC ... WITH PAYLOAD AS BYTES` | Topic | Retain binary payloads without string conversion |
***
## Next Steps
Extend actions with Python functions for complex logic.
Run and deploy these operations from VS Code with LoT Notebooks.
# ACTION Overview
Source: https://docs.coreflux.org/latest/lot-language/actions/overview
Learn how LoT Actions automate MQTT data processing with event-driven logic that runs inside the broker
## Overview
Actions are executable logic blocks that run directly inside the Coreflux MQTT broker. They react to events—incoming messages, time intervals, or manual triggers—and perform operations like data transformation, routing, and alerting.
**Think of Actions like a mail sorting machine.** When a letter (message) arrives, the machine reads the address (topic), decides where it should go (routing logic), and sends it to the right mailbox (publishes to a topic). All of this happens instantly, without needing a person to manually sort each letter.
## Action Capabilities
| Capability | Description |
| -------------------------- | -------------------------------------------------------------------------- |
| **Event-Driven Execution** | Trigger on MQTT messages with wildcard support for flexible topic matching |
| **Time-Based Scheduling** | Execute at regular intervals—seconds to weeks—for monitoring and reporting |
| **Data Transformation** | Extract JSON fields, perform calculations, and restructure payloads inline |
| **Conditional Logic** | Route data, set thresholds, and make decisions with IF/THEN/ELSE |
***
## When to Use Each Pattern
| Pattern | Use When |
| -------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------- |
| **Time-Based** (`ON EVERY`) | Periodic tasks: heartbeats, polling, scheduled reports |
| **Topic-Based** (`ON TOPIC`, `ON CHANGE`) | Event-driven processing: every message (`ON TOPIC`) or only when the payload changes (`ON CHANGE`) |
| **Client lifecycle** (`ON CONNECT`, `ON DISCONNECT`, `ON SUBSCRIBE`) | Session events: audit connections, track subscriptions, react when clients join or leave |
| **Callable** (no trigger) | Reusable utilities invoked by other actions or commands |
| **Python-Extended** (`CALL PYTHON`) | Complex calculations, external libraries, data validation |
***
## In This Section
Start here to learn the DEFINE ACTION structure and build your first action.
Choose the right trigger type for your use case: time-based, topic-based, or callable.
Master SET, GET, PUBLISH, conditionals, and all available LoT operations.
Extend actions with Python when you need complex logic or external libraries.
***
## Action Types
LoT supports three action patterns:
### Time-Based Actions
Execute at regular intervals using `ON EVERY`:
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
DEFINE ACTION SystemHeartbeat
ON EVERY 5 SECONDS DO
PUBLISH TOPIC "system/status" WITH "online"
PUBLISH TOPIC "system/timestamp" WITH TIMESTAMP "UTC"
```
**Key characteristics:**
* Runs on a schedule regardless of incoming data
* Useful for monitoring, polling, and periodic reports
* Supported units: MILLISECONDS, SECONDS, MINUTES, HOURS, DAYS, WEEKS, MONTHS, YEARS
* Calendar-aware scheduling: `ON EVERY WEEKDAY AT "HH:MM:SS"` and `ON EVERY WEEKEND AT "HH:MM:SS"`
### Topic-Based Actions
Execute when MQTT messages arrive using `ON TOPIC` (every matching message) or `ON CHANGE` (only when the payload differs from the last message on that topic):
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
DEFINE ACTION TemperatureProcessor
ON TOPIC "sensors/+/temperature" DO
SET "sensor_id" WITH TOPIC POSITION 2
SET "temp" WITH (GET JSON "value" IN PAYLOAD AS DOUBLE)
IF {temp} > 80 THEN
PUBLISH TOPIC "alerts/high_temp/" + {sensor_id} WITH PAYLOAD
ELSE
PUBLISH TOPIC "processed/" + {sensor_id} WITH PAYLOAD
```
Trigger only when the message payload changes from the last published value with `ON CHANGE`:
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
DEFINE ACTION TemperatureOnChange
ON CHANGE "sensors/+/temperature" DO
SET "sensor_id" WITH TOPIC POSITION 2
SET "temp" WITH PAYLOAD AS DOUBLE
PUBLISH TOPIC "analytics/" + {sensor_id} WITH {temp}
```
**Key characteristics:**
* `ON TOPIC` runs on every matching publish; `ON CHANGE` skips identical payloads on the same topic
* Supports `+` (single-level) and `#` (multi-level) [MQTT wildcards](/latest/mqtt-broker/protocol#wildcards)
* Access payload data via `PAYLOAD` and `GET JSON`
* Extract topic segments with `TOPIC POSITION`
See [Action Triggers](./events#topic-based-triggers) for when to choose each trigger and a side-by-side comparison.
### Client Lifecycle Actions
Execute when MQTT clients connect, disconnect, or subscribe using `ON CONNECT`, `ON DISCONNECT`, or `ON SUBSCRIBE`:
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
DEFINE ACTION AuditConnect
ON CONNECT DO
PUBLISH TOPIC "audit/connect/" + CLIENTID WITH USER + "@" + ENDPOINT
DEFINE ACTION TrackSubscriptions
ON SUBSCRIBE "sensors/#" DO
PUBLISH TOPIC "audit/subscriptions" WITH CLIENTID + " -> " + SUBSCRIBETOPIC
```
**Key characteristics:**
* Runs on session events instead of publishes — useful for audit trails and connection dashboards
* Access session context via `CLIENTID`, `USER`, `ENDPOINT`, `CLEANSESSION`, `DISCONNECTREASON`, `SUBSCRIBETOPIC`, and `SUBSCRIBEQOS`
* `ON SUBSCRIBE` supports optional MQTT wildcard patterns, same semantics as `ON TOPIC`
* The broker only evaluates lifecycle hooks when at least one action defines them
See [Client Lifecycle Triggers](./events#client-lifecycle-triggers) for examples and the session context variable reference.
### Callable Actions
Actions without triggers are invoked manually or by other actions:
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
DEFINE ACTION CalculateAverage
INPUT value1 AS DOUBLE
INPUT value2 AS DOUBLE
DO
SET "avg" WITH (({value1} + {value2}) / 2)
RETURN
OUTPUT avg
```
**Key characteristics:**
* No automatic trigger—must be invoked explicitly
* Can accept input parameters and return values
* Invoked via `CALL ACTION` from other ACTIONs
* Ideal for reusable utility functions
Learn more in [Callable ACTIONs](./callable-actions)—reusable logic, on-demand scripts, and `CALL ACTION`.
***
## Core Operations
Actions use these operations to process data:
SET, GET TOPIC, GET JSON, PUBLISH, and IF/THEN are the core operations for working with data in actions. See the [Operations Reference](./operations) for complete details.
Create and use variables within an action:
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
SET "sensor_id" WITH TOPIC POSITION 2
SET "temperature" WITH (GET TOPIC "sensors/temp" AS DOUBLE)
SET "humidity" WITH (GET JSON "humidity" IN PAYLOAD AS DOUBLE)
PUBLISH TOPIC "output/" + {sensor_id} WITH {temperature}
```
Read from and write to MQTT topics:
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
// Read from a topic
SET "last_value" WITH (GET TOPIC "sensors/temperature" AS DOUBLE)
// Publish to subscribers
PUBLISH TOPIC "output/data" WITH {temperature}
// Store internally (not broadcast)
KEEP TOPIC "cache/last_reading" WITH PAYLOAD
```
Parse JSON payloads and extract fields:
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
// For payload: {"temp": 23.5, "humidity": 65}
SET "temp" WITH (GET JSON "temp" IN PAYLOAD AS DOUBLE)
SET "humidity" WITH (GET JSON "humidity" IN PAYLOAD AS INT)
// Nested paths
SET "city" WITH (GET JSON "location.city" IN PAYLOAD AS STRING)
```
Implement branching logic:
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
IF {temperature} > 80 THEN
PUBLISH TOPIC "alerts/high" WITH {temperature}
ELSE IF {temperature} < 20 THEN
PUBLISH TOPIC "alerts/low" WITH {temperature}
ELSE
PUBLISH TOPIC "status/normal" WITH "OK"
```
***
## Example: Complete Sensor Processor
This example combines topic matching, JSON extraction, conditional routing, and state updates in a realistic sensor processing action.
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
DEFINE ACTION SensorDataProcessor
ON TOPIC "sensors/+/raw" DO
// Extract sensor ID from topic path
// Topic: sensors/temp001/raw → POSITION 2 = "temp001"
SET "sensor_id" WITH TOPIC POSITION 2
// Parse JSON payload
SET "temp_value" WITH (GET JSON "temperature" IN PAYLOAD AS DOUBLE)
SET "humidity" WITH (GET JSON "humidity" IN PAYLOAD AS DOUBLE)
// Conditional routing based on thresholds
IF {temp_value} > 80 THEN
PUBLISH TOPIC "alerts/high_temp/" + {sensor_id} WITH PAYLOAD
ELSE IF {temp_value} < 10 THEN
PUBLISH TOPIC "alerts/low_temp/" + {sensor_id} WITH PAYLOAD
ELSE
PUBLISH TOPIC "sensors/normal/" + {sensor_id} WITH PAYLOAD
// Always update the processed output
PUBLISH TOPIC "sensors/processed/" + {sensor_id} WITH PAYLOAD
// Record the last update timestamp
PUBLISH TOPIC "sensors/" + {sensor_id} + "/last_update" WITH TIMESTAMP "UTC"
```
**What this does:**
1. Triggers when any message arrives at `sensors/*/raw`
2. Extracts the sensor ID from the topic path
3. Parses temperature and humidity from the JSON payload
4. Routes alerts based on temperature thresholds
5. Publishes processed data and timestamps
***
## Actions vs. Models
Actions and Models serve different purposes in LoT:
| Aspect | Actions | Models |
| -------------- | --------------------------------- | ---------------------------- |
| **Purpose** | Process and route data | Structure and format data |
| **Execution** | Event-driven logic | Schema definition |
| **Output** | Any MQTT operation | Structured JSON |
| **Complexity** | Conditionals, loops, Python | Field mapping, calculations |
| **Best For** | Routing, alerting, transformation | Data aggregation, formatting |
Use them together: Actions process and route data, then publish it using a Model to produce structured JSON output.
***
## Next Steps
Learn the complete DEFINE ACTION syntax and components.
Master time-based, topic-based, lifecycle, and ON START triggers.
Reusable logic, on-demand scripts, and `CALL ACTION`.
# ACTION Python Integration
Source: https://docs.coreflux.org/latest/lot-language/actions/python-integration
Extend LoT actions with Python functions for complex calculations and data processing
## Overview
While LoT handles most IoT automation elegantly, some tasks need the full power of a programming language. Python integration lets you call Python functions directly from your LoT actions—no external services, no network calls, no containers. Your Python code runs inside the broker alongside your actions.
**Think of Python as a specialist you can call for help.** Your LoT action handles the routine work (reading sensors, routing messages, checking thresholds), but when it encounters something complex—statistical analysis, regex parsing, or calling an external library—it hands the task to Python and gets the result back instantly.
***
## When to Use Python vs. LoT
Before diving into syntax, understand when Python makes sense:
| Use Python For | Use LoT For |
| -------------------------------------- | ------------------------------------ |
| Complex calculations (statistics, ML) | Simple math (+, -, \*, /) |
| Regex and advanced string parsing | String concatenation |
| Data validation with complex rules | Basic conditionals (IF/THEN) |
| External library calls (numpy, pandas) | MQTT operations (PUBLISH, GET TOPIC) |
| JSON transformation and restructuring | Field extraction (GET JSON) |
If you can do it in LoT, do it in LoT. Python adds overhead—use it when LoT's built-in operations aren't enough.
***
## Writing Python Scripts
Every Python script in LoT must declare its name on the **first line** using a `# Script Name:` comment. That name is what you use in `CALL PYTHON "ScriptName.function_name"`—the part before the dot must match the header exactly.
```python wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
# Script Name: Calculator
def add(a, b):
return a + b
def multiply(a, b):
return a * b
```
The first line must be `# Script Name: YourScriptName`. Without this header, the broker does not register the script and `CALL PYTHON` cannot find it.
| Header | LoT call |
| ------------------------------ | ----------------------------------------------- |
| `# Script Name: Calculator` | `CALL PYTHON "Calculator.add"` |
| `# Script Name: DataProcessor` | `CALL PYTHON "DataProcessor.calculate_average"` |
Define one or more functions below the header. Each function is callable from LoT by name.
***
## CALL PYTHON Syntax
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
CALL PYTHON "ScriptName.function_name"
WITH (parameter1, parameter2, ...)
RETURN AS {variable_name}
```
## Parameter Passing
### Single Parameter
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
CALL PYTHON "Processor.validate"
WITH (PAYLOAD)
RETURN AS {result}
```
### Multiple Parameters
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
SET "min_val" WITH (GET TOPIC "config/min")
SET "max_val" WITH (GET TOPIC "config/max")
CALL PYTHON "Validator.check_range"
WITH (PAYLOAD, {min_val}, {max_val})
RETURN AS {result}
```
## Return Value Handling
Python functions can return simple values or complex JSON objects. The `RETURN AS` clause captures the result into a LoT variable.
### Simple Values
For basic calculations, return a single value:
**Python script:**
```python wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
# Script Name: Calculator
def add_numbers(a, b):
return float(a) + float(b)
```
**LoT action calling the function:**
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
CALL PYTHON "Calculator.add_numbers" WITH (5, 3) RETURN AS {result}
PUBLISH TOPIC "math/result" WITH {result}
```
### JSON Objects
For richer data, return a dictionary—it becomes JSON that LoT can parse:
**Python script returning a dictionary:**
```python wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
# Script Name: Analyzer
def analyze_data(value):
return {
"original": float(value),
"doubled": float(value) * 2,
"squared": float(value) ** 2
}
```
**LoT action extracting fields from the result:**
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
CALL PYTHON "Analyzer.analyze_data" WITH (5) RETURN AS {result}
PUBLISH TOPIC "analysis/full" WITH {result}
PUBLISH TOPIC "analysis/doubled" WITH (GET JSON "doubled" IN {result} AS DOUBLE)
```
## Error Handling
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
CALL PYTHON "Calculator.safe_division" WITH (10, 2) RETURN AS {result}
SET "success" WITH (GET JSON "success" IN {result} AS BOOL)
IF {success} EQUALS TRUE THEN
PUBLISH TOPIC "calculation/result" WITH (GET JSON "result" IN {result} AS DOUBLE)
ELSE
PUBLISH TOPIC "calculation/error" WITH (GET JSON "error" IN {result} AS STRING)
```
## Best Practices
Put `# Script Name: YourScriptName` on line 1 of every Python script. The name must match the script name in `CALL PYTHON "YourScriptName.function_name"`. If calls fail with an unknown script, check spelling and that the header is the very first line.
Target execution times under 100ms. Long-running Python functions block your action and can delay message processing. For heavy computations, consider offloading to an external service.
Wrap Python logic in try/except blocks and return structured error responses. This lets your LoT action gracefully handle failures instead of crashing.
Return dictionaries, lists, strings, numbers, and booleans. Custom objects and classes won't serialize properly. When in doubt, convert to a dictionary.
Python functions receive parameters as strings by default. Always convert types explicitly (e.g., `float(value)`, `int(count)`) and validate ranges before processing.
***
## Next Steps
Review all available LoT operations.
Return to the Actions overview for pattern guidance.
# ACTION Syntax Reference
Source: https://docs.coreflux.org/latest/lot-language/actions/syntax
Complete syntax reference for defining LoT actions - event-driven logic executed by the Coreflux MQTT broker
Actions are executable logic blocks triggered by events such as timed intervals, topic updates, or explicit calls. They are the primary way to implement automation in LoT.
Actions run inside the MQTT broker itself, eliminating the need for external middleware or application servers.
***
## Basic Syntax
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
DEFINE ACTION
ON
DO
```
### Components
A unique identifier for the action. Use descriptive names that indicate the action's purpose (e.g., `SensorDataProcessor`, `HeartbeatMonitor`).
Specifies when the action executes. Can be time-based (`ON EVERY`, `ON TIMESTAMP`), topic-based (`ON TOPIC`, `ON CHANGE`), initialization (`ON START`), client lifecycle (`ON CONNECT`, `ON DISCONNECT`, `ON SUBSCRIBE`), or omitted for callable actions.
One or more LoT statements that execute when the action triggers. These can include publishing, variable assignment, conditionals, and Python calls.
## Action Structure
Every action follows this general structure:
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
DEFINE ACTION ActionName
ON DO
// Variable assignment
SET "variable" WITH
// Conditional logic
IF THEN
// Statements
ELSE
// Alternative statements
// Topic operations
PUBLISH TOPIC "output/topic" WITH
KEEP TOPIC "internal/topic" WITH
```
## Trigger Types
Execute at regular intervals:
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
DEFINE ACTION PeriodicTask
ON EVERY 5 SECONDS DO
PUBLISH TOPIC "system/heartbeat" WITH "alive"
```
Supported units: `SECOND(S)`, `MINUTE(S)`, `HOUR(S)`, `DAY(S)`, `WEEK(S)`
Execute when a topic receives data. Use `ON TOPIC` for every message, or `ON CHANGE` to run only when the payload differs from the last message on that topic:
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
DEFINE ACTION MessageProcessor
ON TOPIC "sensors/temperature" DO
PUBLISH TOPIC "processed/temp" WITH PAYLOAD
```
Detection of values changed:
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
DEFINE ACTION OnValueChange
ON CHANGE "sensors/+/temperature" DO
PUBLISH TOPIC "analytics/updated" WITH PAYLOAD
```
Supports wildcards: `+` (single level), `#` (multi-level). See [Action Triggers](./events#topic-based-triggers) for details.
Execute when MQTT clients connect, disconnect, or subscribe:
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
DEFINE ACTION AuditConnect
ON CONNECT DO
PUBLISH TOPIC "audit/connect/" + CLIENTID WITH USER + "@" + ENDPOINT
DEFINE ACTION AuditDisconnect
ON DISCONNECT DO
PUBLISH TOPIC "audit/disconnect/" + CLIENTID WITH DISCONNECTREASON
DEFINE ACTION TrackSubscriptions
ON SUBSCRIBE "sensors/#" DO
PUBLISH TOPIC "audit/sub" WITH SUBSCRIBETOPIC + " qos=" + SUBSCRIBEQOS
```
Omit the pattern on `ON SUBSCRIBE` to run on every allowed subscription. See [Client Lifecycle Triggers](./events#client-lifecycle-triggers).
No trigger - called manually or by other actions:
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
DEFINE ACTION UtilityFunction
DO
KEEP TOPIC "cache/data" WITH (GET TOPIC "source/data")
```
Invoked via `CALL ACTION` from other ACTIONs. See [Callable ACTIONs](./callable-actions).
## Compatible Keywords
Keywords that can be used within action logic:
### Variable Operations
| Keyword | Description |
| ----------- | -------------------------------------------------------------------------------------------------------------------------------- |
| `SET` | Create or assign a variable |
| `GET TOPIC` | Retrieve data from a topic |
| `GET JSON` | Extract fields from JSON payloads — see [Working with JSON](/latest/lot-language/usage-patterns/working-with-json) |
| `GET PROTO` | Extract fields from protobuf payloads (`GET PROTOBUF` is the same) — see [Protobuf Models](/latest/lot-language/models/protobuf) |
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
// For topic "sensors/temp001/data": POSITION 1="sensors", POSITION 2="temp001", POSITION 3="data"
SET "sensor_id" WITH TOPIC POSITION 2
SET "temperature" WITH (GET TOPIC "sensors/temp" AS DOUBLE)
SET "humidity" WITH (GET JSON "humidity" IN PAYLOAD AS DOUBLE)
```
### Topic Operations
| Keyword | Description |
| --------------- | ----------------------------------------------------------- |
| `PUBLISH TOPIC` | Non-retained publish to subscribers, routes, and dashboards |
| `KEEP TOPIC` | Retained broker-side state, readable with `GET TOPIC` |
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
PUBLISH TOPIC "output/data" WITH {temperature}
PUBLISH TOPIC "devices/" + {device_id} + "/status" WITH "online"
KEEP TOPIC "cache/last_reading" WITH PAYLOAD
```
### Control Flow
| Keyword | Description |
| ---------------------- | --------------------- |
| `IF ... THEN ... ELSE` | Conditional execution |
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
IF {temperature} > 80 THEN
PUBLISH TOPIC "alerts/high_temp" WITH {temperature}
ELSE
PUBLISH TOPIC "status/normal" WITH "OK"
```
### Entities
| Entity | Description |
| ------------------ | --------------------------------------------------------------------------------- |
| `PAYLOAD` | The data that triggered the action; use `PAYLOAD AS BYTES` for binary passthrough |
| `CLIENTID` | MQTT client ID for the session or publisher that fired the trigger |
| `USER` | Authenticated username for the session or publisher |
| `ENDPOINT` | Client address (`ip:port`) on connect and lifecycle events |
| `CLEANSESSION` | `"true"` or `"false"` from the client's CONNECT packet |
| `DISCONNECTREASON` | Disconnect type or MQTT reason on `ON DISCONNECT` |
| `SUBSCRIBETOPIC` | Topic filter from an `ON SUBSCRIBE` event |
| `SUBSCRIBEQOS` | Requested QoS (`0`, `1`, or `2`) from an `ON SUBSCRIBE` event |
| `TOPIC POSITION n` | Extract segment from topic path |
| `TIMESTAMP` | Generate timestamps (UTC, UNIX) |
| `EMPTY` | Check if a topic has no data |
### Python Integration
| Keyword | Description |
| ------------- | ------------------------ |
| `CALL PYTHON` | Execute Python functions |
**Extend LoT with Python:** Use `CALL PYTHON` to execute complex calculations, data validation, or integrate external libraries. See the [Python Integration guide](/latest/lot-language/actions/python-integration) for complete syntax and examples.
***
## Type Casting
Use `AS` to cast values when performing comparisons or calculations:
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
GET TOPIC "sensors/value" AS DOUBLE
GET TOPIC "config/enabled" AS BOOL
GET TOPIC "settings/count" AS INT
GET JSON "temperature" IN PAYLOAD AS DOUBLE
```
Supported types: `STRING`, `INT`, `DOUBLE`, `BOOL`, `TIMESTAMP`, `BYTES`
For binary MQTT payloads you want to **forward unchanged** (protobuf, Modbus frames, image chunks), use `PAYLOAD AS BYTES` with `PUBLISH TOPIC` or `KEEP TOPIC`. See [Binary passthrough](/latest/lot-language/actions/operations#binary-passthrough--payload-as-bytes). To **read or write protobuf fields**, use `GET PROTO` and `PUBLISH MODEL` — see [Protobuf Models](/latest/lot-language/models/protobuf).
***
## Complete Example
**Putting It All Together:** This example demonstrates topic extraction, JSON parsing, conditional logic, and publishing - the core patterns you'll use in most actions.
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
DEFINE ACTION SensorDataProcessor
ON TOPIC "sensors/+/raw" DO
// Extract sensor ID from topic (position 2 = the wildcard)
// Topic: sensors/temp001/raw → POSITION 1="sensors", POSITION 2="temp001", POSITION 3="raw"
SET "sensor_id" WITH TOPIC POSITION 2
// Get the temperature value
SET "temp_value" WITH (GET JSON "temperature" IN PAYLOAD AS DOUBLE)
// Check thresholds
IF {temp_value} > 80 THEN
PUBLISH TOPIC "alerts/high_temp/" + {sensor_id} WITH PAYLOAD
ELSE
PUBLISH TOPIC "sensors/processed/" + {sensor_id} WITH PAYLOAD
// Always update the last reading timestamp
PUBLISH TOPIC "sensors/" + {sensor_id} + "/last_update" WITH TIMESTAMP "UTC"
```
***
## Next Steps
Learn about time-based, topic-based, and client lifecycle triggers in detail.
Master GET, SET, PUBLISH, and conditional logic.
# Introduction to LoT
Source: https://docs.coreflux.org/latest/lot-language/introduction
Learn what LoT (Language of Things) is and see it in action with simple examples
## What is LoT?
LoT (Language of Things) is a human-readable language for IoT automation. It uses near-English syntax to define logic, data structures, and integrations—all executed directly within the Coreflux MQTT broker.
Everything in LoT is built by defining four types of entities — **Actions**, **Models**, **Routes**, and **Rules**. Each one handles a different part of your automation, and inside them you use commands like `PUBLISH`, `SET`, and `GET` to make things happen.
Think of LoT as SQL for real-time MQTT data. Just as SQL transforms database records, LoT transforms live MQTT streams.
**Like building with LEGO blocks.** You pick the right block for the job — Actions for logic, Models for data structure, Routes for connections, Rules for security — and snap them together into a complete system.
## The Four Building Blocks
Everything in LoT is built from four core constructs, each created with the `DEFINE` keyword:
| Construct | Purpose | Trigger Types |
| ----------- | ------------------------------------- | ---------------------------------- |
| **Actions** | Execute logic when events occur | Time, Topic, System events, Manual |
| **Models** | Structure MQTT data into JSON schemas | Topic values, Action calls |
| **Routes** | Connect to external systems | Topic patterns |
| **Rules** | Control who can publish/subscribe | Client identity, Topic patterns |
Event-driven logic triggered by time or MQTT topics.
Data schemas that structure MQTT messages into JSON.
Access control for topics and operations.
Connect to databases, brokers, and external services.
***
## How They Work Together
A typical LoT solution combines all four building blocks. Each definition below uses valid LoT syntax — the same patterns you will see in the [Models](/latest/lot-language/models/overview), [Actions](/latest/lot-language/actions/overview), [Routes](/latest/lot-language/routes/overview), and [Rules](/latest/lot-language/rules/overview) references.
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
-- 1. MODEL: When raw temperature updates, publish structured JSON per sensor
DEFINE MODEL SensorReading WITH TOPIC "sensors/+/formatted"
ADD STRING "sensor_id" WITH TOPIC "sensors/+/id"
ADD DOUBLE "temperature" WITH TOPIC "sensors/+/raw" AS TRIGGER
ADD STRING "unit" WITH "celsius"
ADD STRING "timestamp" WITH TIMESTAMP "UTC"
-- 2. ACTION: React to formatted readings and raise alerts
DEFINE ACTION TemperatureAlert
ON TOPIC "sensors/+/formatted" DO
SET "sensor_id" WITH TOPIC POSITION 2
SET "temp" WITH (GET JSON "temperature" IN PAYLOAD AS DOUBLE)
IF {temp} > 80 THEN
PUBLISH TOPIC "alerts/" + {sensor_id} + "/high_temp" WITH "Temperature above threshold"
-- 3. ROUTE: Persist sensor traffic to PostgreSQL
DEFINE ROUTE SensorDB WITH TYPE POSTGRESQL
ADD SQL_CONFIG
WITH SERVER "postgres.example.com"
WITH PORT '5432'
WITH DATABASE "iot_data"
WITH USERNAME "iot_user"
WITH PASSWORD "secure_password"
ADD EVENT StoreSensorData
WITH SOURCE_TOPIC "sensors/#"
WITH QUERY "INSERT INTO sensor_readings (topic, payload) VALUES ('{topic}', '{value.json}')"
-- 4. RULE: Only supervisors (or root) may publish to alert topics
DEFINE RULE RestrictAlertPublish WITH PRIORITY 100 FOR Publish TO TOPIC "alerts/#"
IF USER IN GROUP "supervisors" OR USER IS "root" THEN
ALLOW
ELSE
DENY
```
| Step | What Happens |
| ---- | ------------------------------------------------------------------------------------------------------------ |
| 1 | When `sensors/temp001/raw` updates, the Model publishes JSON to `sensors/temp001/formatted` |
| 2 | The Action reads `temperature` from that JSON and publishes to `alerts/temp001/high_temp` when it exceeds 80 |
| 3 | The Route inserts every message under `sensors/#` (raw, formatted, and related topics) into PostgreSQL |
| 4 | The Rule denies publish to `alerts/#` unless the client is in `supervisors` or is `root` |
***
## What You'll Learn on This Page
Below you'll find practical examples showing LoT in action - from simple heartbeats to data transformation. Each example demonstrates a core capability you can implement immediately.
***
## Hello World
Three examples showing what LoT can do:
```lot Time-Based Action wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
DEFINE ACTION Heartbeat
ON EVERY 5 SECONDS DO
PUBLISH TOPIC "system/alive" WITH "ok"
```
```lot Topic-Based Action wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
DEFINE ACTION Echo
ON TOPIC "input/message" DO
PUBLISH TOPIC "output/message" WITH PAYLOAD
```
```lot Data Model wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
DEFINE MODEL SensorData WITH TOPIC "sensors/formatted"
ADD STRING "id" WITH "TEMP001"
ADD DOUBLE "value" WITH TOPIC "sensors/raw" AS TRIGGER
```
| Example | Trigger | What it does |
| -------------- | -------------------------- | ---------------------------------------------- |
| **Heartbeat** | Every 5 seconds | Publishes "ok" to indicate the system is alive |
| **Echo** | Message on `input/message` | Forwards the payload to another topic |
| **SensorData** | Value on `sensors/raw` | Formats raw sensor data into structured JSON |
***
## Next Steps
Naming, topic design, reusable logic, and pitfalls to avoid.
Learn how Actions automate MQTT data processing.
# MODEL Examples
Source: https://docs.coreflux.org/latest/lot-language/models/examples
Practical examples of LoT models for common IoT data structuring scenarios.
## Examples Index
Find the right pattern for your use case:
| Example | Pattern | Best For |
| ----------------------------------------------------------------- | ---------------- | ---------------------------------- |
| [Simple Sensor Reading](#simple-sensor-reading) | Basic | Single source, static metadata |
| [Multi-Source Equipment](#multi-source-equipment-status) | Basic | Combining multiple topics |
| [Production Record](#production-record-with-calculations) | Calculated | Derived metrics |
| [Multi-Instance Sensor](#multi-instance-sensor-model) | Wildcard | Multiple similar devices |
| [Factory Hierarchy](#factory-hierarchy-model) | Wildcard | Multi-level topic structures |
| [Alarm Record](#alarm-record-from-json) | Action-Published | JSON processing, conditional logic |
| [Production Event](#production-event-with-calculations) | Action-Published | Complex calculations |
| [Quality Check](#quality-check-with-validation) | Action-Published | Pass/fail determination |
| [Alert Family](#alert-model-family) | Inheritance | Related data types |
| [Sensor with Metadata](#sensor-with-metadata) | Nested Objects | Grouped configuration |
| [Protobuf command and telemetry](#protobuf-command-and-telemetry) | Protobuf (beta) | Binary gateway `.proto` on MQTT |
***
## Basic Model Examples
### Simple Sensor Reading
A straightforward model that formats raw sensor data with static metadata.
```mermaid actions={false} theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
flowchart LR
subgraph Input
A[sensors/raw/temperature]
end
subgraph Model
B[SensorReading]
end
subgraph Output
C[sensors/formatted/temperature]
end
A -->|triggers| B
B -->|publishes| C
```
This model adds context (sensor ID, unit, status) to a raw temperature value:
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
DEFINE MODEL SensorReading WITH TOPIC "sensors/formatted/temperature"
ADD STRING "sensor_id" WITH "TEMP001"
ADD DOUBLE "value" WITH TOPIC "sensors/raw/temperature" AS TRIGGER
ADD STRING "unit" WITH "celsius"
ADD STRING "timestamp" WITH TIMESTAMP "UTC"
ADD STRING "status" WITH "ACTIVE"
```
When a value arrives at `sensors/raw/temperature`, the model publishes:
```json wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
{
"sensor_id": "TEMP001",
"value": 25.5,
"unit": "celsius",
"timestamp": "2025-10-25T14:30:15Z",
"status": "ACTIVE"
}
```
***
### Multi-Source Equipment Status
Combine data from multiple topics into a unified equipment status report.
```mermaid actions={false} theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
flowchart LR
subgraph Inputs
A[equipment/current/id]
B[equipment/current/status]
C[equipment/current/runtime]
D[equipment/current/efficiency]
end
subgraph Model
E[EquipmentStatus]
end
subgraph Output
F[equipment/status/formatted]
end
A --> E
B -->|triggers| E
C --> E
D --> E
E -->|publishes| F
```
Each field pulls from a different source topic. The status topic acts as the trigger:
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
DEFINE MODEL EquipmentStatus WITH TOPIC "equipment/status/formatted"
ADD STRING "equipment_id" WITH TOPIC "equipment/current/id"
ADD STRING "status" WITH TOPIC "equipment/current/status" AS TRIGGER
ADD INT "runtime_hours" WITH TOPIC "equipment/current/runtime"
ADD DOUBLE "efficiency" WITH TOPIC "equipment/current/efficiency"
ADD BOOL "maintenance_required" WITH TOPIC "equipment/current/maintenance_flag"
ADD STRING "last_update" WITH TIMESTAMP "UTC"
ADD STRING "location" WITH "Factory Floor A"
```
When the status topic updates, the model publishes a combined report to `equipment/status/formatted`:
```json wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
{
"equipment_id": "CNC-4500",
"status": "RUNNING",
"runtime_hours": 1823,
"efficiency": 92.4,
"maintenance_required": false,
"last_update": "2025-10-25T14:30:15Z",
"location": "Factory Floor A"
}
```
***
### Production Record with Calculations
Automatically calculate derived values like efficiency percentages.
```mermaid actions={false} theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
flowchart LR
subgraph Inputs
A[quantity]
B[target]
end
subgraph Calculation
C["efficiency = quantity / target × 100"]
end
subgraph Output
D[ProductionRecord]
end
A --> C
B --> C
C --> D
```
The `efficiency_percent` field is calculated inline from other topic values:
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
DEFINE MODEL ProductionRecord WITH TOPIC "production/records/completed"
ADD STRING "batch_id" WITH TOPIC "production/current/batch_id"
ADD STRING "product_code" WITH TOPIC "production/current/product_code"
ADD INT "quantity_produced" WITH TOPIC "production/current/quantity" AS TRIGGER
ADD INT "quantity_target" WITH TOPIC "production/current/target"
ADD DOUBLE "efficiency_percent" WITH (GET TOPIC "production/current/quantity" AS DOUBLE / GET TOPIC "production/current/target" AS DOUBLE * 100)
ADD STRING "operator" WITH TOPIC "production/current/operator"
ADD STRING "completion_time" WITH TIMESTAMP "UTC"
```
Example output when 95 of 100 units are produced:
```json wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
{
"batch_id": "BATCH_2025_001",
"product_code": "WIDGET_A",
"quantity_produced": 95,
"quantity_target": 100,
"efficiency_percent": 95.0,
"operator": "John Smith",
"completion_time": "2025-10-25T14:30:15Z"
}
```
***
## Wildcard Model Examples
### Multi-Instance Sensor Model
Handle multiple sensors with a single model definition using the `+` wildcard.
```mermaid actions={false} theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
flowchart TB
subgraph Inputs
A[sensors/temp001/value]
B[sensors/pressure002/value]
C[sensors/humidity003/value]
end
subgraph SingleModel
D["GenericSensorData sensors/+/formatted"]
end
subgraph Outputs
E[sensors/temp001/formatted]
F[sensors/pressure002/formatted]
G[sensors/humidity003/formatted]
end
A -->|triggers| D
B -->|triggers| D
C -->|triggers| D
D --> E
D --> F
D --> G
```
The `+` wildcard matches any single level in the topic hierarchy:
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
DEFINE MODEL GenericSensorData WITH TOPIC "sensors/+/formatted"
ADD STRING "sensor_id" WITH TOPIC "sensors/+/id"
ADD DOUBLE "value" WITH TOPIC "sensors/+/value" AS TRIGGER
ADD STRING "unit" WITH TOPIC "sensors/+/unit"
ADD STRING "location" WITH TOPIC "sensors/+/location"
ADD STRING "timestamp" WITH TIMESTAMP "UTC"
```
When `sensors/temp001/value` receives data, the model publishes to `sensors/temp001/formatted`:
```json wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
{
"sensor_id": "TEMP001",
"value": 23.7,
"unit": "celsius",
"location": "Building A - Room 101",
"timestamp": "2025-10-25T14:30:15Z"
}
```
Each sensor instance (temp001, pressure002, etc.) gets its own output topic automatically. The wildcard maintains topic context through the model.
***
### Factory Hierarchy Model
Handle multi-level topic hierarchies with multiple wildcards.
```mermaid actions={false} theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
flowchart TB
subgraph Factory
A["factory/line1/machine1/status"]
B["factory/line1/machine2/status"]
C["factory/line2/machine1/status"]
end
subgraph Model
D["MachineData factory/+/+/data/formatted"]
end
subgraph Outputs
E["factory/line1/machine1/data/formatted"]
F["factory/line1/machine2/data/formatted"]
G["factory/line2/machine1/data/formatted"]
end
A -->|triggers| D
B -->|triggers| D
C -->|triggers| D
D --> E
D --> F
D --> G
```
Multiple `+` wildcards capture different levels of the hierarchy:
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
DEFINE MODEL MachineData WITH TOPIC "factory/+/+/data/formatted"
ADD STRING "line_id" WITH TOPIC "factory/+/id"
ADD STRING "machine_id" WITH TOPIC "factory/+/+/id"
ADD STRING "status" WITH TOPIC "factory/+/+/status" AS TRIGGER
ADD DOUBLE "output_rate" WITH TOPIC "factory/+/+/output_rate"
ADD STRING "timestamp" WITH TIMESTAMP "UTC"
```
When `factory/line1/machine1/status` updates, the model publishes to `factory/line1/machine1/data/formatted`:
```json wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
{
"line_id": "LINE_01",
"machine_id": "MACH_01",
"status": "RUNNING",
"output_rate": 145.8,
"timestamp": "2025-10-25T14:30:15Z"
}
```
***
## Models Published by Actions Examples
Use this pattern when you need **full control** over publishing logic—conditional decisions, complex calculations, JSON extraction, or custom routing. The action determines when and where to publish.
### Alarm Record from JSON
Process incoming JSON payloads into structured alarm records.
```mermaid actions={false} theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
flowchart LR
subgraph Input
A["alarms/+/json_input {type, value, threshold, ...}"]
end
subgraph Action
B[ProcessAlarmJSON]
end
subgraph Output
C["alarms/structured/{equipment_id} AlarmRecord model"]
end
A -->|triggers| B
B -->|PUBLISH MODEL| C
```
First, define the model schema with `COLLAPSED` (no automatic trigger):
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
DEFINE MODEL AlarmRecord COLLAPSED
ADD STRING "alarm_id"
ADD STRING "equipment_id"
ADD STRING "alarm_type"
ADD DOUBLE "trigger_value"
ADD DOUBLE "threshold"
ADD STRING "severity"
ADD STRING "message"
ADD STRING "timestamp"
ADD BOOL "auto_generated"
```
Then create an action that extracts JSON fields and publishes the model:
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
DEFINE ACTION ProcessAlarmJSON
ON TOPIC "alarms/+/json_input" DO
SET "equipment_id" WITH TOPIC POSITION 2
PUBLISH MODEL AlarmRecord TO "alarms/structured/" + {equipment_id} WITH
alarm_id = (RANDOM UUID)
equipment_id = {equipment_id}
alarm_type = (GET JSON "type" IN PAYLOAD AS STRING)
trigger_value = (GET JSON "value" IN PAYLOAD AS DOUBLE)
threshold = (GET JSON "threshold" IN PAYLOAD AS DOUBLE)
severity = (GET JSON "severity" IN PAYLOAD AS STRING)
message = (GET JSON "message" IN PAYLOAD AS STRING)
timestamp = TIMESTAMP "UTC"
auto_generated = TRUE
```
When a JSON payload arrives at `alarms/pump003/json_input`, the action publishes to `alarms/structured/pump003`:
```json wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
{
"alarm_id": "f47ac10b-58cc-4372-a567-0e02b2c3d479",
"equipment_id": "pump003",
"alarm_type": "OVERHEAT",
"trigger_value": 95.3,
"threshold": 85.0,
"severity": "CRITICAL",
"message": "Motor temperature exceeded safe limit",
"timestamp": "2025-10-25T14:30:15Z",
"auto_generated": true
}
```
***
### Production Event with Calculations
Create production records with computed metrics like efficiency.
```mermaid actions={false} theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
flowchart LR
subgraph Input
A["production/+/events {parts, cycle_time, target, ...}"]
end
subgraph Action
B["ProcessProductionEvent Calculate efficiency"]
end
subgraph Output
C["production/events/structured/{line} ProductionEvent model"]
end
A -->|triggers| B
B -->|PUBLISH MODEL| C
```
Define the schema for production events:
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
DEFINE MODEL ProductionEvent COLLAPSED
ADD STRING "event_id"
ADD STRING "line_id"
ADD STRING "event_type"
ADD INT "parts_count"
ADD DOUBLE "cycle_time"
ADD DOUBLE "efficiency_percent"
ADD STRING "operator"
ADD STRING "timestamp"
```
The action performs calculations before publishing:
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
DEFINE ACTION ProcessProductionEvent
ON TOPIC "production/+/events" DO
SET "line_id" WITH TOPIC POSITION 2
SET "parts" WITH (GET JSON "parts_produced" IN PAYLOAD AS INT)
SET "cycle" WITH (GET JSON "cycle_time" IN PAYLOAD AS DOUBLE)
SET "target" WITH (GET JSON "target_cycle_time" IN PAYLOAD AS DOUBLE)
SET "eff" WITH ({target} / {cycle} * 100)
PUBLISH MODEL ProductionEvent TO "production/events/structured/" + {line_id} WITH
event_id = (RANDOM UUID)
line_id = {line_id}
event_type = (GET JSON "event_type" IN PAYLOAD AS STRING)
parts_count = {parts}
cycle_time = {cycle}
efficiency_percent = {eff}
operator = (GET JSON "operator" IN PAYLOAD AS STRING)
timestamp = TIMESTAMP "UTC"
```
When a production event arrives at `production/line2/events` with a target cycle time of 10s and an actual cycle time of 8.5s, the action publishes to `production/events/structured/line2`:
```json wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
{
"event_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"line_id": "line2",
"event_type": "BATCH_COMPLETE",
"parts_count": 250,
"cycle_time": 8.5,
"efficiency_percent": 117.6,
"operator": "Maria Garcia",
"timestamp": "2025-10-25T14:30:15Z"
}
```
***
### Quality Check with Validation
Determine pass/fail status based on measurement tolerances.
```mermaid actions={false} theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
flowchart LR
subgraph Input
A["quality/+/measurement {value, target, tolerance}"]
end
subgraph Action
B["ValidateQuality Check tolerance range"]
end
subgraph Outputs
C["quality/passed/{id}"]
D["quality/failed/{id}"]
end
A -->|triggers| B
B -->|if within tolerance| C
B -->|if outside tolerance| D
```
Complex conditional logic (like tolerance checking) is cleaner in an action than inline in a model. The action can also route to different topics based on the result.
Define the quality reading schema:
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
DEFINE MODEL QualityReading COLLAPSED
ADD STRING "inspection_id"
ADD STRING "part_id"
ADD DOUBLE "measurement"
ADD DOUBLE "target"
ADD DOUBLE "tolerance"
ADD STRING "result"
ADD STRING "timestamp"
```
The action checks if the measurement is within tolerance and routes accordingly:
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
DEFINE ACTION ValidateQuality
ON TOPIC "quality/+/measurement" DO
SET "part_id" WITH TOPIC POSITION 2
SET "value" WITH (GET JSON "value" IN PAYLOAD AS DOUBLE)
SET "target" WITH (GET JSON "target" IN PAYLOAD AS DOUBLE)
SET "tolerance" WITH (GET JSON "tolerance" IN PAYLOAD AS DOUBLE)
SET "min_allowed" WITH ({target} - {tolerance})
SET "max_allowed" WITH ({target} + {tolerance})
IF {value} >= {min_allowed} AND {value} <= {max_allowed} THEN
PUBLISH MODEL QualityReading TO "quality/passed/" + {part_id} WITH
inspection_id = (RANDOM UUID)
part_id = {part_id}
measurement = {value}
target = {target}
tolerance = {tolerance}
result = "PASS"
timestamp = TIMESTAMP "UTC"
ELSE
PUBLISH MODEL QualityReading TO "quality/failed/" + {part_id} WITH
inspection_id = (RANDOM UUID)
part_id = {part_id}
measurement = {value}
target = {target}
tolerance = {tolerance}
result = "FAIL"
timestamp = TIMESTAMP "UTC"
```
When a measurement of 10.02 mm arrives for a part with a target of 10.00 mm and tolerance of 0.05 mm, it passes and publishes to `quality/passed/PART_A1`:
```json wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
{
"inspection_id": "b7e8f9a0-1234-5678-90ab-cdef01234567",
"part_id": "PART_A1",
"measurement": 10.02,
"target": 10.0,
"tolerance": 0.05,
"result": "PASS",
"timestamp": "2025-10-25T14:30:15Z"
}
```
***
## Model Inheritance Examples
For complete inheritance patterns and syntax, see [Model Inheritance](./model-inheritance).
### Alert Model Family
Create specialized alert types from a common base model.
```mermaid actions={false} theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
flowchart TB
subgraph BaseModel
A["BaseAlert alert_id, source, timestamp, severity, message, acknowledged"]
end
subgraph SpecializedModels
B["TemperatureAlert + temperature_value + threshold_exceeded + sensor_location"]
C["PressureAlert + pressure_value + max_safe_pressure + system_affected"]
D["MaintenanceAlert + equipment_id + hours_since_service + maintenance_type"]
end
A --> B
A --> C
A --> D
```
First, define the base model with common fields:
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
DEFINE MODEL BaseAlert COLLAPSED
ADD STRING "alert_id"
ADD STRING "source_system"
ADD STRING "timestamp"
ADD STRING "severity"
ADD STRING "message"
ADD BOOL "acknowledged"
```
Then create specialized models that inherit and extend it:
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
DEFINE MODEL TemperatureAlert FROM BaseAlert
ADD DOUBLE "temperature_value"
ADD DOUBLE "threshold_exceeded"
ADD STRING "sensor_location"
ADD STRING "unit"
```
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
DEFINE MODEL PressureAlert FROM BaseAlert
ADD DOUBLE "pressure_value"
ADD DOUBLE "max_safe_pressure"
ADD STRING "pressure_unit"
ADD STRING "system_affected"
```
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
DEFINE MODEL MaintenanceAlert FROM BaseAlert
ADD STRING "equipment_id"
ADD STRING "maintenance_type"
ADD INT "hours_since_service"
ADD INT "recommended_interval"
```
Use the specialized model in an action:
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
DEFINE ACTION GenerateTemperatureAlert
ON TOPIC "sensors/temperature/+/alert" DO
SET "sensor_id" WITH TOPIC POSITION 3
PUBLISH MODEL TemperatureAlert TO "alerts/temperature/" + {sensor_id} WITH
alert_id = (RANDOM UUID)
source_system = "TemperatureMonitor"
timestamp = TIMESTAMP "UTC"
severity = "HIGH"
message = "Temperature threshold exceeded"
acknowledged = FALSE
temperature_value = (GET JSON "value" IN PAYLOAD AS DOUBLE)
threshold_exceeded = (GET JSON "threshold" IN PAYLOAD AS DOUBLE)
sensor_location = (GET JSON "location" IN PAYLOAD AS STRING)
unit = "celsius"
```
When an alert triggers for sensor `TH_042`, the action publishes to `alerts/temperature/TH_042` with both inherited and specialized fields:
```json wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
{
"alert_id": "c3d4e5f6-7890-1234-abcd-ef5678901234",
"source_system": "TemperatureMonitor",
"timestamp": "2025-10-25T14:30:15Z",
"severity": "HIGH",
"message": "Temperature threshold exceeded",
"acknowledged": false,
"temperature_value": 92.7,
"threshold_exceeded": 85.0,
"sensor_location": "Boiler Room B",
"unit": "celsius"
}
```
***
## Nested Object Examples
### Sensor with Metadata
Group related configuration and context fields into nested sub-objects using the `OBJECT` type and indentation.
```mermaid actions={false} theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
flowchart LR
subgraph Inputs
A[sensors/+/value]
B[sensors/+/id]
C[sensors/+/location]
D[sensors/+/floor]
E[sensors/+/building]
F[sensors/+/unit]
end
subgraph Model
G["SensorWithMetadata sensors/+/formatted"]
end
subgraph Output
H["sensors/{id}/formatted {value, metadata: {location, ...}}"]
end
A -->|triggers| G
B --> G
C --> G
D --> G
E --> G
F --> G
G -->|publishes| H
```
The `metadata` field uses `OBJECT` to group location details into a nested object. Fields indented under `ADD OBJECT` become its properties. When indentation returns to the parent level, fields belong to the top-level model again:
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
DEFINE MODEL SensorWithMetadata WITH TOPIC "sensors/+/formatted"
ADD STRING "sensor_id" WITH TOPIC "sensors/+/id"
ADD DOUBLE "value" WITH TOPIC "sensors/+/value" AS TRIGGER
ADD OBJECT "metadata"
ADD STRING "unit" WITH TOPIC "sensors/+/unit"
ADD STRING "location" WITH TOPIC "sensors/+/location"
ADD INT "floor" WITH TOPIC "sensors/+/floor"
ADD STRING "building" WITH TOPIC "sensors/+/building"
ADD STRING "timestamp" WITH TIMESTAMP "UTC"
```
When `sensors/temp001/value` receives data, the model publishes to `sensors/temp001/formatted`:
```json wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
{
"sensor_id": "TEMP001",
"value": 25.5,
"metadata": {
"unit": "celsius",
"location": "Room 101",
"floor": 3,
"building": "Building A"
},
"timestamp": "2025-10-25T14:30:15Z"
}
```
The indentation rule applies everywhere `OBJECT` is used — in basic models, wildcard models, and `COLLAPSED` templates published from actions.
***
## Protobuf Examples
Dynamic protobuf encoding and decoding in LoT is **beta**. Field numbers must match your vendor `.proto` — see [Protobuf Models](./protobuf) for `PROTO_TAG`, type mapping, and pitfalls.
### Protobuf command and telemetry
Decode a binary command from a gateway, keep the command type on a topic, then pack JSON tag values back into protobuf telemetry the gateway already understands.
```mermaid actions={false} theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
flowchart LR
subgraph Input
A["plant/+/+/command protobuf bytes"]
B["plant/+/+/tags JSON tags"]
end
subgraph Actions
C[OnPlcCommand]
D[PublishTelemetry]
end
subgraph Output
E["plant/.../pending_command"]
F["plant/.../telemetry protobuf bytes"]
end
A -->|GET PROTO| C
C --> E
B --> D
D -->|PUBLISH MODEL| F
```
Declare every command arm on the parent so unused submessages do not shift wire numbers. `ADD OBJECT` holds a slot open; you only need `GET PROTO` for the fields you read:
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
DEFINE MODEL DeviceCommand WITH FORMAT PROTOBUF COLLAPSED
ADD INT "command_type" PROTO_TAG 1
ADD OBJECT "header" PROTO_TAG 2
ADD OBJECT "write_setpoint" PROTO_TAG 3
ADD OBJECT "ack_alarm" PROTO_TAG 4
ADD OBJECT "reserved_slot" PROTO_TAG 5
ADD OBJECT "read_snapshot" PROTO_TAG 6
```
When protobuf bytes arrive on `plant/line1/plc01/command`, the action reads `command_type` and stores it for later:
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
DEFINE ACTION OnPlcCommand
ON TOPIC "plant/+/+/command" DO
SET "line" WITH TOPIC POSITION 2
SET "device_id" WITH TOPIC POSITION 3
SET "cmd" WITH (GET PROTO "command_type" IN PAYLOAD AS INT USING MODEL "DeviceCommand")
KEEP TOPIC "plant/" + {line} + "/" + {device_id} + "/pending_command" WITH {cmd}
```
Nested messages you fill on publish use `ADD MODEL`, not indented `ADD OBJECT`. Tag the sparse fields explicitly:
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
DEFINE MODEL MessageHeader WITH FORMAT PROTOBUF COLLAPSED
ADD STRING "device_id" PROTO_TAG 1
ADD STRING "timestamp" PROTO_TAG 2
DEFINE MODEL ProcessValues WITH FORMAT PROTOBUF COLLAPSED
ADD DOUBLE "temperature" PROTO_TAG 1
ADD DOUBLE "pressure" PROTO_TAG 2
DEFINE MODEL TelemetryMessage WITH FORMAT PROTOBUF COLLAPSED
ADD INT "message_type" PROTO_TAG 1
ADD MODEL MessageHeader "header" PROTO_TAG 2
ADD MODEL ProcessValues "process_values" PROTO_TAG 3
```
When JSON tags arrive on `plant/line1/plc01/tags`, the action publishes **raw protobuf bytes** to `plant/line1/plc01/telemetry`:
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
DEFINE ACTION PublishTelemetry
ON TOPIC "plant/+/+/tags" DO
SET "line" WITH TOPIC POSITION 2
SET "device_id" WITH TOPIC POSITION 3
SET "temp" WITH (GET JSON "Temperature" IN PAYLOAD AS DOUBLE)
SET "pressure" WITH (GET JSON "Pressure" IN PAYLOAD AS DOUBLE)
SET "ts" WITH TIMESTAMP "UNIX"
PUBLISH MODEL TelemetryMessage TO "plant/" + {line} + "/" + {device_id} + "/telemetry" WITH
message_type = 2
header.device_id = {device_id}
header.timestamp = {ts}
process_values.temperature = {temp}
process_values.pressure = {pressure}
```
Example JSON on `plant/line1/plc01/tags` that this action encodes:
```json wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
{
"Temperature": 72.4,
"Pressure": 1.18
}
```
The destination topic carries binary protobuf, not JSON. A payload that matches this schema has field **1** = `2` (message type), field **2** = the header (`device_id` + Unix timestamp), and field **3** = process values (`temperature` + `pressure`). Decode a captured payload with `protoc --decode_raw` to confirm those field numbers.
Trigger-based models (`WITH TOPIC` + `AS TRIGGER`) still auto-publish JSON. Use `COLLAPSED` plus `PUBLISH MODEL` whenever the edge must see protobuf bytes. Full type mapping and `GET PROTO` sources: [Protobuf Models](./protobuf).
***
## Choosing the Right Pattern
**Use when:** Data flows continuously and formatting is straightforward.
**Best for:** Sensor readings, status updates, simple aggregation.
**Trigger:** Automatic when source topic updates.
**Use when:** You need derived metrics computed from source values.
**Best for:** Percentages, totals, averages, unit conversions.
**Note:** Keep calculations simple; use actions for complex logic.
**Use when:** You have multiple instances of the same data structure.
**Best for:** Fleets of sensors, multi-line factories, device groups.
**Benefit:** One model definition handles all instances.
**Use when:** You need control over timing, routing, or complex logic.
**Best for:** JSON extraction, conditional publishing, pass/fail routing, complex calculations.
**Benefit:** Full action power with structured output.
**Use when:** You have related data types sharing common fields.
**Best for:** Alert families, equipment types, event categories.
**Benefit:** DRY principle—define common fields once.
**Use when:** Your JSON output needs grouped or hierarchical data.
**Best for:** Metadata grouping, configuration objects, multi-level status reports.
**Benefit:** Clean nested JSON without external formatting. Indentation controls structure.
**Use when:** A gateway or SCADA system already speaks a `.proto` on MQTT.
**Best for:** Decoding binary commands with `GET PROTO` and emitting telemetry bytes with `PUBLISH MODEL`.
**Benefit:** Match vendor field numbers without a separate transformation service. See [Protobuf Models](./protobuf).
***
## Next Steps
Connect your broker to external systems and databases.
Master the action syntax for publishing models.
# MODEL Inheritance
Source: https://docs.coreflux.org/latest/lot-language/models/model-inheritance
Create model families with shared fields using the FROM keyword for code reuse and consistency
## Why Use Model Inheritance?
When your system has related data types—different kinds of alerts, equipment, or events—you often repeat the same fields across multiple models. Model inheritance lets you define common fields once in a **base model**, then extend it with specialized fields for each variant.
**Like a family tree for data.** A "TemperatureAlert" is a child of "Alert"—it inherits all the standard alert properties (ID, timestamp, severity) but adds its own temperature-specific fields. Change the parent, and all children get the update.
### When to Use Inheritance
| Scenario | Example |
| ------------------------ | ------------------------------------------------------ |
| **Alert systems** | Base alert + temperature, pressure, maintenance alerts |
| **Equipment monitoring** | Base equipment + pumps, conveyors, motors |
| **Event tracking** | Base event + production, quality, safety events |
| **Sensor networks** | Base sensor + temperature, pressure, flow sensors |
***
## Inheritance Syntax
The `FROM` keyword creates a child model that inherits all fields from a parent:
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
DEFINE MODEL ChildModel FROM ParentModel
ADD STRING "specialized_field"
ADD DOUBLE "another_field"
```
The child model automatically includes every field from the parent, plus any additional fields you define.
### How It Works
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
// Parent model with common fields
DEFINE MODEL BaseAlert COLLAPSED
ADD STRING "alert_id"
ADD STRING "timestamp"
ADD STRING "severity"
ADD STRING "message"
ADD BOOL "acknowledged"
// Child model inherits all parent fields + adds specialized ones
DEFINE MODEL TemperatureAlert FROM BaseAlert
ADD DOUBLE "temperature_value"
ADD DOUBLE "threshold_exceeded"
ADD STRING "sensor_location"
```
**Result:** `TemperatureAlert` has 8 fields total:
* 5 inherited from `BaseAlert` (alert\_id, timestamp, severity, message, acknowledged)
* 3 specialized fields (temperature\_value, threshold\_exceeded, sensor\_location)
Base models are typically defined with `COLLAPSED` since they're meant to be extended, not used directly. The child models inherit this behavior.
***
## Designing Base Models
A good base model contains fields that **every variant needs**. Put yourself in the position of someone processing alerts, equipment data, or events—what fields do they always expect?
### Base Model Best Practices
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
DEFINE MODEL BaseEquipment COLLAPSED
// Identification - always needed
ADD STRING "equipment_id"
ADD STRING "equipment_name"
// Context - helps locate and categorize
ADD STRING "location"
ADD STRING "status"
// Operational data - common across equipment
ADD INT "runtime_hours"
ADD STRING "last_maintenance"
// Metadata - for tracking and auditing
ADD STRING "operator"
ADD STRING "timestamp"
```
Don't include type-specific fields in base models. If only some variants need a field, it belongs in the child model.
### What to Include in Base Models
| Field Category | Examples | Why |
| --------------- | -------------------------------- | ------------------------------------- |
| **Identifiers** | `id`, `name`, `code` | Every record needs to be identifiable |
| **Timestamps** | `timestamp`, `created_at` | Essential for ordering and debugging |
| **Status** | `status`, `severity`, `state` | Common across most data types |
| **Context** | `location`, `source`, `operator` | Helps downstream processing |
| **Flags** | `acknowledged`, `processed` | Common workflow states |
***
## Creating Specialized Models
Child models add fields specific to their domain. Keep these focused—only add what that particular variant needs.
### Alert Family Example
Starting from a common base, create specialized alert types:
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
DEFINE MODEL BaseAlert COLLAPSED
ADD STRING "alert_id"
ADD STRING "source_system"
ADD STRING "timestamp"
ADD STRING "severity"
ADD STRING "message"
ADD BOOL "acknowledged"
```
Temperature alerts add measurement data and thresholds:
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
DEFINE MODEL TemperatureAlert FROM BaseAlert
ADD DOUBLE "temperature_value"
ADD DOUBLE "threshold_exceeded"
ADD STRING "sensor_location"
ADD STRING "unit"
```
Pressure alerts track system-level concerns:
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
DEFINE MODEL PressureAlert FROM BaseAlert
ADD DOUBLE "pressure_value"
ADD DOUBLE "max_safe_pressure"
ADD STRING "pressure_unit"
ADD STRING "system_affected"
```
Maintenance alerts focus on service schedules:
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
DEFINE MODEL MaintenanceAlert FROM BaseAlert
ADD STRING "equipment_id"
ADD STRING "maintenance_type"
ADD INT "hours_since_service"
ADD INT "recommended_interval"
ADD STRING "priority_level"
```
***
## Publishing Inherited Models
Inherited models work exactly like regular COLLAPSED models—use `PUBLISH MODEL` from an Action and provide values for all fields (inherited + specialized):
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
DEFINE ACTION GenerateTemperatureAlert
ON TOPIC "sensors/temperature/+/alert" DO
SET "sensor_id" WITH TOPIC POSITION 3
PUBLISH MODEL TemperatureAlert TO "alerts/temperature/" + {sensor_id} WITH
// Inherited fields from BaseAlert
alert_id = (RANDOM UUID)
source_system = "TemperatureMonitor"
timestamp = TIMESTAMP "UTC"
severity = "HIGH"
message = "Temperature threshold exceeded"
acknowledged = FALSE
// Specialized fields for TemperatureAlert
temperature_value = (GET JSON "value" IN PAYLOAD AS DOUBLE)
threshold_exceeded = (GET JSON "threshold" IN PAYLOAD AS DOUBLE)
sensor_location = (GET JSON "location" IN PAYLOAD AS STRING)
unit = "celsius"
```
### Polymorphic Action Pattern
One Action can handle multiple model types by checking the incoming data and publishing the appropriate variant:
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
DEFINE ACTION PolymorphicAlertGenerator
ON TOPIC "sensors/+/+/alert" DO
SET "sensor_type" WITH TOPIC POSITION 2
SET "sensor_id" WITH TOPIC POSITION 3
IF {sensor_type} EQUALS "temperature" THEN
PUBLISH MODEL TemperatureAlert TO "alerts/temperature/" + {sensor_id} WITH
alert_id = (RANDOM UUID)
source_system = "SensorNetwork"
timestamp = TIMESTAMP "UTC"
severity = (GET JSON "severity" IN PAYLOAD AS STRING)
message = "Temperature alert triggered"
acknowledged = FALSE
temperature_value = (GET JSON "value" IN PAYLOAD AS DOUBLE)
threshold_exceeded = (GET JSON "threshold" IN PAYLOAD AS DOUBLE)
sensor_location = (GET JSON "location" IN PAYLOAD AS STRING)
unit = "celsius"
ELSE IF {sensor_type} EQUALS "pressure" THEN
PUBLISH MODEL PressureAlert TO "alerts/pressure/" + {sensor_id} WITH
alert_id = (RANDOM UUID)
source_system = "SensorNetwork"
timestamp = TIMESTAMP "UTC"
severity = (GET JSON "severity" IN PAYLOAD AS STRING)
message = "Pressure alert triggered"
acknowledged = FALSE
pressure_value = (GET JSON "value" IN PAYLOAD AS DOUBLE)
max_safe_pressure = (GET JSON "max_pressure" IN PAYLOAD AS DOUBLE)
pressure_unit = "PSI"
system_affected = (GET JSON "system" IN PAYLOAD AS STRING)
```
All alert types share the same base structure, so downstream systems can process common fields (like `severity` and `acknowledged`) without knowing the specific alert type.
***
## Complete Examples
A complete equipment monitoring system with specialized equipment types:
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
// Base model with common equipment fields
DEFINE MODEL BaseEquipment COLLAPSED
ADD STRING "equipment_id"
ADD STRING "equipment_name"
ADD STRING "location"
ADD STRING "status"
ADD INT "runtime_hours"
ADD STRING "last_maintenance"
ADD STRING "operator"
ADD STRING "timestamp"
// Pump-specific model
DEFINE MODEL PumpEquipment FROM BaseEquipment
ADD DOUBLE "flow_rate"
ADD DOUBLE "pressure_output"
ADD INT "rpm"
ADD DOUBLE "power_consumption"
ADD STRING "pump_type"
// Conveyor-specific model
DEFINE MODEL ConveyorEquipment FROM BaseEquipment
ADD DOUBLE "belt_speed"
ADD INT "items_per_minute"
ADD DOUBLE "belt_tension"
ADD BOOL "emergency_stop_active"
ADD STRING "direction"
```
**Action to publish equipment reports:**
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
DEFINE ACTION EquipmentReporter
ON TOPIC "equipment/+/+/status_update" DO
SET "equipment_type" WITH TOPIC POSITION 2
SET "equipment_id" WITH TOPIC POSITION 3
IF {equipment_type} EQUALS "pump" THEN
PUBLISH MODEL PumpEquipment TO "equipment/reports/pump/" + {equipment_id} WITH
equipment_id = {equipment_id}
equipment_name = (GET JSON "name" IN PAYLOAD AS STRING)
location = (GET JSON "location" IN PAYLOAD AS STRING)
status = (GET JSON "status" IN PAYLOAD AS STRING)
runtime_hours = (GET JSON "runtime" IN PAYLOAD AS INT)
last_maintenance = (GET JSON "last_service" IN PAYLOAD AS STRING)
operator = (GET JSON "operator" IN PAYLOAD AS STRING)
timestamp = TIMESTAMP "UTC"
flow_rate = (GET JSON "flow_rate" IN PAYLOAD AS DOUBLE)
pressure_output = (GET JSON "pressure" IN PAYLOAD AS DOUBLE)
rpm = (GET JSON "rpm" IN PAYLOAD AS INT)
power_consumption = (GET JSON "power" IN PAYLOAD AS DOUBLE)
pump_type = "Centrifugal"
ELSE IF {equipment_type} EQUALS "conveyor" THEN
PUBLISH MODEL ConveyorEquipment TO "equipment/reports/conveyor/" + {equipment_id} WITH
equipment_id = {equipment_id}
equipment_name = (GET JSON "name" IN PAYLOAD AS STRING)
location = (GET JSON "location" IN PAYLOAD AS STRING)
status = (GET JSON "status" IN PAYLOAD AS STRING)
runtime_hours = (GET JSON "runtime" IN PAYLOAD AS INT)
last_maintenance = (GET JSON "last_service" IN PAYLOAD AS STRING)
operator = (GET JSON "operator" IN PAYLOAD AS STRING)
timestamp = TIMESTAMP "UTC"
belt_speed = (GET JSON "speed" IN PAYLOAD AS DOUBLE)
items_per_minute = (GET JSON "throughput" IN PAYLOAD AS INT)
belt_tension = (GET JSON "tension" IN PAYLOAD AS DOUBLE)
emergency_stop_active = (GET JSON "estop" IN PAYLOAD AS BOOL)
direction = (GET JSON "direction" IN PAYLOAD AS STRING)
```
An event system for production, maintenance, and quality events:
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
// Base event structure
DEFINE MODEL BaseEvent COLLAPSED
ADD STRING "event_id"
ADD STRING "event_category"
ADD STRING "source"
ADD STRING "timestamp"
ADD STRING "severity"
ADD STRING "description"
// Production-specific events
DEFINE MODEL ProductionEvent FROM BaseEvent
ADD STRING "line_id"
ADD STRING "batch_id"
ADD INT "parts_affected"
ADD STRING "operator"
ADD DOUBLE "production_impact"
// Quality-specific events
DEFINE MODEL QualityEvent FROM BaseEvent
ADD STRING "inspection_station"
ADD STRING "product_batch"
ADD INT "items_tested"
ADD INT "items_failed"
ADD STRING "failure_reason"
// Maintenance-specific events
DEFINE MODEL MaintenanceEvent FROM BaseEvent
ADD STRING "equipment_id"
ADD STRING "maintenance_type"
ADD INT "downtime_minutes"
ADD STRING "technician"
ADD DOUBLE "cost_estimate"
```
A sensor hierarchy for different measurement types:
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
// Base sensor structure
DEFINE MODEL BaseSensor COLLAPSED
ADD STRING "sensor_id"
ADD STRING "sensor_name"
ADD STRING "location"
ADD STRING "status"
ADD STRING "last_calibration"
ADD BOOL "calibration_valid"
ADD STRING "timestamp"
ADD INT "reading_count"
// Temperature sensor
DEFINE MODEL TemperatureSensor FROM BaseSensor
ADD DOUBLE "temperature"
ADD STRING "temperature_unit"
ADD DOUBLE "min_range"
ADD DOUBLE "max_range"
ADD DOUBLE "accuracy"
// Pressure sensor
DEFINE MODEL PressureSensor FROM BaseSensor
ADD DOUBLE "pressure"
ADD STRING "pressure_unit"
ADD DOUBLE "max_working_pressure"
ADD STRING "connection_type"
ADD BOOL "overpressure_protection"
// Flow sensor
DEFINE MODEL FlowSensor FROM BaseSensor
ADD DOUBLE "flow_rate"
ADD STRING "flow_unit"
ADD DOUBLE "pipe_diameter"
ADD STRING "fluid_type"
ADD DOUBLE "viscosity"
```
***
## Best Practices
Only include fields that **every** child model needs. If only some variants use a field, it belongs in the child model, not the base.
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
// Good - base has universal fields only
DEFINE MODEL BaseAlert COLLAPSED
ADD STRING "alert_id"
ADD STRING "timestamp"
ADD STRING "severity"
// Bad - 'temperature_unit' doesn't apply to all alerts
DEFINE MODEL BaseAlert COLLAPSED
ADD STRING "alert_id"
ADD STRING "timestamp"
ADD STRING "severity"
ADD STRING "temperature_unit" // Too specific!
```
Name child models to clearly indicate their specialization:
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
// Good - clear inheritance relationship
DEFINE MODEL TemperatureAlert FROM BaseAlert
DEFINE MODEL PressureAlert FROM BaseAlert
// Bad - unclear relationship
DEFINE MODEL TempData FROM BaseAlert
DEFINE MODEL PressData FROM BaseAlert
```
When you have multiple inheritance levels, keep the structure clear:
```
BaseEntity
└── BaseEquipment
├── PumpEquipment
├── ConveyorEquipment
└── MotorEquipment
```
When using `PUBLISH MODEL`, you must provide values for both inherited and specialized fields:
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
PUBLISH MODEL TemperatureAlert TO "alerts/temp" WITH
// Don't forget inherited fields!
alert_id = (RANDOM UUID)
timestamp = TIMESTAMP "UTC"
severity = "HIGH"
// Plus specialized fields
temperature_value = 85.5
threshold_exceeded = 80.0
```
***
## Limitations
**Current limitations of model inheritance:**
* **Single inheritance only** — A model can only inherit from one parent (no multiple inheritance)
* **No field override** — Child models cannot redefine fields from the parent with different types
* **COLLAPSED required** — Base models should use `COLLAPSED` since they're templates, not triggered models
***
## Next Steps
Master the PUBLISH MODEL syntax for dynamic data creation.
See more inheritance examples in context.
# MODEL Overview
Source: https://docs.coreflux.org/latest/lot-language/models/overview
Learn how LoT models transform, structure, and format MQTT data into consistent JSON schemas
## What are Models?
Models in LoT are like database schemas for MQTT data. They define the structure of your data, ensure consistency across your system, and provide automatic JSON formatting.
Models transform raw MQTT messages into structured, typed data - eliminating the need for external data transformation layers.
## Model Capabilities
Define fields with specific types (STRING, INT, DOUBLE, BOOL, ARRAY) to create consistent data schemas.
Automatically explode incoming JSON payloads into individual topics for easy field access.
Combine data from multiple MQTT topics into a single structured output.
Perform inline calculations and conditional logic to derive new values.
***
## How Models Work
Models do more than format data. They aggregate values from multiple MQTT topics, scale across device fleets with wildcards, compute derived metrics inline, and apply conditional logic — all within the broker, with zero external code. Here's how the core flow works.
### Receiving JSON Payloads
When a device sends a full telemetry payload, you often need to access individual fields independently — for threshold alerts, dashboard widgets, or other models. All models automatically handle this by "exploding" incoming JSON payloads into individual topics:
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
DEFINE MODEL SensorData WITH TOPIC "sensors/device1/data"
```
**When JSON arrives at** `sensors/device1/data`:
```json wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
{
"temperature": 23.5,
"humidity": 65,
"status": "active"
}
```
**LoT automatically creates:**
* `sensors/device1/data/temperature` → `23.5`
* `sensors/device1/data/humidity` → `65`
* `sensors/device1/data/status` → `"active"`
This "explosion" behavior allows you to subscribe to individual fields or use them in other models and actions.
### Producing Structured Output
When downstream systems — databases, dashboards, REST APIs — expect a specific JSON schema, a model guarantees that format on every publish. When a model triggers (either automatically or via action), it publishes structured JSON combining all its fields:
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
DEFINE MODEL EquipmentStatus WITH TOPIC "equipment/formatted"
ADD STRING "equipment_id" WITH TOPIC "equipment/id"
ADD STRING "status" WITH TOPIC "equipment/status" AS TRIGGER
ADD INT "runtime_hours" WITH TOPIC "equipment/runtime"
ADD STRING "last_update" WITH TIMESTAMP "UTC"
```
**Produces:**
```json wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
{
"equipment_id": "PUMP001",
"status": "RUNNING",
"runtime_hours": 1250,
"last_update": "2025-10-25T14:30:15Z"
}
```
### Aggregating Multiple Sources
When you need a unified equipment dashboard from scattered PLC topics, or a combined production report from sensors across different lines, a single model pulls live values from many independent topics and merges them into one structured output — replacing external aggregation scripts or middleware entirely.
```mermaid actions={false} theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
flowchart LR
subgraph Inputs
A[equipment/current/id]
B[equipment/current/status]
C[equipment/current/runtime]
D[equipment/current/efficiency]
end
subgraph Model
E[EquipmentStatus]
end
subgraph Output
F[equipment/status/formatted]
end
A --> E
B -->|triggers| E
C --> E
D --> E
E -->|publishes| F
```
Each field draws from a different source topic, but the model waits for the trigger field (`status`) to update before publishing a combined snapshot:
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
DEFINE MODEL EquipmentStatus WITH TOPIC "equipment/status/formatted"
ADD STRING "equipment_id" WITH TOPIC "equipment/current/id"
ADD STRING "status" WITH TOPIC "equipment/current/status" AS TRIGGER
ADD INT "runtime_hours" WITH TOPIC "equipment/current/runtime"
ADD DOUBLE "efficiency" WITH TOPIC "equipment/current/efficiency"
ADD BOOL "maintenance_required" WITH TOPIC "equipment/current/maintenance_flag"
ADD STRING "last_update" WITH TIMESTAMP "UTC"
```
### Building Nested Objects with Indentation
When your API or database expects grouped fields — such as a `metadata` object inside a sensor reading, or a `status` block with nested `maintenance` details — use the `OBJECT` type to produce nested JSON directly. Fields indented one level deeper than `ADD OBJECT` become properties of that sub-object — the same way `ADD` fields under `DEFINE MODEL` become top-level properties.
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
DEFINE MODEL SensorWithMetadata WITH TOPIC "sensors/+/formatted"
ADD STRING "sensor_id" WITH TOPIC "sensors/+/id"
ADD DOUBLE "value" WITH TOPIC "sensors/+/value" AS TRIGGER
ADD STRING "unit" WITH "celsius"
ADD OBJECT "metadata"
ADD STRING "location" WITH TOPIC "sensors/+/location"
ADD INT "floor" WITH TOPIC "sensors/+/floor"
ADD STRING "building" WITH TOPIC "sensors/+/building"
ADD STRING "timestamp" WITH TIMESTAMP "UTC"
```
**Produces:**
```json wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
{
"sensor_id": "TEMP001",
"value": 25.5,
"unit": "celsius",
"metadata": {
"location": "Room 101",
"floor": 3,
"building": "Building A"
},
"timestamp": "2025-10-25T14:30:15Z"
}
```
The indentation level determines the nesting. Fields at the same depth as `ADD OBJECT` (like `"timestamp"` above) return to the parent object.
Indentation must be consistent. Use either tabs or spaces throughout your model — mixing them will cause parsing errors. Each nesting level adds one indentation step.
### What Else Can Models Do?
The examples above cover the fundamentals, but models go much further:
* **Nested Objects** — Use the `OBJECT` type with indentation to group related fields into sub-objects, producing clean nested JSON hierarchies. [See examples →](./examples#sensor-with-metadata)
* **Wildcard Models** — Define one model with `+` wildcards and it automatically handles every matching device. One definition, hundreds of instances, zero duplication. [See examples →](./examples#multi-instance-sensor-model)
* **Inline Calculations** — Compute efficiency percentages, unit conversions, and running averages directly inside the model definition — no external processing needed. [See examples →](./examples#production-record-with-calculations)
* **Conditional Logic** — Dynamically tag field values based on live data: label readings as `"HIGH"` or `"NORMAL"`, flag quality as `"PASS"` or `"FAIL"`, assign priority levels.
* **Action-Published Templates** — Use `COLLAPSED` models as reusable schemas, then publish them from actions with full control over timing, routing, and conditional decisions. [Learn more →](./publish-model)
* **Protobuf (beta)** — Match a vendor `.proto` with `WITH FORMAT PROTOBUF`, `PROTO_TAG`, and `ADD MODEL`, then decode with `GET PROTO` or emit bytes with `PUBLISH MODEL`. [Learn more →](./protobuf)
For example, a single wildcard model automatically scales across every matching device — one definition handles your entire fleet:
```mermaid actions={false} theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
flowchart LR
subgraph singleDef [Single Definition]
A["GenericSensor sensors/+/formatted"]
end
subgraph autoInst [Auto-Created Instances]
B[sensors/temp001/formatted]
C[sensors/pressure002/formatted]
D[sensors/humidity003/formatted]
end
A --> B
A --> C
A --> D
```
[See the full wildcard example →](./examples#multi-instance-sensor-model)
***
## Model Types
LoT supports three model patterns, each suited for different scenarios:
| Pattern | Use When |
| ------------------------------ | ---------------------------------------------------- |
| **Basic Model** | You want automatic triggering on data changes |
| **Template Model (COLLAPSED)** | You need control over when and where data is created |
| **Inheritance (FROM)** | You have related data types sharing common fields |
### Basic Models
Basic models automatically trigger and publish structured data when their trigger field updates:
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
DEFINE MODEL SensorReading WITH TOPIC "sensors/formatted/temperature"
ADD STRING "sensor_id" WITH "TEMP001"
ADD DOUBLE "value" WITH TOPIC "sensors/raw/temperature" AS TRIGGER
ADD STRING "unit" WITH "celsius"
ADD STRING "timestamp" WITH TIMESTAMP "UTC"
```
**Key characteristics:**
* `WITH TOPIC` specifies where output is published
* `AS TRIGGER` defines which field change causes publication
* Runs automatically when trigger field updates
### Template Models (COLLAPSED)
Template models are reusable schemas for dynamic data creation from within actions. The `COLLAPSED` keyword indicates they have no automatic trigger:
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
DEFINE MODEL AlarmRecord COLLAPSED
ADD STRING "alarm_id"
ADD STRING "equipment_id"
ADD STRING "severity"
ADD STRING "timestamp"
DEFINE ACTION ProcessAlarm
ON TOPIC "alarms/+/input" DO
SET "equip_id" WITH TOPIC POSITION 2
PUBLISH MODEL AlarmRecord TO "alarms/structured/" + {equip_id} WITH
alarm_id = (RANDOM UUID)
equipment_id = {equip_id}
severity = (GET JSON "severity" IN PAYLOAD AS STRING)
timestamp = TIMESTAMP "UTC"
```
**Key characteristics:**
* `COLLAPSED` means no automatic trigger
* Published explicitly with `PUBLISH MODEL`
* Full control over when and where data is created
Learn how to publish template models from actions in [Publishing Models](./publish-model).
### Model Inheritance (FROM)
Create model families with shared fields using the `FROM` keyword:
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
DEFINE MODEL BaseAlert COLLAPSED
ADD STRING "alert_id"
ADD STRING "timestamp"
ADD STRING "severity"
DEFINE MODEL TemperatureAlert FROM BaseAlert
ADD DOUBLE "temperature_value"
ADD DOUBLE "threshold"
```
**Key characteristics:**
* Child models inherit all parent fields
* Add specialized fields as needed
* Promotes code reuse and consistency
Learn more about designing model hierarchies in [Model Inheritance](./model-inheritance).
***
## Data Sources
Model fields can be populated from various sources:
Use static values for constants like units, default thresholds, or fixed identifiers that never change:
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
DEFINE MODEL StaticExample WITH TOPIC "output/static"
ADD STRING "unit" WITH "celsius"
ADD INT "default_threshold" WITH 100
ADD BOOL "enabled" WITH TRUE
```
Pull live values from MQTT topics. Use `AS TRIGGER` on the field that should cause the model to publish:
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
DEFINE MODEL TopicExample WITH TOPIC "output/dynamic"
ADD DOUBLE "temperature" WITH TOPIC "sensors/raw/temp"
ADD STRING "status" WITH TOPIC "equipment/status" AS TRIGGER
```
Add automatic timestamps in UTC format or as Unix epoch integers for time-series data:
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
DEFINE MODEL TimestampExample WITH TOPIC "output/timed"
ADD STRING "created_at" WITH TIMESTAMP "UTC"
ADD INT "unix_time" WITH TIMESTAMP "UNIX"
```
Compute derived values inline using arithmetic operations on topic data:
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
DEFINE MODEL CalculatedExample WITH TOPIC "output/calculated"
ADD DOUBLE "efficiency" WITH (GET TOPIC "produced" AS DOUBLE / GET TOPIC "target" AS DOUBLE * 100)
```
Apply conditional logic to set field values based on dynamic conditions:
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
DEFINE MODEL ConditionalExample WITH TOPIC "output/conditional"
ADD STRING "status" WITH IF (GET TOPIC "value" > 80) THEN "HIGH" ELSE "NORMAL"
```
***
## Next Steps
Learn the complete syntax for defining model fields and triggers.
Create model families with shared fields using FROM.
# MODEL Protobuf
Source: https://docs.coreflux.org/latest/lot-language/models/protobuf
Decode and encode binary protobuf on MQTT with LoT models, GET PROTO, and PUBLISH MODEL
## Speak the same binary schema as your gateway
Industrial gateways and PLCs often publish **binary protobuf** on MQTT while the broker maps plant tags (Modbus, S7, OPC UA) to plain topics. LoT models let you **decode** those commands, run normal tag logic in the middle, and **encode** telemetry back — without a separate transformation service.
Dynamic protobuf encoding and decoding in LoT is **beta**. Prefer **`PUBLISH MODEL`** for binary MQTT output. Match your vendor `.proto` with `PROTO_TAG` and report issues with that layout and your broker version.
**Wire numbers are mailbox slots, not labels.** Protobuf finds a field by its number (`PROTO_TAG`), not by the LoT or `.proto` field name. If the slot numbers drift, the message still arrives — it just lands in the wrong boxes, or looks empty.
### When to use this
Use protobuf models when:
* a gateway or SCADA system publishes **raw protobuf bytes** on MQTT;
* you must **emit protobuf** that a vendor decoder already understands;
* field numbers in the `.proto` are **sparse** (for example `101`) or nested.
Use [JSON models](./overview) and [`GET JSON`](/latest/lot-language/actions/operations#get-json) when payloads are JSON. Use the [Sparkplug B Route](/latest/lot-language/routes/system/sparkplug-b) when the schema is Sparkplug B (`spBv1.0/#`), not a custom plant `.proto`.
***
Start from the direction you need: decode inbound bytes, publish outbound bytes, or both.
## Quick Start
Read top-level fields from a binary command payload with `GET PROTO`. Declare every command arm so unused submessages keep their wire numbers:
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
DEFINE MODEL DeviceCommand WITH FORMAT PROTOBUF COLLAPSED
ADD INT "command_type" PROTO_TAG 1
ADD OBJECT "header" PROTO_TAG 2
ADD OBJECT "write_setpoint" PROTO_TAG 3
ADD OBJECT "ack_alarm" PROTO_TAG 4
ADD OBJECT "reserved_slot" PROTO_TAG 5
ADD OBJECT "read_snapshot" PROTO_TAG 6
```
The Action reads `command_type` from the triggering payload:
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
DEFINE ACTION OnPlcCommand
ON TOPIC "plant/+/+/command" DO
SET "cmd" WITH (GET PROTO "command_type" IN PAYLOAD AS INT USING MODEL "DeviceCommand")
KEEP TOPIC "plant/pending_command" WITH {cmd}
```
Nested messages you fill on publish use `ADD MODEL`:
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
DEFINE MODEL MessageHeader WITH FORMAT PROTOBUF COLLAPSED
ADD STRING "device_id" PROTO_TAG 1
ADD STRING "timestamp" PROTO_TAG 2
DEFINE MODEL ProcessValues WITH FORMAT PROTOBUF COLLAPSED
ADD DOUBLE "temperature" PROTO_TAG 1
ADD DOUBLE "pressure" PROTO_TAG 2
DEFINE MODEL TelemetryMessage WITH FORMAT PROTOBUF COLLAPSED
ADD INT "message_type" PROTO_TAG 1
ADD MODEL MessageHeader "header" PROTO_TAG 2
ADD MODEL ProcessValues "process_values" PROTO_TAG 3
```
The Action packs JSON tag values into that envelope and publishes **bytes** on the gateway topic:
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
DEFINE ACTION PublishTelemetry
ON TOPIC "plant/+/+/tags" DO
SET "line" WITH TOPIC POSITION 2
SET "device_id" WITH TOPIC POSITION 3
SET "temp" WITH GET JSON "Temperature" IN PAYLOAD AS DOUBLE
SET "pressure" WITH GET JSON "Pressure" IN PAYLOAD AS DOUBLE
SET "ts" WITH TIMESTAMP "UNIX"
PUBLISH MODEL TelemetryMessage TO "plant/" + {line} + "/" + {device_id} + "/telemetry" WITH
message_type = 2
header.timestamp = {ts}
process_values.temperature = {temp}
process_values.pressure = {pressure}
```
***
## Where protobuf applies
| Mechanism | Protobuf on MQTT? |
| ------------------------------------------------------------ | ------------------------------------------------------------------------- |
| **`PUBLISH MODEL … TO "topic"`** in an Action | **Yes** — binary (or JSON plus a `/protobuf` sidecar topic for `BOTH`) |
| **Trigger-based `DEFINE MODEL … WITH TOPIC` + `AS TRIGGER`** | **No** — still JSON on the base topic; use **`PUBLISH MODEL`** for binary |
| **OT route tag topics** (Modbus, S7, OPC UA mappings) | Usually **JSON or plain values** — not protobuf unless you add it |
Define protobuf models for **edge** topics. Use **`GET PROTO`** to read and **`PUBLISH MODEL`** to write. Tag reads and writes stay on your existing OT routes.
```mermaid actions={false} theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
sequenceDiagram
participant GW as Gateway or SCADA
participant Broker
participant PLC as PLC via OT route
GW->>Broker: DeviceCommand proto bytes
Broker->>Broker: GET PROTO command_type
Broker->>PLC: TRIGGER route or tag write
PLC->>Broker: tag values on MQTT topics
Broker->>GW: PUBLISH MODEL TelemetryMessage proto bytes
```
***
## Match your `.proto`
To stay aligned with a hand-written schema (including **non-contiguous** field numbers and nested messages):
1. **`PROTO_TAG`** — wire numbers must match the `.proto` (not field names).
2. **Nested messages you fill on publish** — **`ADD MODEL ChildName "field"`** where `ChildName` is a `DEFINE MODEL`.
3. **Reserved wire slots** — **`ADD OBJECT "field" PROTO_TAG n`** when the parent must keep empty submessage fields so later tags stay aligned.
`GET PROTO` and `GET PROTOBUF` are the same verb. `PROTO` and `PROTOBUF` are the same format keyword.
### Type mapping
A field is only decoded when the **wire type** the sender used matches the type your model implies. Get this wrong and that field is skipped — there is no error on the wire.
| Proto type | Declare in LoT | Read with `GET PROTO … AS` |
| -------------------------------------------------------------------- | ------------------------- | ---------------------------- |
| `double` | `ADD DOUBLE` | `DOUBLE` |
| `int32` · `int64` · `uint32` · `uint64` · `sint32` · `sint64` · enum | `ADD INT` | `INT` |
| `bool` | `ADD BOOL` | `BOOL` |
| `string` | `ADD STRING` | `STRING` |
| `bytes` | `ADD OBJECT` | `BYTES` |
| nested `message` (publishing a filled body) | `ADD MODEL Child "field"` | — (fill via `PUBLISH MODEL`) |
| nested `message` (holding a wire slot open) | `ADD OBJECT "field"` | `BYTES` |
`ADD INT` is a **varint**. Proto `fixed32`, `sfixed32`, `fixed64`, and `sfixed64` use a different wire encoding and have no LoT keyword (`ADD FIXED32` does not exist). If those types appear in the vendor schema, declaring them as `ADD INT` will skip or misread the field.
### What happens on a mismatch
The decoder follows the protobuf unknown-field rule rather than failing the whole message:
| Situation | Result |
| -------------------------------------------- | ------------------------------------------------------------------ |
| Declared type matches the incoming wire type | Field decodes |
| Field number not in your model | Skipped |
| Truncated or malformed field | Decoding of that message stops there; fields already read are kept |
A skipped field is **absent**, and `GET PROTO` returns `null` for it. There is no error log — an unexpected `null` almost always means a wire-type mismatch.
### How field numbers are assigned
The broker does not ship a plant schema. Numbers come from your `DEFINE MODEL`:
| Your model definition | Wire field numbers |
| --------------------- | ----------------------------------------------- |
| No `PROTO_TAG` | **1, 2, 3, …** in LoT source order |
| Every field tagged | Exactly those tags |
| Mixed | Tagged = explicit; untagged = next free integer |
If the `.proto` uses field **101** but LoT omits `PROTO_TAG 101`, the next sequential number is used and the gateway decoder fails.
***
## Inbound: `GET PROTO`
Prefer **`IN PAYLOAD`** on the triggering topic. Use the other sources when the bytes are already cached or wrapped as Base64:
| Source | Bytes come from |
| ----------------------- | ------------------------------------------------------- |
| **`IN PAYLOAD`** | The MQTT payload that fired the Action |
| **`IN GET TOPIC "…"`** | Last payload stored for that topic |
| **`IN` a LoT variable** | A Base64 string in that variable (decoded before parse) |
Pass **`USING MODEL "Name"`** unless a `DEFINE MODEL … WITH TOPIC` already registered that schema on the topic. **`AS INT` / `DOUBLE` / `STRING` / `BOOL` / `BYTES`** selects the LoT type after decode.
Read from the last payload stored on another topic:
```lot wrap focus={3} theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
DEFINE ACTION RetryCachedCommand
ON TOPIC "plant/+/+/command/retry" DO
SET "cmd" WITH (GET PROTO "command_type" IN GET TOPIC "plant/line1/plc01/command" AS INT USING MODEL "DeviceCommand")
```
Or decode Base64 that arrived as JSON:
```lot wrap focus={4} theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
DEFINE ACTION DecodeBase64Command
ON TOPIC "plant/+/encoded" DO
SET "b64" WITH GET JSON "payload_b64" IN PAYLOAD AS STRING
SET "cmd" WITH (GET PROTO "command_type" IN {b64} AS INT USING MODEL "DeviceCommand")
```
Notes that matter in practice:
* **Each `GET PROTO` decodes the payload from scratch.** Reading two fields means two independent passes.
Do not use `IF PAYLOAD IS PROTO` to detect binary MQTT. Call **`GET PROTO … IN PAYLOAD`** instead.
### Parent command with reserved field numbers
Vendor `.proto` files often declare several command arms but send only one. List **every** submessage slot on the parent so unused fields do not shift wire numbers. Use **`ADD OBJECT`** for unused slots. When **publishing** a filled nested message, use **`ADD MODEL`** on the parent instead.
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
DEFINE MODEL DeviceCommand WITH FORMAT PROTOBUF COLLAPSED
ADD INT "command_type" PROTO_TAG 1
ADD OBJECT "header" PROTO_TAG 2
ADD OBJECT "write_setpoint" PROTO_TAG 3
ADD OBJECT "ack_alarm" PROTO_TAG 4
ADD OBJECT "reserved_slot" PROTO_TAG 5
ADD OBJECT "read_snapshot" PROTO_TAG 6
```
***
## Middle: tags stay JSON
Between command and telemetry, most OT setups already have **tag values** on topics — from a `DEFINE ROUTE` mapping, a collapsed JSON model, or single-value publishes. That step stays JSON or plain text unless your plant standard requires protobuf on those topics too.
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
DEFINE ACTION ReadPlcTags
ON TOPIC "plant/line1/plc01/tags" DO
SET "temp" WITH GET JSON "Temperature" IN PAYLOAD AS DOUBLE
SET "pressure" WITH GET JSON "Pressure" IN PAYLOAD AS DOUBLE
SET "speed" WITH GET JSON "MotorSpeed" IN PAYLOAD AS DOUBLE
PUBLISH TOPIC "plant/line1/plc01/snapshot/temperature" WITH {temp}
```
***
## Outbound: `PUBLISH MODEL`
### Topic vs format
| Model format | Payload on **`TO "…"`** topic |
| ---------------------------------------- | -------------------------------------------------------------- |
| **`WITH FORMAT JSON`** (default) | JSON string |
| **`WITH FORMAT PROTOBUF`** / **`PROTO`** | Raw protobuf **bytes** |
| **`WITH FORMAT BOTH`** | JSON on the main topic + binary on a `/protobuf` sidecar topic |
The next `PUBLISH MODEL` to the same topic **merges**: binary for `PROTO` only; JSON from the main topic for `JSON` / `BOTH`. **`KEEP` + protobuf** updates the byte cache without per-field JSON fan-out.
Trigger-based models (`WITH TOPIC` + `AS TRIGGER`) still auto-publish **JSON** on the base topic. Use `PUBLISH MODEL` when the edge must see binary.
### Response envelope with sparse tags
Alarms and events often use high field numbers (**101**, **102**). Tag them explicitly; use **`ADD MODEL`** for nested bodies you fill on publish. `MessageHeader` and `ProcessValues` match Quick Start — the new pieces are `AlarmEvent` and field **101**:
```lot wrap focus={10-18} theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
DEFINE MODEL MessageHeader WITH FORMAT PROTOBUF COLLAPSED
ADD STRING "device_id" PROTO_TAG 1
ADD STRING "timestamp" PROTO_TAG 2
DEFINE MODEL ProcessValues WITH FORMAT PROTOBUF COLLAPSED
ADD DOUBLE "temperature" PROTO_TAG 1
ADD DOUBLE "pressure" PROTO_TAG 2
ADD INT "motor_speed" PROTO_TAG 3
DEFINE MODEL AlarmEvent WITH FORMAT PROTOBUF COLLAPSED
ADD INT "alarm_code" PROTO_TAG 1
ADD STRING "alarm_text" PROTO_TAG 2
DEFINE MODEL TelemetryMessage WITH FORMAT PROTOBUF COLLAPSED
ADD INT "message_type" PROTO_TAG 1
ADD MODEL MessageHeader "header" PROTO_TAG 2
ADD MODEL ProcessValues "process_values" PROTO_TAG 3
ADD MODEL AlarmEvent "alarm_event" PROTO_TAG 101
```
LoT names are for **`PUBLISH MODEL`** / **`GET PROTO`**. Wire numbers come only from **`PROTO_TAG`**. Set `message_type` to the enum integer your `.proto` expects.
### Raw `bytes` fields
Some messages carry opaque **`bytes`** (recipe blob, certificate, file chunk). Assign them with **`BYTES FROM BASE64`**:
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
DEFINE MODEL RecipeBlock WITH FORMAT PROTOBUF COLLAPSED
ADD INT "recipe_id" PROTO_TAG 1
ADD OBJECT "payload" PROTO_TAG 2
```
Then the Action fills `payload` from a Base64 JSON field:
```lot wrap focus={3-5} theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
DEFINE ACTION StoreRecipe
ON TOPIC "plant/+/recipe/upload" DO
PUBLISH MODEL RecipeBlock TO "plant/archive/recipe" WITH
recipe_id = 42
payload = BYTES FROM BASE64 (GET JSON "data_b64" IN PAYLOAD AS STRING)
```
To forward an entire protobuf payload unchanged (no field access), use [`PAYLOAD AS BYTES`](/latest/lot-language/actions/operations#binary-passthrough--payload-as-bytes) instead of `GET PROTO`.
***
## Common pitfalls
**`ADD OBJECT`** when **publishing** a populated nested **message** produces JSON-in-bytes. Use **`ADD MODEL`**.
Events at **101+** need an explicit tag. Omitting unused command slots on the parent also shifts later wire indexes.
`device` is a LoT keyword, so `SET "device"` may parse but interpolating that name does not. Use `device_id`. If an Action fails with `Unexpected '' where IDENTIFIER was expected`, rename the variable.
***
## Troubleshooting
`GET PROTO` returns `"null"` rather than raising, so a wrong schema looks like missing data.
| Symptom | Likely cause | Fix |
| ------------------------------------------------------ | ------------------------------------------------------------ | ------------------------------------------------------------------------- |
| Every field is `null` | Wrong model in `USING MODEL`, or the payload is not protobuf | Confirm the message name and that the publisher sends raw bytes, not JSON |
| Values decode but the gateway rejects what you publish | Field numbers drifted from the `.proto` | Add explicit `PROTO_TAG` to every field |
| Numbers are wildly wrong (huge or negative) | Fixed-width integer read as a varint, or signedness mismatch | Check `fixed32` vs `sfixed32` in the `.proto` |
To confirm what is on the wire, decode a captured payload with a protobuf raw decoder (`protoc --decode_raw`). It reports field numbers and wire types without needing the schema — that pair must match your `DEFINE MODEL`.
**Beta limitations**
* Trigger-based models auto-publish JSON only — use `PUBLISH MODEL` for binary.
* No native `google.protobuf.Timestamp`; carry timestamps as `STRING` or Unix `INT`.
* The sidecar topic for `BOTH` is a `/protobuf` suffix on the destination topic (not `/proto`).
* No protobuf **groups** (wire types 3 and 4) — a group stops decoding of that message.
* Packed repeated scalars are skipped so decoding can continue; they are not expanded into LoT arrays.
***
## Next Steps
Control when and where COLLAPSED models publish, including protobuf bytes.
Full GET PROTO syntax, sources, and types.
# MODEL Publishing
Source: https://docs.coreflux.org/latest/lot-language/models/publish-model
Create structured data dynamically from actions using COLLAPSED models and PUBLISH MODEL
## When to Use Action Models
Action models (COLLAPSED models) are templates for structured data that you publish explicitly from within actions. They're ideal when you need:
* **Control over timing** - Publish only when specific conditions are met
* **Dynamic routing** - Send data to different topics based on logic
* **Data transformation** - Process and enrich data before structuring
Unlike basic models that trigger automatically on topic changes, action models let you decide exactly when and where structured data is created. With `WITH FORMAT PROTOBUF`, that output is binary protobuf bytes rather than JSON — see [Protobuf Models](./protobuf).
### Example Use Case
Imagine you receive raw alarm data and need to:
1. Extract relevant fields from JSON
2. Add a unique ID and timestamp
3. Route critical alarms to a special topic
4. Publish a structured record
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
DEFINE MODEL AlarmRecord COLLAPSED
ADD STRING "alarm_id"
ADD STRING "equipment_id"
ADD STRING "severity"
ADD STRING "timestamp"
DEFINE ACTION ProcessAlarm
ON TOPIC "alarms/+/input" DO
SET "equipment_id" WITH TOPIC POSITION 2
SET "severity" WITH (GET JSON "severity" IN PAYLOAD AS STRING)
PUBLISH MODEL AlarmRecord TO "alarms/structured/" + {equipment_id} WITH
alarm_id = (RANDOM UUID)
equipment_id = {equipment_id}
severity = {severity}
timestamp = TIMESTAMP "UTC"
IF {severity} EQUALS "CRITICAL" THEN
PUBLISH TOPIC "alerts/critical/" + {equipment_id} WITH PAYLOAD
```
***
## Step-by-Step Guide
Create a template with all required fields. The `COLLAPSED` keyword indicates this model has no automatic trigger:
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
DEFINE MODEL SensorRecord COLLAPSED
ADD STRING "sensor_id"
ADD STRING "sensor_type"
ADD DOUBLE "temperature"
ADD DOUBLE "humidity"
ADD STRING "quality_status"
ADD STRING "timestamp"
```
Define an action that extracts data and publishes the model:
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
DEFINE ACTION ProcessSensorJSON
ON TOPIC "sensors/+/json_data" DO
SET "sensor_id" WITH TOPIC POSITION 2
// Extract from JSON payload
SET "temp" WITH (GET JSON "temperature" IN PAYLOAD AS DOUBLE)
SET "hum" WITH (GET JSON "humidity" IN PAYLOAD AS DOUBLE)
SET "type" WITH (GET JSON "type" IN PAYLOAD AS STRING)
SET "quality" WITH (GET JSON "quality" IN PAYLOAD AS STRING)
// Publish structured record
PUBLISH MODEL SensorRecord TO "sensors/processed/" + {sensor_id} WITH
sensor_id = {sensor_id}
sensor_type = {type}
temperature = {temp}
humidity = {hum}
quality_status = {quality}
timestamp = TIMESTAMP "UTC"
```
**Publish to:** `sensors/temp001/json_data`
```json wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
{
"temperature": 23.5,
"humidity": 65,
"type": "environmental",
"quality": "GOOD"
}
```
**Result at:** `sensors/processed/temp001`
```json wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
{
"sensor_id": "temp001",
"sensor_type": "environmental",
"temperature": 23.5,
"humidity": 65,
"quality_status": "GOOD",
"timestamp": "2025-10-25T14:30:15Z"
}
```
***
## Use Case Examples
Route alarms by severity while creating structured records:
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
DEFINE MODEL AlarmRecord COLLAPSED
ADD STRING "alarm_id"
ADD STRING "equipment_id"
ADD STRING "alarm_type"
ADD DOUBLE "trigger_value"
ADD STRING "severity"
ADD STRING "timestamp"
DEFINE ACTION ProcessAlarmJSON
ON TOPIC "alarms/+/input" DO
SET "equipment_id" WITH TOPIC POSITION 2
PUBLISH MODEL AlarmRecord TO "alarms/structured/" + {equipment_id} WITH
alarm_id = (RANDOM UUID)
equipment_id = {equipment_id}
alarm_type = (GET JSON "type" IN PAYLOAD AS STRING)
trigger_value = (GET JSON "value" IN PAYLOAD AS DOUBLE)
severity = (GET JSON "severity" IN PAYLOAD AS STRING)
timestamp = TIMESTAMP "UTC"
// Route critical alarms
SET "sev" WITH (GET JSON "severity" IN PAYLOAD AS STRING)
IF {sev} EQUALS "CRITICAL" THEN
PUBLISH TOPIC "alerts/critical/" + {equipment_id} WITH PAYLOAD
```
Calculate metrics and generate quality reports:
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
DEFINE MODEL QualityReport COLLAPSED
ADD STRING "report_id"
ADD STRING "station_id"
ADD INT "items_tested"
ADD INT "items_passed"
ADD DOUBLE "pass_rate"
ADD STRING "timestamp"
DEFINE ACTION GenerateQualityReport
ON TOPIC "quality/+/results" DO
SET "station" WITH TOPIC POSITION 2
SET "tested" WITH (GET JSON "total" IN PAYLOAD AS INT)
SET "passed" WITH (GET JSON "passed" IN PAYLOAD AS INT)
SET "rate" WITH ({passed} AS DOUBLE / {tested} AS DOUBLE * 100)
PUBLISH MODEL QualityReport TO "reports/quality/" + {station} WITH
report_id = (RANDOM UUID)
station_id = {station}
items_tested = {tested}
items_passed = {passed}
pass_rate = {rate}
timestamp = TIMESTAMP "UTC"
IF {rate} < 90 THEN
PUBLISH TOPIC "alerts/quality/low_rate" WITH {station} + ": " + {rate} + "%"
```
Create structured event logs with context:
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
DEFINE MODEL EventLog COLLAPSED
ADD STRING "event_id"
ADD STRING "event_type"
ADD STRING "source"
ADD STRING "description"
ADD STRING "severity"
ADD STRING "timestamp"
DEFINE ACTION LogEvent
ON TOPIC "events/+/occurred" DO
SET "event_type" WITH TOPIC POSITION 2
PUBLISH MODEL EventLog TO "logs/events/" + {event_type} WITH
event_id = (RANDOM UUID)
event_type = {event_type}
source = (GET JSON "source" IN PAYLOAD AS STRING)
description = (GET JSON "message" IN PAYLOAD AS STRING)
severity = (GET JSON "severity" IN PAYLOAD AS STRING)
timestamp = TIMESTAMP "UTC"
```
***
## JSON vs protobuf on the destination topic
`PUBLISH MODEL` writes whatever format the model declares:
| Model format | Payload on **`TO "…"`** |
| -------------------------------- | -------------------------------------------------------------- |
| `WITH FORMAT JSON` (default) | JSON string |
| `WITH FORMAT PROTOBUF` / `PROTO` | Raw protobuf **bytes** |
| `WITH FORMAT BOTH` | JSON on the main topic + binary on a `/protobuf` sidecar topic |
Trigger-based models (`WITH TOPIC` + `AS TRIGGER`) still auto-publish JSON. Use a `COLLAPSED` model and `PUBLISH MODEL` when a gateway must receive protobuf. Nested messages you fill on publish use `ADD MODEL Child "field"`, not indented `ADD OBJECT`.
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
DEFINE MODEL TelemetryMessage WITH FORMAT PROTOBUF COLLAPSED
ADD INT "message_type" PROTO_TAG 1
ADD DOUBLE "temperature" PROTO_TAG 2
ADD DOUBLE "pressure" PROTO_TAG 3
```
The Action copies JSON tags into that envelope and publishes **bytes**:
```lot wrap focus={6-9} theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
DEFINE ACTION PublishTelemetry
ON TOPIC "plant/+/tags" DO
SET "temp" WITH GET JSON "Temperature" IN PAYLOAD AS DOUBLE
SET "pressure" WITH GET JSON "Pressure" IN PAYLOAD AS DOUBLE
PUBLISH MODEL TelemetryMessage TO "plant/gateway/telemetry" WITH
message_type = 2
temperature = {temp}
pressure = {pressure}
```
Full type mapping, `PROTO_TAG`, and `GET PROTO` are on [Protobuf Models](./protobuf).
***
## Best Practices
Explicit type casting prevents runtime errors:
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
// Good
SET "temp" WITH (GET JSON "temperature" IN PAYLOAD AS DOUBLE)
// Risky
SET "temp" WITH (GET JSON "temperature" IN PAYLOAD)
```
Check data before publishing:
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
SET "value" WITH (GET JSON "value" IN PAYLOAD AS DOUBLE)
IF {value} >= 0 AND {value} <= 100 THEN
PUBLISH MODEL ValidData TO "output" WITH value = {value}
ELSE
PUBLISH TOPIC "errors/validation" WITH "Invalid value: " + {value}
```
Include identifiers in output topics for easy filtering:
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
// For topic "sensors/temp001/data" - wildcard is at position 2
SET "sensor_id" WITH TOPIC POSITION 2
PUBLISH MODEL SensorData TO "processed/" + {sensor_id} + "/data" WITH ...
```
Use `RANDOM UUID` for record identification:
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
PUBLISH MODEL Record TO "records/new" WITH
record_id = (RANDOM UUID)
...
```
## Next Steps
See complete examples for common use cases.
Review available operations for action logic.
# MODEL Syntax and Components
Source: https://docs.coreflux.org/latest/lot-language/models/schema-definition
Complete reference for defining model schemas, field types, data sources, and triggers
## Why Define a Schema?
Models transform scattered MQTT data into structured, predictable JSON. Instead of parsing raw payloads in your application, you define *once* how data should look—and Coreflux handles the rest.
**Like a form that fills itself out.** You define what fields the form should have (temperature, humidity, timestamp), and the model automatically grabs the latest values from different sources whenever it needs to publish.
### When to Use This Reference
Use this page when you need to:
* Define field types for your model (STRING, INT, DOUBLE, etc.)
* Pull values from multiple MQTT topics into a single output
* Calculate or transform values inline
* Control *when* your model publishes using triggers
* Align protobuf field numbers with a vendor `.proto` (`PROTO_TAG`, `WITH FORMAT PROTOBUF`)
***
## Basic Model Syntax
Every model follows this structure:
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
DEFINE MODEL WITH TOPIC ""
ADD "" WITH [AS TRIGGER]
ADD "" WITH
...
```
### Components
Unique identifier for the model. Use descriptive names like `SensorReading`, `EquipmentStatus`, `ProductionRecord`.
The MQTT topic where the model's JSON output will be published. Supports wildcards for multi-instance models. Not required on `COLLAPSED` templates published with `PUBLISH MODEL`.
Optional. `JSON` (default), `PROTOBUF` / `PROTO`, or `BOTH`. Controls what `PUBLISH MODEL` puts on the destination topic. See [Protobuf Models](./protobuf).
One of: `STRING`, `INT`, `DOUBLE`, `BOOL`, `ARRAY`, `OBJECT`, or `MODEL` (nested protobuf message)
Optional. Marks this field as the trigger—the model publishes only when this field's source topic updates.
***
## Trigger Mechanism
The `AS TRIGGER` modifier controls *when* your model publishes. Without it, you'd flood your broker with updates every time *any* source topic changes.
**Like a doorbell that takes a photo.** The model waits quietly until the trigger topic "rings," then takes a snapshot of all fields and publishes the complete picture. No ring, no photo.
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
DEFINE MODEL TriggeredModel WITH TOPIC "output/triggered"
ADD STRING "equipment_id" WITH TOPIC "equipment/id"
ADD STRING "status" WITH TOPIC "equipment/status" AS TRIGGER
ADD INT "runtime" WITH TOPIC "equipment/runtime"
ADD STRING "timestamp" WITH TIMESTAMP "UTC"
```
**Behavior:**
* Model publishes **only** when `equipment/status` changes
* Changes to `equipment/id` or `equipment/runtime` alone do not trigger publication
* All field values are fetched at trigger time
### No Trigger Specified
If no field has `AS TRIGGER`, the model acts as a template. You must publish it explicitly using `PUBLISH MODEL` from an Action.
Learn how to publish models on-demand in [Publishing Models](./publish-model).
### Multiple Data Sources, Single Trigger
You can pull data from many topics but trigger on just one:
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
DEFINE MODEL MultiSourceModel WITH TOPIC "output/multi"
ADD STRING "sensor_a" WITH TOPIC "sensors/a/value"
ADD STRING "sensor_b" WITH TOPIC "sensors/b/value"
ADD STRING "sensor_c" WITH TOPIC "sensors/c/value" AS TRIGGER
ADD STRING "timestamp" WITH TIMESTAMP "UTC"
```
**Only publishes when sensor C updates**, but includes current values from sensors A and B.
***
## Field Types
Each field in your model has a type that determines how the value is serialized to JSON (or to protobuf when the model uses `WITH FORMAT PROTOBUF`).
### STRING
Text data of any length:
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
ADD STRING "sensor_id" WITH "TEMP001"
ADD STRING "status" WITH TOPIC "equipment/status"
ADD STRING "timestamp" WITH TIMESTAMP "UTC"
```
### INT
Whole numbers (integers):
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
ADD INT "count" WITH 0
ADD INT "runtime_hours" WITH TOPIC "equipment/runtime"
ADD INT "total" WITH (GET TOPIC "passed" + GET TOPIC "failed")
```
### DOUBLE
Decimal numbers (floating-point):
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
ADD DOUBLE "temperature" WITH 23.5
ADD DOUBLE "efficiency" WITH TOPIC "metrics/efficiency"
ADD DOUBLE "percentage" WITH (GET TOPIC "actual" AS DOUBLE / GET TOPIC "target" AS DOUBLE * 100)
```
### BOOL
Boolean values (true/false):
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
ADD BOOL "enabled" WITH TRUE
ADD BOOL "maintenance_required" WITH TOPIC "equipment/needs_maintenance"
ADD BOOL "is_valid" WITH (GET TOPIC "value" > 0)
```
### ARRAY
JSON arrays:
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
ADD ARRAY "readings" WITH TOPIC "sensors/batch/readings"
ADD ARRAY "tags" WITH ["production", "line1", "active"]
```
### OBJECT
Nested JSON objects. Fields indented one level deeper than `ADD OBJECT` become properties of the sub-object. When the indentation returns to the parent level, subsequent fields belong to the parent again.
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
ADD OBJECT "metadata"
ADD STRING "location" WITH "Building A"
ADD INT "floor" WITH 3
```
Produces a nested structure in the JSON output:
```json wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
{
"metadata": {
"location": "Building A",
"floor": 3
}
}
```
Indentation must be consistent throughout the model. Use either tabs or spaces — mixing them causes parsing errors.
### MODEL (nested protobuf)
Names another `DEFINE MODEL` as a nested protobuf message. Required when you **publish** a filled submessage. Pair with `PROTO_TAG` so the wire number matches the `.proto`.
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
ADD MODEL AudioInfo "audio_info" PROTO_TAG 2
```
Do not confuse this with indented `ADD OBJECT` for JSON nesting. For protobuf wire layout, see [Protobuf Models](./protobuf).
### PROTO\_TAG
Sets the protobuf field number. Without it, tags are assigned **1, 2, 3, …** in source order. Use an explicit tag whenever the `.proto` is not sequential.
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
ADD INT "message_type" PROTO_TAG 1
ADD MODEL AlarmEvent "alarm_event" PROTO_TAG 101
```
***
## Data Sources
Fields can pull values from several sources. Mix and match as needed.
### Static Values
Fixed values that never change—useful for metadata, units, or default settings:
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
DEFINE MODEL StaticExample WITH TOPIC "output/static"
ADD STRING "unit" WITH "celsius"
ADD INT "default_threshold" WITH 100
ADD DOUBLE "conversion_factor" WITH 1.8
ADD BOOL "enabled" WITH TRUE
ADD STRING "location" WITH "Factory Floor A"
```
### Topic Data
Pull live values from other MQTT topics. The value is fetched *at publish time*, not when the model is defined.
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
DEFINE MODEL TopicExample WITH TOPIC "output/dynamic"
ADD DOUBLE "temperature" WITH TOPIC "sensors/raw/temperature"
ADD STRING "status" WITH TOPIC "equipment/current/status" AS TRIGGER
ADD INT "count" WITH TOPIC "production/count"
```
**Example:** If `sensors/raw/temperature` currently holds `23.5`, the model's `temperature` field will be `23.5` when it publishes. If the value changes to `24.0` before the next publish, the model will output `24.0`.
### Timestamps
Built-in timestamp generation—no external time source needed:
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
ADD STRING "timestamp" WITH TIMESTAMP "UTC" // "2025-10-25T14:30:15Z"
ADD STRING "iso_time" WITH TIMESTAMP "ISO" // ISO 8601 format
ADD INT "unix_time" WITH TIMESTAMP "UNIX" // 1729864215
ADD INT "unix_ms" WITH TIMESTAMP "UNIX-MS" // Milliseconds since epoch
```
### Calculated Values
Perform inline math using topic data:
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
DEFINE MODEL CalculatedExample WITH TOPIC "output/calculated"
ADD DOUBLE "efficiency" WITH (GET TOPIC "produced" AS DOUBLE / GET TOPIC "target" AS DOUBLE * 100)
ADD INT "total" WITH (GET TOPIC "passed" + GET TOPIC "failed")
ADD DOUBLE "fahrenheit" WITH (GET TOPIC "celsius" AS DOUBLE * 9 / 5 + 32)
ADD DOUBLE "average" WITH ((GET TOPIC "val1" + GET TOPIC "val2" + GET TOPIC "val3") / 3)
```
Always use `AS DOUBLE` or `AS INT` when performing mathematical operations to ensure correct type handling.
### Conditional Values
Dynamic values based on conditions:
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
DEFINE MODEL ConditionalExample WITH TOPIC "output/conditional"
ADD STRING "status" WITH IF (GET TOPIC "value" > 80) THEN "HIGH" ELSE "NORMAL"
ADD STRING "quality" WITH IF (GET TOPIC "defects" = 0) THEN "PASS" ELSE "FAIL"
ADD INT "priority" WITH IF (GET TOPIC "severity" EQUALS "CRITICAL") THEN 1
ELSE IF (GET TOPIC "severity" EQUALS "HIGH") THEN 2
ELSE 3
```
***
## Wildcard Topics
### Multi-Instance Models
Use `+` wildcards to create models that handle multiple instances automatically:
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
DEFINE MODEL MultiInstanceModel WITH TOPIC "sensors/+/formatted"
```
**Matches:**
* `sensors/temp001/formatted`
* `sensors/pressure002/formatted`
* `sensors/humidity003/formatted`
Each unique topic creates a separate model instance.
### JSON Reception with Wildcards
When JSON arrives at a wildcard topic, the broker "explodes" it into individual topics:
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
DEFINE MODEL MaterialData WITH TOPIC "Process/+/+/Material/Content"
```
**When JSON arrives at** `Process/Machine/33/Material/Content`:
```json wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
{"MaterialID": 132323, "Name": "Peppers", "Quantity": 21}
```
**Explodes to:**
* `Process/Machine/33/Material/Content/MaterialID` → `132323`
* `Process/Machine/33/Material/Content/Name` → `Peppers`
* `Process/Machine/33/Material/Content/Quantity` → `21`
***
## Complete Examples
This model aggregates equipment data from multiple topics into a single, structured output. It triggers whenever the status changes.
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
DEFINE MODEL EquipmentStatus WITH TOPIC "equipment/status/formatted"
ADD STRING "equipment_id" WITH TOPIC "equipment/current/id"
ADD STRING "status" WITH TOPIC "equipment/current/status" AS TRIGGER
ADD INT "runtime_hours" WITH TOPIC "equipment/current/runtime"
ADD DOUBLE "efficiency" WITH TOPIC "equipment/current/efficiency"
ADD BOOL "maintenance_required" WITH TOPIC "equipment/current/maintenance_flag"
ADD STRING "last_update" WITH TIMESTAMP "UTC"
ADD STRING "location" WITH "Factory Floor A"
```
**Output:**
```json wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
{
"equipment_id": "PUMP001",
"status": "RUNNING",
"runtime_hours": 1250,
"efficiency": 87.5,
"maintenance_required": false,
"last_update": "2025-10-25T14:30:15Z",
"location": "Factory Floor A"
}
```
This model calculates efficiency metrics inline. It triggers when the quantity updates, capturing a snapshot of the production run.
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
DEFINE MODEL ProductionRecord WITH TOPIC "production/records/completed"
ADD STRING "batch_id" WITH TOPIC "production/current/batch_id"
ADD STRING "product_code" WITH TOPIC "production/current/product_code"
ADD INT "quantity_produced" WITH TOPIC "production/current/quantity" AS TRIGGER
ADD INT "quantity_target" WITH TOPIC "production/current/target"
ADD DOUBLE "efficiency_percent" WITH (GET TOPIC "production/current/quantity" AS DOUBLE / GET TOPIC "production/current/target" AS DOUBLE * 100)
ADD STRING "operator" WITH TOPIC "production/current/operator"
ADD STRING "completion_time" WITH TIMESTAMP "UTC"
```
**Output:**
```json wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
{
"batch_id": "BATCH-2025-1025",
"product_code": "PRD-A1234",
"quantity_produced": 850,
"quantity_target": 1000,
"efficiency_percent": 85.0,
"operator": "John Smith",
"completion_time": "2025-10-25T16:45:30Z"
}
```
***
## Best Practices
Establish naming conventions and follow them across all models:
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
// Good - consistent across models
ADD STRING "timestamp"
ADD STRING "sensor_id"
ADD STRING "equipment_id"
// Avoid - inconsistent
ADD STRING "time_stamp"
ADD STRING "sensorID"
ADD STRING "equip_ID"
```
Explicit type casting prevents errors and ensures predictable results:
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
// Good
ADD DOUBLE "efficiency" WITH (GET TOPIC "produced" AS DOUBLE / GET TOPIC "target" AS DOUBLE * 100)
// Risky - implicit type handling
ADD DOUBLE "efficiency" WITH (GET TOPIC "produced" / GET TOPIC "target" * 100)
```
Always include fields that help downstream consumers understand the data:
* Identification fields (IDs, names)
* Timestamps
* Units of measurement
* Status or quality indicators
Trigger on the primary data field, not metadata:
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
// Good - trigger on the main measurement
ADD DOUBLE "value" WITH TOPIC "sensors/temperature" AS TRIGGER
// Bad - triggers too frequently or unpredictably
ADD STRING "timestamp" WITH TIMESTAMP "UTC" AS TRIGGER
```
***
## Next Steps
Learn to publish models dynamically from Actions.
See complete model examples for common use cases.
# Actions & Events
Source: https://docs.coreflux.org/latest/lot-language/panels/actions-events
Reacting to clicks and value changes — publishing commands and navigating between panels.
Events make a panel interactive: a click publishes a command, a field change writes a setpoint. The pattern is always `ON DO `.
**Like a light switch wired to more than a light.** Flipping it (the event) can turn on the lamp, ring a bell, and log the time (the actions) — the switch doesn't care what's wired to it.
***
First the event triggers, then each action you can wire to them. The broker parses this grammar; which triggers the HUB actually dispatches, and any write-back caveats, are documented with the [input components](/hub/dashboards/components/inputs) — that catalog ships with the HUB, not the broker.
## Event triggers
| Event | Fires when |
| -------------- | ----------------------------------------------------------------------------------------------------------- |
| `ON CLICK DO` | Mouse click / tap |
| `ON CHANGE DO` | Value changed (`text-field`, `number-field`) |
| `ON TOGGLE DO` | Toggle state changed — see [input components](/hub/dashboards/components/inputs) for HUB write-back support |
## Actions
### Publish to a topic
The workhorse: send a command or value to any MQTT topic.
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
ON CLICK DO
PUBLISH TOPIC "factory/commands/start" WITH 1
```
Literal payloads on buttons work out of the box. On `text-field` and `number-field`, use `WITH VALUE` to publish what the operator typed — see the [Input components](/hub/dashboards/components/inputs) page for full write-back examples.
### Navigate between panels
Jump to another panel by name:
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
ON CLICK DO
NAVIGATE TO PANEL "Zone1Details"
```
Or return to the previous panel:
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
ON CLICK DO
NAVIGATE BACK
```
See the [Navigation section in Defining a Panel](/latest/lot-language/panels/defining-panels#navigation) for a complete overview-and-detail example with screenshots.
## Common mistakes
Input components don't publish by themselves — the `ON DO` handler does. A button without `ON CLICK DO PUBLISH ...` is a label in practice.
`PUBLISH` writes to the broker; the panel only reflects it if some component **binds** to that topic. Check the bound topic and the published topic match (command vs. feedback topics are often intentionally different).
## Next Steps
The controls that fire these events, with full write-back examples.
Complete panels that put events and actions to work.
# Data Binding
Source: https://docs.coreflux.org/latest/lot-language/panels/data-binding
Wiring components to live MQTT topics — JSON extraction, coercion, and formatting.
A binding is the live wire between a component and your data. When the source changes, the widget updates — no polling, no refresh. Most "why is my widget blank?" questions are answered on this page.
**Like plugging an appliance into a socket.** The widget is the appliance, the topic is the socket — once plugged in, the power (data) flows on its own.
***
Start by wiring one widget to one topic; the deeper reference — targets, coercion, JSON extraction — follows after.
## Bind a widget in 30 seconds
Pick a topic you can see updating in the [Data Viewer](/hub/mqtt/data-viewer), and point a component at it with one `BIND` line:
```lot wrap focus={10} theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
DEFINE PANEL SensorMonitor
WITH VISIBILITY PUBLIC
WITH STATE PUBLISHED
WITH TITLE "Sensor Monitor"
WITH LAYOUT "grid" COLUMNS 2
ADD COMPONENT "TempGauge" WITH TYPE "gauge"
SET RANGE FROM 0 TO 100
SET UNIT "°C"
BIND VALUE TO TOPIC "sensor/temperature"
```
Save, publish a number to `sensor/temperature`, and the needle moves. That one highlighted line is all a binding is: *which aspect of the widget* (`VALUE`) gets fed *from where* (`TOPIC "sensor/temperature"`).
## Where data comes from
Today, bindings read from **MQTT topics** — the widget updates on every message. Browse what's available in the [Data Viewer](/hub/mqtt/data-viewer):
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
BIND VALUE TO TOPIC "sensor/temperature"
```
## Extracting from JSON payloads
Real device payloads are rarely a bare number. When the topic carries a JSON object, add `JSON_PATH` on the component to pick a field:
```lot wrap focus={3} theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
ADD COMPONENT "EnergyDisplay" WITH TYPE "value"
BIND VALUE TO TOPIC "machines/pump1/data"
SET JSON_PATH "$.energy_wh"
SET UNIT "Wh"
```
Always inspect the payload in the [Data Viewer](/hub/mqtt/data-viewer) before binding — you'll see immediately whether you need `JSON_PATH`, `AS NUMBER`, or neither.
***
That covers most panels. The rest of the page is the reference: the full grammar, every bind target, and the transformation options that work today.
## Anatomy of a binding
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
BIND TO TOPIC ""
```
Three parts, all required:
* **Target** — which aspect of the component receives the data (`VALUE`, `STATE`, `DATA`, ...).
* **`TO TOPIC`** — both words are mandatory; the source is always an MQTT topic path.
* **Topic path** — the topic string in quotes.
## Bind targets
| Target | Used with | Description |
| ------------- | ------------------------------------------- | -------------------- |
| `VALUE` | gauge, value, slider, chart, realtime-chart | Numeric value |
| `STATE` | indicator, toggle, switch | Boolean / state |
| `CONTENT` | text, label, alert | Text content |
| `MAX` / `MIN` | gauge, value | Dynamic range bounds |
| `COLOR` | Any | Dynamic color |
| `TITLE` | card, panel | Dynamic title |
| `LABEL` | button, toggle, text | Dynamic label |
| `DATA` | chart, table, list | Dataset |
| `VISIBLE` | Any | Show / hide |
| `SRC` | image | Dynamic source URL |
## Type coercion
If a topic publishes text but the component expects a number or boolean, append a coercion keyword to the bind line:
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
BIND VALUE TO TOPIC "sensor/raw_value" AS NUMBER
BIND STATE TO TOPIC "status/text" AS BOOLEAN
```
Validated on a live panel: a text payload `"87.5"` coerced with `AS NUMBER` displays as `87.50` on a `value` component.
## Formatting readouts
Apply units and decimal places on the **component**, not on the `BIND` line:
```lot wrap focus={4-5} theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
ADD COMPONENT "FlowRate" WITH TYPE "value"
BIND VALUE TO TOPIC "flow/rate"
SET UNIT "L/min"
SET DECIMALS 1
```
`SET UNIT` and `SET DECIMALS` are documented per widget in [Display components](/hub/dashboards/components/display).
## Common mistakes
Check the binding line word by word — these are the usual silent breakers:
| Wrong (silently broken) | Correct |
| -------------------------- | ---------------------------------------------- |
| `BIND TOPIC "kpi/x"` | `BIND VALUE TO TOPIC "kpi/x"` — target missing |
| `BIND VALUE TOPIC "kpi/x"` | `BIND VALUE TO TOPIC "kpi/x"` — `TO` missing |
The topic carries JSON — add `SET JSON_PATH "$.field"` to the component, as shown in [Extracting from JSON payloads](#extracting-from-json-payloads).
## Next Steps
The other half of interactivity: reacting to clicks, changes, and thresholds.
Which target each widget accepts, with rendered examples.
# Defining a Panel
Source: https://docs.coreflux.org/latest/lot-language/panels/defining-panels
Reading and writing panel definitions: structure, layouts, positioning, and lifecycle.
Every dashboard is one `DEFINE PANEL` block, written in the Dashboard Manager or generated by the AI Assistant. The block names the panel, sets a few panel-wide attributes, and lists the components it contains — this page walks through each part.
**Like a recipe card for a screen.** The header says what the dish is and how the table is set (title, layout); each ingredient line below adds one widget and says where its data comes from.
***
Start with the smallest complete panel, then each section below adds one concept: reading the syntax, layouts, positioning, and lifecycle.
## Panel skeleton
A panel opens with `DEFINE PANEL`, four panel-level attributes, then one or more components:
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
DEFINE PANEL MyPanel
WITH VISIBILITY PUBLIC
WITH STATE PUBLISHED
WITH TITLE "My Panel"
WITH LAYOUT "grid" COLUMNS 3
ADD COMPONENT "MyComponent" WITH TYPE "gauge"
SET RANGE FROM 0 TO 100
BIND VALUE TO TOPIC "sensor/temperature"
```
* The panel **name** (`MyPanel`) identifies the panel in the Dashboard Manager.
* `WITH VISIBILITY` and `WITH STATE` declare the panel's audience and lifecycle — see [Lifecycle and visibility](#lifecycle-and-visibility).
* `WITH TITLE` is the heading people see; `WITH LAYOUT` arranges the components.
* Components are added with `ADD COMPONENT "" WITH TYPE ""`.
**Always include `WITH VISIBILITY PUBLIC` and `WITH STATE PUBLISHED`.** A panel saved without them may not appear in the Dashboard Manager list.
## Layouts
The layout is declared on the panel header and controls how the panel's top-level components are arranged:
| Layout | Syntax | Use it for |
| ---------- | ------------------------------------- | ---------------------------------------------- |
| Grid | `WITH LAYOUT "grid" COLUMNS 3 ROWS 2` | Most dashboards — explicit placement and spans |
| Vertical | `WITH LAYOUT "vertical"` | Simple stacked mobile/tablet views |
| Horizontal | `WITH LAYOUT "horizontal"` | Strips of KPIs on wide screens |
Each tab below arranges the same three station readouts with a different layout — only the `WITH LAYOUT` line changes:
Components flow into the declared columns, one cell each:
```lot wrap focus={5} theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
DEFINE PANEL StationGrid
WITH VISIBILITY PUBLIC
WITH STATE PUBLISHED
WITH TITLE "Station — Grid"
WITH LAYOUT "grid" COLUMNS 3
ADD COMPONENT "Temperature" WITH TYPE "value"
BIND VALUE TO TOPIC "station/temperature"
SET UNIT "°C"
ADD COMPONENT "Humidity" WITH TYPE "value"
BIND VALUE TO TOPIC "station/humidity"
SET UNIT "%"
ADD COMPONENT "Pressure" WITH TYPE "value"
BIND VALUE TO TOPIC "station/pressure"
SET UNIT "bar"
```
Components stack top to bottom, each taking the full width:
```lot wrap focus={5} theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
DEFINE PANEL StationVertical
WITH VISIBILITY PUBLIC
WITH STATE PUBLISHED
WITH TITLE "Station — Vertical"
WITH LAYOUT "vertical"
ADD COMPONENT "Temperature" WITH TYPE "value"
BIND VALUE TO TOPIC "station/temperature"
SET UNIT "°C"
ADD COMPONENT "Humidity" WITH TYPE "value"
BIND VALUE TO TOPIC "station/humidity"
SET UNIT "%"
ADD COMPONENT "Pressure" WITH TYPE "value"
BIND VALUE TO TOPIC "station/pressure"
SET UNIT "bar"
```
Components sit side by side in a single row:
```lot wrap focus={5} theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
DEFINE PANEL StationHorizontal
WITH VISIBILITY PUBLIC
WITH STATE PUBLISHED
WITH TITLE "Station — Horizontal"
WITH LAYOUT "horizontal"
ADD COMPONENT "Temperature" WITH TYPE "value"
BIND VALUE TO TOPIC "station/temperature"
SET UNIT "°C"
ADD COMPONENT "Humidity" WITH TYPE "value"
BIND VALUE TO TOPIC "station/humidity"
SET UNIT "%"
ADD COMPONENT "Pressure" WITH TYPE "value"
BIND VALUE TO TOPIC "station/pressure"
SET UNIT "bar"
```
### Mixing layouts with cards
The panel layout positions **top-level** components. To group several widgets into one grid slot, wrap them in a [`card`](/hub/dashboards/components/containers#card): the card occupies a cell (and can span columns like any component), while its children stack inside it. This is how you mix a coarse grid with finer-grained grouping:
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
DEFINE PANEL MixedLayout
WITH VISIBILITY PUBLIC
WITH STATE PUBLISHED
WITH TITLE "Mixed Layout"
WITH LAYOUT "grid" COLUMNS 2
ADD COMPONENT "ZoneCard" WITH TYPE "card"
SET TITLE "Zone 1"
ADD COMPONENT "ZoneTemp" WITH TYPE "value"
BIND VALUE TO TOPIC "zone1/temperature"
SET UNIT "°C"
ADD COMPONENT "ZoneStatus" WITH TYPE "indicator"
BIND STATE TO TOPIC "zone1/status"
SET ON_VALUE "running" WITH COLOR "green"
SET ON_VALUE "stopped" WITH COLOR "red"
ADD COMPONENT "SpeedGauge" WITH TYPE "gauge"
SET RANGE FROM 0 TO 100
SET UNIT "%"
BIND VALUE TO TOPIC "zone1/conveyor/speed"
```
The grid sees two components — the card and the gauge — while the card internally stacks a readout and an indicator. Use grid position for independent widgets; use a card when components belong to the same asset and should travel together.
## Positioning components on a grid
Use `AT COLUMN x ROW y` to place a component, and `SPAN n COLUMNS` to stretch it across the grid:
```lot wrap focus={7,8,12,13,17,18} theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
DEFINE PANEL GridExample
WITH VISIBILITY PUBLIC
WITH STATE PUBLISHED
WITH TITLE "Grid Example"
WITH LAYOUT "grid" COLUMNS 3 ROWS 2
ADD COMPONENT "FullWidthChart" WITH TYPE "realtime-chart"
AT COLUMN 1 ROW 1 SPAN 3 COLUMNS
SET LABEL "Throughput"
BIND VALUE TO TOPIC "factory/throughput"
ADD COMPONENT "Gauge1" WITH TYPE "gauge"
AT COLUMN 1 ROW 2
SET RANGE FROM 0 TO 100
BIND VALUE TO TOPIC "sensor/1"
ADD COMPONENT "Gauge2" WITH TYPE "gauge"
AT COLUMN 2 ROW 2
SET RANGE FROM 0 TO 100
BIND VALUE TO TOPIC "sensor/2"
```
## Navigation
A dashboard doesn't have to fit on one screen. Any component with a click handler can jump to another panel with `NAVIGATE TO PANEL ""`, and the target panel can return with `NAVIGATE BACK` — the classic pattern is an overview panel with a button per zone, each leading to a detail panel.
The overview panel links to the detail panel by name:
```lot wrap focus={11-14} theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
DEFINE PANEL NavOverview
WITH VISIBILITY PUBLIC
WITH STATE PUBLISHED
WITH TITLE "Zone Overview"
WITH LAYOUT "grid" COLUMNS 2
ADD COMPONENT "Zone1Temp" WITH TYPE "value"
BIND VALUE TO TOPIC "sensor/temperature"
SET UNIT "°C"
ADD COMPONENT "Zone1Details" WITH TYPE "button"
SET LABEL "Zone 1 details"
ON CLICK DO
NAVIGATE TO PANEL "NavDetail"
```
The detail panel offers the way back with `NAVIGATE BACK`:
```lot wrap focus={12-15} theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
DEFINE PANEL NavDetail
WITH VISIBILITY PUBLIC
WITH STATE PUBLISHED
WITH TITLE "Zone 1 — Detail"
WITH LAYOUT "grid" COLUMNS 2
ADD COMPONENT "DetailTemp" WITH TYPE "gauge"
SET RANGE FROM 0 TO 100
SET UNIT "°C"
BIND VALUE TO TOPIC "sensor/temperature"
ADD COMPONENT "BackButton" WITH TYPE "button"
SET LABEL "Back to overview"
ON CLICK DO
NAVIGATE BACK
```
Navigation is just another action — it works from buttons, cards, or anything else with an `ON CLICK` handler. The full action reference is in [Actions & Events](/latest/lot-language/panels/actions-events#navigate-between-panels).
## Lifecycle and visibility
A panel also carries lifecycle attributes:
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
DEFINE PANEL Zone1
WITH VISIBILITY PUBLIC
WITH STATE PUBLISHED
WITH TITLE "Zone 1"
```
| Attribute | Values | Meaning |
| ----------------- | ---------------------------------- | ------------------------------------------------------------------------------------------------------- |
| `WITH STATE` | `DRAFT` · `PUBLISHED` · `ARCHIVED` | **Draft** while building (hidden), **Published** for daily use, **Archived** to retire without deleting |
| `WITH VISIBILITY` | `PUBLIC` · `SHARED` · `PRIVATE` | Who is allowed to open the panel |
* New panels open in **Draft** — switch to `PUBLISHED` when operators should rely on the panel.
* `PUBLIC` panels are open to any connected client; `PRIVATE` restricts to the creator and admins.
* The same lifecycle is surfaced as badges in the Dashboard Manager — see [Dashboards in the HUB](/hub/dashboards/overview).
**Current HUB behaviour.** These attributes are honoured by the renderer but are not yet manageable from the HUB itself: a panel saved without explicit `WITH VISIBILITY` / `WITH STATE` values may not appear in the Dashboard Manager list at all, and the manager offers no way to view or change them outside the source text. Until that integration improves, always write both attributes explicitly in the definition.
## Common mistakes
Properties use `SET`, not `WITH`. `WITH UNIT "°C"`, `WITH RANGE 0, 100`, and `WITH DECIMALS 2` do nothing — write `SET UNIT "°C"`, `SET RANGE FROM 0 TO 100`, `SET DECIMALS 2`.
Component **types** are set on the header, not in the body. `WITH TYPE "line"` inside a chart body is silently ignored — use `SET CHART_TYPE "line"` instead.
Check that both `WITH VISIBILITY PUBLIC` and `WITH STATE PUBLISHED` are present — panels saved without them may not appear in the panel list.
## Next Steps
The widgets you place inside a panel, property by property.
Wire each component to a topic, model, or query.
# AI Routes
Source: https://docs.coreflux.org/latest/lot-language/routes/ai-routes
Deploy AI agents as LoT Routes — then reach them over MQTT interaction topics or CALL AGENT from Actions, solo or as a multi-agent team
## AI Agents in LoT
Your broker already moves sensor data, drives PLCs, and stores history. **AI agents** add something new: specialists that *reason* over that world — in plain language, on the same machine, with the same MQTT and Routes you already defined.
In Coreflux, each agent is a **LoT Route** with `TYPE AGENT`. One block gives you the model, the persona, the tools, and the guardrails. Spin up an operator that answers shift questions. Add a classifier that never sees a chat window. Run five agents on one broker, each with its own brain and boundaries — no sidecar service, no copy-paste into a separate chatbot.
**Defining the Route is only half the story.** The other half is *who gets to knock on the door*: operators on MQTT topics the Route owns, or LoT Actions that dial the agent when something happens on the plant floor. Same agent definition — two front doors. Use one, use both, mix them across a team of Routes.
**Like hiring for the control room, then choosing desk phone vs. internal line.** `DEFINE ROUTE … WITH TYPE AGENT` is the hire. `INTERACTION_TOPIC` is the desk line for people and apps. `CALL AGENT` is what your Actions use when a sensor fires or a timer ticks — no public chat required.
### When to use AI Routes
* **One agent, one job** — chat helper, classifier, or scheduled production/maintenance/solar report; each gets its own Route
* **Multi-agent teams** — split models, prompts, and permissions by role ([Multi-Agent Deployments](#multi-agent-deployments))
* **Ask the plant, don't only dashboard it** — "how many sensors are reporting?" before you wire another panel
* **MQTT-facing chat** — `INTERACTION_TOPIC` on the Route, plus MQTT ask/reply topics (your Action or custom app)
* **Hands-off automation** — `CALL AGENT` when a topic fires, a timer ticks, or a pipeline needs a verdict
First time? Build both patterns in ten minutes: [Creating Agents in Coreflux](/latest/quick-start/creating-agents). Then come back here for providers, tools, and production detail.
***
## Complete Example (Copy & Deploy)
Pick one tab to ship something fast, or deploy **both** on the same broker. Each example is self-contained — you do not need to read the rest of this page first.
**Prerequisites (both tabs):** Broker running and an [OpenAI API key](https://platform.openai.com/api-keys) stored on the broker as a secret (once):
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
KEEP SECRET "OPENAI_KEY" WITH "sk-..."
```
The examples below reference it with `GET SECRET "OPENAI_KEY"`. Prefer local inference? Swap to `PROVIDER "ollama"` — see [Picking a Provider](#picking-a-provider).
**Pattern:** [Route interaction topics](#pattern-1-route-interaction-topics) — the AGENT Route owns the model and `INTERACTION_TOPIC`; a thin Action exposes **ask/reply MQTT topics** you can hit from MQTT Explorer or the HUB [Data Viewer](/hub/mqtt/data-viewer).
The [HUB AI Assistant](/hub/ai-assistant) (Coreflux icon in the dock) is a **separate** built-in agent installed by the AI Setup wizard — it is not wired to `PlantAssistant` or your custom `INTERACTION_TOPIC`.
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
DEFINE ROUTE PlantAssistant WITH TYPE AGENT
ADD AGENT_CONFIG
WITH PROVIDER "openai"
WITH MODEL "gpt-5.5"
WITH API_KEY GET SECRET "OPENAI_KEY"
WITH BROKER_TOOLS "true"
WITH INTERACTION_MODE "interactive"
WITH INTERACTION_TOPIC "plant/assistant/interact"
WITH SYSTEM_PROMPT "You are the plant assistant. Use broker tools to answer from live MQTT data. Be concise."
DEFINE ACTION AskPlantAssistant
ON TOPIC "plant/assistant/ask" DO
CALL AGENT "PlantAssistant.execute"
WITH (task = PAYLOAD)
RETURN AS {reply}
PUBLISH TOPIC "plant/assistant/reply" WITH {reply}
```
Paste both blocks into **Coreflux HUB** (LoT Editor → Routes, then Actions), a LoT Notebook, or your usual deploy path. Confirm **PlantAssistant** (AGENT) and **AskPlantAssistant** (Action) are healthy.
In **MQTT Explorer** or HUB **Data Viewer** (MQTT app), publish your question to `plant/assistant/ask` and subscribe to `plant/assistant/reply`:
| Direction | Topic | Example payload |
| --------- | ----------------------- | ----------------------------------------- |
| Publish | `plant/assistant/ask` | `Which MQTT topics are active right now?` |
| Subscribe | `plant/assistant/reply` | Agent answer (plain text) |
Try another live-data prompt, for example: `What is the latest payload on sensors/zone1/temperature?`
If the agent needs clarification, it publishes to `plant/assistant/interact/ask`. Reply on `plant/assistant/interact/reply/{request_id}` with the `{request_id}` from that message — separate from the `plant/assistant/ask` topic your Action listens on.
You can **talk with your data** over MQTT — the agent uses broker tools, not a static chat transcript.
**Pattern:** [`CALL AGENT` in LoT Actions](#pattern-2-call-agent-in-lot-actions) — LoT runs the agent on a schedule; no chat UI.
Example: **hourly production report** from live `production/` MQTT data — throughput, off-target lines, and follow-ups for supervisors.
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
DEFINE ROUTE ProductionReporter WITH TYPE AGENT
ADD AGENT_CONFIG
WITH PROVIDER "openai"
WITH MODEL "gpt-5.4-mini"
WITH API_KEY GET SECRET "OPENAI_KEY"
WITH BROKER_TOOLS "true"
WITH INTERACTION_MODE "autonomous"
WITH SYSTEM_PROMPT "You write production shift reports. Use broker tools on production-related topics. Bullet points: throughput, downtime signals, quality risks."
DEFINE ACTION HourlyProductionReport
ON EVERY 1 HOUR DO
CALL AGENT "ProductionReporter.execute"
WITH (task = "Hourly production report: summarise output and efficiency from production/ topics, flag lines or batches that look off-target, note anything that needs follow-up before the next shift.", use_history = "false")
RETURN AS {report}
PUBLISH TOPIC "reports/production/hourly" WITH {report}
```
`ProductionReporter` omits `INTERACTION_TOPIC` — only the Action triggers it. Maintenance or solar: reuse the pattern with `maintenance/` or `solar/` topics. More use cases: [Creating Agents in Coreflux](/latest/quick-start/creating-agents).
Add **ProductionReporter** under Routes, then **HourlyProductionReport** under Actions.
Subscribe to `reports/production/hourly` in MQTT Explorer or your dashboard.
Publish test data under `production/` if needed. Temporarily use `ON EVERY 30 SECONDS`, verify, then restore `ON EVERY 1 HOUR`.
**Automatic production reporting** — LoT triggers the agent; supervisors read the output topic.
Run **both tabs** together when you want shift chat (`PlantAssistant`) and an hourly production report (`ProductionReporter`) on the same broker. Everything below covers providers, tools, multi-agent teams, and production settings in depth.
***
## Two Ways to Interact
You write the agent once in LoT. **How the world talks to it** splits two ways — pick the door that fits, or run both on the same broker.
| Pattern | Where it lives | Best for |
| ------------------------------------------------------------------- | ------------------------------------------------------------------------------ | -------------------------------------------------------------------------------------- |
| **[Route interaction topics](#pattern-1-route-interaction-topics)** | `INTERACTION_TOPIC` on the AGENT Route (+ MQTT topics your Action or app uses) | People, custom chat UIs, MQTT Explorer, HUB Data Viewer |
| **[`CALL AGENT` in Actions](#pattern-2-call-agent-in-lot-actions)** | LoT `CALL AGENT "RouteName.execute"` inside an Action | Sensor pipelines, timers, branching logic, structured jobs with no public chat surface |
```mermaid theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
flowchart TB
subgraph define [Define once in LoT]
Route[AGENT Route model, tools, prompt]
end
subgraph mqtt [Pattern 1 — Route topics]
Client[App, HUB, or MQTT client]
Prefix[INTERACTION_TOPIC prefix]
Client <-->|ask / reply| Prefix
Prefix --> Route
end
subgraph lot [Pattern 2 — LoT Action]
Trigger[ON TOPIC or ON EVERY]
Action[Action body]
Trigger --> Action
Action -->|CALL AGENT| Route
end
```
You can mix them: an **Operator** Route with `INTERACTION_TOPIC` for shift handover chat, plus a **Classifier** Route that only ever runs via `CALL AGENT` from a sensor Action.
***
## Minimal Example
The shortest possible AI Route points at a local Ollama model and nothing else. It has no tools yet — it simply answers questions using the LLM:
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
DEFINE ROUTE Assistant WITH TYPE AGENT
ADD AGENT_CONFIG
WITH MODEL "llama3.2"
```
This default assumes Ollama is running locally at `http://localhost:11434`. Pull the model first with `ollama pull llama3.2` if you haven't already. If the broker runs in Docker, use the Ollama container hostname in Compose (e.g. `http://ollama:11434`) or `http://host.docker.internal:11434` when Ollama is on the host.
***
## Picking a Provider
AI Routes support four providers out of the box. Pick the one that matches where your model lives — local or cloud. Cloud providers need an API key, which you should store as a secret rather than pasting into the Route.
Runs entirely on your machine — no API key, no cloud dependency. Good for getting started and for privacy-sensitive workloads. The broker default is `llama3.2`; for stronger local quality in 2026, many teams use `qwen3:8b` or `qwen3:4b` on the same Ollama endpoint.
**Prerequisite:** [Install Ollama](https://ollama.com/download) and have it running before you deploy this Route. Pull your model in Ollama first (e.g. `ollama pull qwen3:8b`) — the `MODEL` value must match a model already available in `ollama list`. Coreflux connects to your Ollama endpoint; it does not install Ollama or download models for you. Lightweight models may work for a quick test, but often are not capable enough for reliable agent work or good answer quality.
If the broker runs in **Docker**, `localhost` inside the container is not the host—set `BASE_URL` to the reachable Ollama endpoint. With **Docker Compose**, use the Ollama service name (e.g. `http://ollama:11434`). If Ollama runs on the host, use `http://host.docker.internal:11434` (Docker Desktop) or the host's LAN IP.
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
DEFINE ROUTE Assistant WITH TYPE AGENT
ADD AGENT_CONFIG
WITH PROVIDER "ollama"
WITH BASE_URL "http://localhost:11434"
WITH MODEL "qwen3:8b"
```
Uses OpenAI's GPT-5 generation (e.g. `gpt-5.4-mini` for cost-sensitive work, `gpt-5.5` for frontier reasoning). Store the API key as a secret so it never appears in your Route definition.
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
DEFINE ROUTE Assistant WITH TYPE AGENT
ADD AGENT_CONFIG
WITH PROVIDER "openai"
WITH MODEL "gpt-5.4-mini"
WITH API_KEY GET SECRET "OPENAI_KEY"
```
Uses Claude 4.6/4.7 models. Same secret-based pattern — `claude-sonnet-4-6` for most production agents, `claude-opus-4-7` when you need maximum reasoning depth.
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
DEFINE ROUTE Assistant WITH TYPE AGENT
ADD AGENT_CONFIG
WITH PROVIDER "anthropic"
WITH MODEL "claude-sonnet-4-6"
WITH API_KEY GET SECRET "ANTHROPIC_KEY"
```
Uses Mistral's hosted models — `mistral-large-latest` (Large 3) for quality, `mistral-small-latest` when you want lower cost and latency. Works the same way: set the provider and point to a stored API key.
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
DEFINE ROUTE Assistant WITH TYPE AGENT
ADD AGENT_CONFIG
WITH PROVIDER "mistral"
WITH MODEL "mistral-large-latest"
WITH API_KEY GET SECRET "MISTRAL_KEY"
```
Model IDs change often. Use the exact slug from your provider's catalog (OpenAI model guide, Anthropic model overview, Mistral API list, `ollama list`). Set secrets with `KEEP SECRET "OPENAI_KEY" WITH "sk-..."` — they never appear in logs or stored Route definitions.
***
## Core Configuration
These are the parameters most users will touch. The `AGENT` route has more advanced knobs (history pruning, tracing, token limits, confirmation flows), but you don't need them on day one.
| Parameter | Type | Default | What it does |
| ------------------- | ------- | ------------ | -------------------------------------------------------------------- |
| `PROVIDER` | string | `ollama` | Which LLM backend to use: `ollama`, `openai`, `anthropic`, `mistral` |
| `MODEL` | string | `llama3.2` | Model name for the chosen provider |
| `API_KEY` | string | — | API key for cloud providers (use `GET SECRET`) |
| `TEMPERATURE` | number | `0.7` | Creativity: `0.0` is deterministic, `1.0+` is more creative |
| `SYSTEM_PROMPT` | string | *(built-in)* | Sets the agent's persona and rules |
| `BROKER_TOOLS` | boolean | `false` | Give the agent built-in tools to read/write MQTT and Routes |
| `MCP_ROUTES` | string | — | Comma-separated names of MCP Routes the agent can call |
| `INTERACTION_MODE` | string | `autonomous` | `autonomous` runs to completion, `interactive` can ask you questions |
| `INTERACTION_TOPIC` | string | *(auto)* | Base MQTT topic for interaction messages |
| `AGENT_MODE` | string | `agent` | `agent` (full control) or `insight` (read-only exploration) |
More options exist for production deployments — conversation history pruning, execution tracing, tool confirmation, max iteration limits. Start with the basics above, then add them as you need them.
***
## What Tools Can the Agent Use?
A fresh agent only knows how to chat. To make it actually *do* things, you give it tools. There are three sources, and you can mix all three.
### Broker Tools
Setting `BROKER_TOOLS true` unlocks built-in tools so the agent can interact with your broker directly — no extra configuration required.
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
DEFINE ROUTE Assistant WITH TYPE AGENT
ADD AGENT_CONFIG
WITH MODEL "llama3.2"
WITH BROKER_TOOLS "true"
```
With broker tools enabled, the agent can:
* **List MQTT topics** and peek at their latest payloads
* **Read a specific topic** on demand
* **Publish messages** to any topic
* **List your Routes** to see what's connected
This is the fastest way to get an agent that can answer questions like *"what's the current temperature on factory/sensor/01?"* or *"which Routes are running right now?"*.
### MCP Tools
MCP (Model Context Protocol) servers expose tools like file systems, Slack, Google Drive, or custom integrations. Any [MCP Route](./mcp-routes) you've defined can be wired into an AI Route with `MCP_ROUTES`:
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
DEFINE ROUTE Assistant WITH TYPE AGENT
ADD AGENT_CONFIG
WITH MODEL "llama3.2"
WITH MCP_ROUTES "FileMcp, SlackMcp"
```
The agent discovers every tool exposed by those MCP servers automatically. If `SlackMcp` offers a `send-message` tool, the agent can call it when appropriate.
### Your Other Routes
The agent also sees every other Route you've already defined. If you have a PostgreSQL Route with an `InsertReading` event, the agent automatically gets a tool named `SensorDB.trigger_InsertReading`. Same for Modbus, REST, MongoDB, and so on — you don't need to re-declare anything.
Define your data Routes first, then add the AI Route. The agent inherits all of them as tools the moment it starts.
***
## Insight vs. Agent Mode
Not every AI Route should be able to publish messages or trigger equipment. Use `AGENT_MODE` to control how much power the agent has:
| Mode | What it can do | Good for |
| ----------------- | -------------------------------------------------------------- | ------------------------------------------------- |
| `insight` | Read-only: list topics, read payloads, explore Routes | Exploring a broker, answering "what's connected?" |
| `agent` (default) | Full: read **and** write, trigger Routes, modify configuration | Automation, chat assistants, hands-on operators |
**Start in `insight` mode while you experiment.** The agent can still explain your broker and read data — but it can't accidentally publish to production topics while you're tuning the prompt.
***
## Pattern 1: Route Interaction Topics
Set `INTERACTION_TOPIC` on the AGENT Route and the broker wires **interaction traffic** under that prefix (for example follow-up questions on `{prefix}/ask` and replies on `{prefix}/reply/{request_id}` in interactive mode).
To **start** a conversation from MQTT Explorer or the HUB [Data Viewer](/hub/mqtt/data-viewer), add a thin Action that listens on your ask topic, runs `CALL AGENT "YourRoute.execute"`, and publishes the answer — as in the [Complete Example](#complete-example-copy--deploy) chat tab. Custom apps can implement the same prefix without that Action if they speak the broker's interaction protocol.
The [HUB AI Assistant](/hub/ai-assistant) is not your custom AGENT Route — it is the dock chat installed via **AI Setup** (`BrokerAgent`). Use it to build dashboards and LoT; use `INTERACTION_TOPIC` + MQTT (or your UI) for agents you define in LoT.
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
DEFINE ROUTE Operator WITH TYPE AGENT
ADD AGENT_CONFIG
WITH MODEL "llama3.2"
WITH BROKER_TOOLS "true"
WITH INTERACTION_MODE "interactive"
WITH INTERACTION_TOPIC "plant/operator"
WITH INTERACTION_TIMEOUT "120"
WITH SYSTEM_PROMPT "You are the plant assistant. Help operators using broker tools."
```
| Setting | Role |
| --------------------- | ------------------------------------------------------------------------------------------------ |
| `INTERACTION_TOPIC` | Base MQTT prefix for this Route's chat (here: `plant/operator`) |
| `INTERACTION_MODE` | `interactive` lets the agent pause and ask follow-ups; `autonomous` runs each task to completion |
| `INTERACTION_TIMEOUT` | Seconds to wait for a human reply in interactive mode before continuing |
When the agent needs clarification in **interactive** mode, traffic uses subtopics under your prefix:
| Direction | Topic | What happens |
| -------------- | ---------------------------------------- | ---------------------------------------------- |
| Agent → client | `{INTERACTION_TOPIC}/ask` | Agent publishes a follow-up question |
| Client → agent | `{INTERACTION_TOPIC}/reply/{request_id}` | Your UI or MQTT client answers; work continues |
Example with `INTERACTION_TOPIC "plant/operator"`: subscribe to `plant/operator/ask`, publish replies to `plant/operator/reply/{request_id}`.
Use **Route topics** when humans or external apps drive the agent. Use **`CALL AGENT`** when LoT logic drives it — you do not need both on every Route.
***
## Pattern 2: `CALL AGENT` in LoT Actions
Keep the AGENT Route as the brain, but invoke it **from LoT** with `CALL AGENT "RouteName.execute"`. The Action's trigger (`ON TOPIC`, `ON EVERY`, and so on) decides *when* the agent runs; the `task` argument carries *what* to do. The result lands in a variable you can branch on or publish anywhere.
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
DEFINE ACTION ClassifyReading
ON TOPIC "sensors/+/reading" DO
CALL AGENT "Classifier.execute"
WITH (task = PAYLOAD, use_history = "false")
RETURN AS {label}
PUBLISH TOPIC "sensors/classified" WITH {label}
```
| Argument | Typical use |
| ------------- | ----------------------------------------------------------------------------------------- |
| `task` | Instruction or payload text (required) |
| `session_id` | Same id on the next call continues one thread (per device, ticket, or line) |
| `use_history` | `"false"` for one-off labels; `"true"` when follow-up calls should remember earlier steps |
Specialist and watchdog agents often **omit** `INTERACTION_TOPIC` so nothing listens on a public chat prefix — they only run when an Action calls them.
You *can* bridge `CALL AGENT` to custom MQTT topics if you want a bespoke ask/reply pair, but those topics belong to **your Action**, not the Route's built-in interaction surface:
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
DEFINE ACTION AskAssistant
ON TOPIC "assistant/ask" DO
CALL AGENT "Assistant.execute"
WITH (task = PAYLOAD)
RETURN AS {reply}
PUBLISH TOPIC "assistant/reply" WITH {reply}
```
Here `assistant/ask` and `assistant/reply` are Action-defined topics; the agent still runs via `CALL AGENT`, not via `INTERACTION_TOPIC`.
Running **several AI Routes**? Pass `session_id` and `use_history` on each `CALL AGENT` so parallel automation jobs do not share the wrong memory. See [Multi-Agent Deployments](#multi-agent-deployments).
***
## How It All Fits Together
The diagram below shows both patterns on one broker: MQTT clients use the Route's `INTERACTION_TOPIC`; LoT Actions use `CALL AGENT`. Optional tool sources are the same in either case.
```mermaid theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
flowchart TB
subgraph callers [Callers]
MQTT[MQTT client or HUB]
LotAction[LoT Action]
end
subgraph route [AGENT Route Assistant]
Prefix[INTERACTION_TOPIC plant/operator]
AgentCore[LLM + tools]
Prefix --> AgentCore
LotAction -->|CALL AGENT| AgentCore
end
MQTT <-->|chat prefix| Prefix
BrokerTools[Broker tools]
MCP[MCP Routes]
Others[Other Routes]
AgentCore --> BrokerTools
AgentCore --> MCP
AgentCore --> Others
```
Every tool box is optional. A minimal agent with only `MODEL` still answers — it just cannot read your broker until you enable tools.
***
## Multi-Agent Deployments
You are not limited to a single AI Route. The same Coreflux broker can run **several agents at once** — each one a normal Route with its own name, model, system prompt, and permissions. That matters whether you are learning the product or sizing it for production: you can give **operators** a conversational helper, give **automation** a fast classifier that never sees a chat window, and keep **guests** on a read-only view — without buying extra middleware or spinning up separate services for each role.
From a buyer's perspective, multi-agent is how you **match cost to workload** (local models for high-volume tasks, cloud models where quality matters most) and **match risk to audience** (full control for trusted staff, insight-only for everyone else). From a newcomer's perspective, it is simply "define another `DEFINE ROUTE … WITH TYPE AGENT` block with a different name" — the broker runs them side by side.
**Think of it like staffing a control room with specialists instead of one generalist** — a friendly voice for the team at the desk, a quiet expert that only labels sensor readings, and a scheduled check-in that writes a health summary. They share the same broker, not the same job description.
### Why give the broker more than one agent?
One catch-all assistant is hard to tune and expensive to run everywhere. Splitting the work pays off because:
* **Personalities differ.** A chat assistant should be warm and conversational; a data classifier should be terse and deterministic.
* **Access levels differ.** The Route that can trigger equipment should not be the same one you hand to a visitor.
* **Models and costs differ.** A premium cloud model is a poor fit for every sensor reading — a local model can handle volume for free while heavier reasoning stays on the model you pay for.
* **Memory should stay isolated.** The person at shift handover should not inherit someone else's half-finished conversation by accident.
### How people and software reach each agent
Map each Route to [Pattern 1](#pattern-1-route-interaction-topics) or [Pattern 2](#pattern-2-call-agent-in-lot-actions) — or use both patterns on different Routes in the same project:
| Entry | Pattern | Typical setup |
| ------------------------------------ | --------------------- | ------------------------------------------------------------------------------------------- |
| Operators, chat apps, MQTT clients | **Route topics** | `INTERACTION_TOPIC` + ask/reply Action or custom UI; often `INTERACTION_MODE "interactive"` |
| Sensor pipelines, timers, batch jobs | **`CALL AGENT`** | `ON TOPIC` / `ON EVERY` Action, no `INTERACTION_TOPIC` on the Route |
| Background-only specialists | **`CALL AGENT` only** | `autonomous` mode, omit `INTERACTION_TOPIC` — no public chat prefix |
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
CALL AGENT "Classifier.execute"
WITH (task = {job}, session_id = {deviceId}, use_history = "false")
RETURN AS {result}
```
### Conversations and memory
Each **named AI Route** keeps its own conversation state. Two different Routes never share the same chat history — they are separate assistants. **Within** one Route, a **session id** (from your MQTT topic path when using interactive chat, or from `session_id` on `CALL AGENT`) keeps parallel conversations apart: two operators, two browser tabs, or two machines can talk to the same Route without their threads colliding. If you omit `session_id`, the broker still runs the task using its default session behaviour for that call — pass an explicit `session_id` when you need a reliable multi-step thread (for example one id per device or per ticket).
### How multi-agent traffic can look
```mermaid theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
flowchart LR
Human[Operator or app]
ChatTopic[MQTT chat prefix]
OpRoute[Operator AI Route]
SensorEvent[Sensor or event]
LotAction[LoT Action]
WorkerRoute[Specialist AI Route]
ResultTopic[Result topic]
Human -->|question| ChatTopic
ChatTopic --> OpRoute
OpRoute -->|answer| Human
SensorEvent --> LotAction
LotAction -->|CALL AGENT| WorkerRoute
WorkerRoute --> ResultTopic
```
### Four agent archetypes
Most teams mix a few of these patterns — use one on a small setup, combine several when roles split.
**How to read the tabs:** **Operator** and **Reader** are usually **MQTT-facing** (people or HUB). **Specialist** is typically **LoT-only** (no chat topic). **Watchdog** is triggered on a **schedule** from LoT but does not need its own chat line.
The staff-facing assistant. Lives on a chat topic, has broker tools, and runs in interactive mode so it can ask follow-up questions. This is the agent your team talks to.
**Entry:** MQTT under your `INTERACTION_TOPIC` prefix (via a thin ask/reply Action or a custom UI).
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
DEFINE ROUTE Operator WITH TYPE AGENT
ADD AGENT_CONFIG
WITH PROVIDER "openai"
WITH MODEL "gpt-5.5"
WITH API_KEY GET SECRET "OPENAI_KEY"
WITH BROKER_TOOLS "true"
WITH INTERACTION_MODE "interactive"
WITH INTERACTION_TOPIC "plant/operator"
WITH SYSTEM_PROMPT "You are the plant assistant. Help operators understand what is happening and take action when asked."
```
**Best for:** chat UIs, control-room copilots, customer-facing assistants.
A silent workhorse with no chat surface — no `INTERACTION_TOPIC`, no MQTT entry point. The only way in is a `CALL AGENT` from another LoT Action. Perfect for structured, repeatable tasks like classification or extraction.
**Entry:** LoT only. Keep `INTERACTION_MODE` **`autonomous`** (the default) and skip `INTERACTION_TOPIC` so the Route does not listen for public chat traffic.
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
DEFINE ROUTE Classifier WITH TYPE AGENT
ADD AGENT_CONFIG
WITH PROVIDER "ollama"
WITH MODEL "qwen3:8b"
WITH INTERACTION_MODE "autonomous"
WITH TEMPERATURE "0.0"
WITH SYSTEM_PROMPT "Classify sensor readings as NORMAL, WARNING, or CRITICAL. Reply with only the label."
```
Then drive it from any Action that needs a label:
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
DEFINE ACTION ClassifySensor
ON TOPIC "sensors/+/reading" DO
SET "device" WITH TOPIC POSITION 2
SET "reading" WITH (PAYLOAD AS STRING)
CALL AGENT "Classifier.execute"
WITH (task = "Reading from " + {device} + ": " + {reading})
RETURN AS {label}
PUBLISH TOPIC "sensors/" + {device} + "/class" WITH {label}
```
**Best for:** classification, tagging, extraction — anything called from automation, not humans.
A scheduled sentinel. It wakes up on a timer, inspects broker state via broker tools, and publishes a summary or alert. No human triggers it.
**Entry:** A timed LoT Action that calls `CALL AGENT` — no dedicated chat topic required.
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
DEFINE ROUTE Watchdog WITH TYPE AGENT
ADD AGENT_CONFIG
WITH PROVIDER "ollama"
WITH MODEL "qwen3:4b"
WITH INTERACTION_MODE "autonomous"
WITH BROKER_TOOLS "true"
WITH SYSTEM_PROMPT "You are a health monitor. Check all routes for errors and publish a concise alert summary if any are unhealthy."
```
Wire it to a timed Action so it runs unattended:
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
DEFINE ACTION HourlyHealthCheck
ON EVERY 1 HOUR DO
CALL AGENT "Watchdog.execute"
WITH (task = "Check all routes for errors and summarise the broker health.")
RETURN AS {report}
PUBLISH TOPIC "plant/health/hourly" WITH {report}
```
**Best for:** health checks, scheduled reports, overnight summaries.
The agent you can hand to anyone. It runs in `insight` mode, so it can read every topic and Route but cannot write anything — even if asked nicely. Safe for visitors, dashboards, and read-only audiences.
**Entry:** MQTT read-only chat on your `INTERACTION_TOPIC` — safe for guests because `AGENT_MODE` is `insight`.
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
DEFINE ROUTE Reader WITH TYPE AGENT
ADD AGENT_CONFIG
WITH PROVIDER "openai"
WITH MODEL "gpt-5.4-mini"
WITH API_KEY GET SECRET "OPENAI_KEY"
WITH BROKER_TOOLS "true"
WITH AGENT_MODE "insight"
WITH INTERACTION_MODE "interactive"
WITH INTERACTION_TOPIC "plant/reader"
WITH SYSTEM_PROMPT "You are a read-only plant observer. Answer questions about current state. You cannot change anything."
```
**Best for:** management dashboards, visitor demos, on-call engineers who need to look but not touch.
### Mixing providers across agents
`PROVIDER` and `MODEL` are set per Route, so every agent in the team can run on a different brain. Swap any of them by redeploying a single Route definition — no broker restart, no infrastructure change.
| Use case | Suggested model | Why |
| ---------------------------- | ------------------------------------------------- | ------------------------------------------------------------- |
| High-stakes operator chat | OpenAI `gpt-5.5` or Anthropic `claude-opus-4-7` | Frontier reasoning, handles ambiguity and multi-step tool use |
| Balanced production chat | OpenAI `gpt-5.4` or Anthropic `claude-sonnet-4-6` | Strong quality at lower cost than top-tier models |
| Low-latency classifiers | Ollama `qwen3:8b` (local) | Fast, private, good tool-following for structured labels |
| Scheduled operations reports | OpenAI `gpt-5.4-mini` | Production, maintenance, or solar digests on a timer |
| Management read portal | OpenAI `gpt-5.4-mini` | Cost-effective for low-volume read queries |
### Smart habits when you run several agents
* **Name Routes by role** — `Line3Classifier` or `VisitorGuide`, not `Agent2`, so your team and your LoT Actions stay readable.
* **Give each worker only the tools it needs** — A classifier rarely needs broker-wide publish access or MCP; fewer tools mean faster runs and fewer surprises.
* **Remember that cost adds up** — Each Route can call a cloud model on its own schedule. More agents and more `CALL AGENT` traffic multiply tokens and rate limits.
* **Match power to trust** — Use [Insight vs. Agent Mode](#insight-vs-agent-mode) for read-only audiences; reserve full `agent` mode for staff Routes that must act.
* **Add confirmation where mistakes hurt** — `TOOL_EXECUTION_MODE "confirm"` on any Route that should pause before a destructive tool runs.
### Per-agent safety rails
Safety settings apply **per Route**, so a strict assistant and a powerful one can live on the same broker. Pair the ideas in [Insight vs. Agent Mode](#insight-vs-agent-mode) with the habits above: lock read-only audiences to `insight`, keep trusted operators in full `agent` mode, and use `TOOL_EXECUTION_MODE "confirm"` only where you need a human gate before risky tools. Trace topics, iteration limits, and history pruning are also per agent — turn tracing on for the Route you are debugging so the rest of the team stays quiet.
***
## Best Practices
Never paste API keys directly into a Route definition. Use `KEEP SECRET "MY_KEY" WITH "sk-..."` once, then reference it with `WITH API_KEY GET SECRET "MY_KEY"`. Secrets are never logged or exposed in stored definitions.
When you're still shaping the system prompt or picking a model, run the agent with `WITH AGENT_MODE "insight"`. It can still read topics and explore Routes, but it can't publish or trigger anything — so a bad prompt won't affect production data.
The default prompt is generic. Tell the agent *who it is* and *what it should refuse to do*:
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
WITH SYSTEM_PROMPT "You are a factory assistant. Answer questions about the production line using broker tools. Do not publish to any topic starting with 'control/' without explicit confirmation."
```
`MAX_ITERATIONS` caps how many tool calls the agent can make per task. The default (`10`) is fine for most questions. Lift it only if you have complex multi-step workflows — runaway loops are both slow and expensive on cloud providers.
***
## Troubleshooting
* Make sure Ollama is running: `ollama list` should respond without errors
* Confirm the model is pulled: `ollama pull llama3.2`
* If Ollama is on a different machine, set `WITH BASE_URL "http://host:11434"` explicitly
* If the broker runs in **Docker**, `localhost` points at the container—not the host. With **Docker Compose**, use the Ollama service (e.g. `http://ollama:11434`). If Ollama is on the host, try `http://host.docker.internal:11434` (Docker Desktop) or the host LAN IP
* Check firewall rules between the broker and the Ollama host
* Confirm `BROKER_TOOLS "true"` is set (or that `MCP_ROUTES` lists valid Routes)
* Smaller local models sometimes ignore tools — try a more capable pull (e.g. `qwen3:8b` over `llama3.2`, or switch to a cloud model such as `gpt-5.4-mini` or `claude-sonnet-4-6`)
* Sharpen the `SYSTEM_PROMPT` so the agent knows it *should* use tools for data questions
* Verify the secret actually exists: publishing `LIST SECRETS` via the command console should show its name
* Check you're using the right `PROVIDER` — Anthropic keys don't work for OpenAI and vice versa
* For Anthropic, make sure your key has access to the specific model you set in `MODEL`
* Raise `MAX_TOKENS` — long answers need more room (default is `4096`)
* Check the model actually supports the request (some smaller models struggle with long context)
* If you chained several tool calls, the agent may have hit `MAX_ITERATIONS` — increase it and retry
***
## Next Steps
Hands-on tutorial — chat over MQTT, automatic reports, broker tools, and multi-agent layouts in minutes.
Plug external tools — Slack, file systems, Google Drive, custom servers — into your agent.
# CrateDB Route
Source: https://docs.coreflux.org/latest/lot-language/routes/data-storage/cratedb
Store MQTT data in CrateDB for distributed time-series analytics and SQL queries
## CrateDB Overview
The `CRATEDB` route stores MQTT messages in CrateDB, a distributed SQL database optimized for time-series and machine data. It combines the familiarity of SQL with the scalability needed for IoT workloads.
CrateDB excels at high-volume time-series data with fast ingestion and SQL query capabilities. Use it when you need real-time analytics on sensor data at scale.
## Basic Syntax
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
DEFINE ROUTE SensorDB WITH TYPE CRATEDB
ADD CRATEDB_CONFIG
WITH HOST "cratedb.example.com"
WITH PORT '5432'
WITH DATABASE "iot"
WITH USERNAME "crate"
WITH PASSWORD "secure_password"
ADD EVENT StoreSensorReading
WITH SOURCE_TOPIC "sensors/+/reading"
WITH QUERY "INSERT INTO sensor_data (ts, sensor_id, value) VALUES (?, ?, ?)"
```
***
## Connection Configuration
### CRATEDB\_CONFIG Parameters
CrateDB server hostname or IP address.
Database username.
Database password.
CrateDB PostgreSQL protocol port. Default: 5432.
Target database/schema name.
**Never hardcode credentials in production.** Use environment variables and encrypted secrets to keep sensitive values out of your route definitions:
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
WITH HOST GET ENV "DB_HOST"
WITH USERNAME GET ENV "DB_USER"
WITH PASSWORD GET SECRET "DB_PASSWORD"
```
See [Environment Variables & Secrets](/latest/mqtt-broker/secrets-and-env) for setup and usage.
***
## Writing Data
Store sensor readings with timestamps:
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
DEFINE ROUTE TimeSeriesDB WITH TYPE CRATEDB
ADD CRATEDB_CONFIG
WITH HOST "cratedb.example.com"
WITH PORT '5432'
WITH DATABASE "iot"
WITH USERNAME "crate"
WITH PASSWORD "secure_password"
ADD EVENT StoreTimeSeries
WITH SOURCE_TOPIC "sensors/+/reading"
WITH DESTINATION_TOPIC "db/crate/status"
WITH QUERY "INSERT INTO sensor_data (ts, sensor_id, value) VALUES (?, ?, ?)"
```
Optimized for high ingestion rates:
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
DEFINE ROUTE MetricsDB WITH TYPE CRATEDB
ADD CRATEDB_CONFIG
WITH HOST "cratedb.example.com"
WITH PORT '5432'
WITH DATABASE "metrics"
WITH USERNAME "metrics_writer"
WITH PASSWORD "secure_password"
ADD EVENT StoreMetrics
WITH SOURCE_TOPIC "metrics/#"
WITH QUERY "INSERT INTO metrics (ts, topic, data) VALUES (?, ?, ?)"
```
Route to different tables by topic:
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
DEFINE ROUTE MultiTableDB WITH TYPE CRATEDB
ADD CRATEDB_CONFIG
WITH HOST "cratedb.example.com"
WITH PORT '5432'
WITH DATABASE "iot"
WITH USERNAME "crate"
WITH PASSWORD "secure_password"
ADD EVENT StoreSensors
WITH SOURCE_TOPIC "sensors/#"
WITH QUERY "INSERT INTO sensors (ts, topic, data) VALUES (?, ?, ?)"
ADD EVENT StoreAlerts
WITH SOURCE_TOPIC "alerts/#"
WITH QUERY "INSERT INTO alerts (ts, topic, data) VALUES (?, ?, ?)"
```
Insert or ignore on conflict. CrateDB supports `ON CONFLICT DO NOTHING` to skip duplicate inserts for idempotent ingestion:
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
DEFINE ROUTE EventLog WITH TYPE CRATEDB
ADD CRATEDB_CONFIG
WITH HOST "cratedb.example.com"
WITH PORT '5432'
WITH DATABASE "iot"
WITH USERNAME "crate"
WITH PASSWORD "secure_password"
ADD EVENT StoreUniqueEvent
WITH SOURCE_TOPIC "events/+/occurred"
WITH QUERY "INSERT INTO events (event_id, device_id, event_type, ts) VALUES ('{value.json.event_id}', '{value.json.device_id}', '{value.json.event_type}', '{value.json.timestamp}') ON CONFLICT (event_id) DO NOTHING"
```
`ON CONFLICT (event_id) DO NOTHING` ensures duplicate messages — common in IoT delivery — never create duplicate rows.
Modify existing records when a status change arrives:
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
DEFINE ROUTE DeviceRegistry WITH TYPE CRATEDB
ADD CRATEDB_CONFIG
WITH HOST "cratedb.example.com"
WITH PORT '5432'
WITH DATABASE "iot"
WITH USERNAME "crate"
WITH PASSWORD "secure_password"
ADD EVENT UpdateDeviceStatus
WITH SOURCE_TOPIC "devices/+/status"
WITH QUERY "UPDATE devices SET current_status = '{value.json.status}', last_seen = NOW() WHERE device_id = '{value.json.device_id}'"
```
Remove records on a decommission or cleanup event:
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
DEFINE ROUTE DeviceRegistry WITH TYPE CRATEDB
ADD CRATEDB_CONFIG
WITH HOST "cratedb.example.com"
WITH PORT '5432'
WITH DATABASE "iot"
WITH USERNAME "crate"
WITH PASSWORD "secure_password"
ADD EVENT DecommissionDevice
WITH SOURCE_TOPIC "devices/+/decommissioned"
WITH QUERY "DELETE FROM devices WHERE device_id = '{value.json.device_id}'"
```
**Alternative: STORE IN with Models** — Instead of writing EVENT queries, you can bind a [model](/latest/lot-language/models/overview) directly to this route. Every `PUBLISH MODEL` call automatically inserts a row — no query needed:
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
DEFINE MODEL SensorReading
ADD STRING "sensor_id"
ADD DOUBLE "value"
STORE IN "TimeSeriesDB"
WITH TABLE "sensor_data"
```
See [Data Storage Overview](./overview#with-models-store-in) for the full STORE IN workflow.
***
## Reading Data
EVENTs also support SELECT queries to read data back from the database. Publish a message to the event's `SOURCE_TOPIC`, and the query result is published to `DESTINATION_TOPIC`:
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
DEFINE ROUTE SensorDB WITH TYPE CRATEDB
ADD CRATEDB_CONFIG
WITH HOST "cratedb.example.com"
WITH PORT '5432'
WITH DATABASE "iot"
WITH USERNAME "crate"
WITH PASSWORD "secure_password"
ADD EVENT GetLatestReading
WITH SOURCE_TOPIC "db/query/latest"
WITH DESTINATION_TOPIC "db/result/latest"
WITH QUERY "SELECT sensor_id, value FROM sensor_data WHERE sensor_id = '{payload}' ORDER BY ts DESC LIMIT 1"
```
To trigger this query, publish the sensor ID to the source topic:
```
Topic: db/query/latest
Payload: temp001
```
The query result is published to `db/result/latest`, where your actions or external clients can consume it.
### Timed Polling
EVENTs can also run SELECT queries on a fixed schedule using `WITH EVERY`. This is especially powerful with CrateDB's fast aggregations:
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
DEFINE ROUTE TimeSeriesDB WITH TYPE CRATEDB
ADD CRATEDB_CONFIG
WITH HOST "cratedb.example.com"
WITH PORT '5432'
WITH DATABASE "iot"
WITH USERNAME "crate"
WITH PASSWORD "secure_password"
ADD EVENT PollLatestReadings
WITH EVERY 5 SECONDS
WITH DESTINATION_TOPIC "dashboard/sensors/latest"
WITH QUERY "SELECT device_id, AVG(value) as avg_value, MAX(ts) as last_seen FROM sensor_data WHERE ts > NOW() - INTERVAL '1 minute' GROUP BY device_id ORDER BY device_id"
```
***
## Table Schema Examples
Create tables optimized for time-series:
```sql wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
-- Partitioned time-series table
CREATE TABLE sensor_data (
ts TIMESTAMP WITH TIME ZONE,
sensor_id TEXT,
value DOUBLE PRECISION
) PARTITIONED BY (ts);
-- Metrics with JSON storage
CREATE TABLE metrics (
ts TIMESTAMP WITH TIME ZONE,
topic TEXT,
data OBJECT(DYNAMIC)
) PARTITIONED BY (ts);
```
***
## Troubleshooting
* Verify HOST and PORT are correct
* Check CrateDB is running and accepting connections
* Verify firewall allows connections on port 5432
* Verify USERNAME and PASSWORD are correct
* Check user has permissions on the DATABASE
* Verify table exists and has correct schema
* Check column types match inserted data
***
## Next Steps
Compare all storage options.
Configure Snowflake cloud data warehouse.
# File Storage Route
Source: https://docs.coreflux.org/latest/lot-language/routes/data-storage/file-storage
Store MQTT data to local CSV and JSON files for simple persistence and backups
## File Storage Overview
The `FILE_STORAGE` route writes MQTT messages to local CSV or JSON files. It provides simple persistence for logging, backups, and data export without requiring a database server.
File storage is ideal for simple logging, local backups, and exporting data for offline analysis. Use it when you don't need a full database but want persistent records.
## Basic Syntax
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
DEFINE ROUTE DataLogger WITH TYPE FILE_STORAGE
ADD FILE_STORAGE_CONFIG
WITH STORAGE_DIR "/data/logs"
WITH FORMAT "CSV"
WITH FILE_PATTERN "{0}_{date}.csv"
WITH APPEND "true"
ADD EVENT LogSensorData
WITH SOURCE_TOPIC "sensors/#"
WITH QUERY "CLEAN:{table: sensor_data, data: {value.json}}"
```
***
## Connection Configuration
### FILE\_STORAGE\_CONFIG Parameters
Directory path for storing files.
File name pattern. Use `{0}` for table name, `{date}` for current date. Default: `{0}.csv`
File format: `CSV` or `JSON`. Default: CSV.
CSV delimiter character. Default: comma (`,`).
Append to existing files. Default: false (overwrite).
Auto-add timestamp field. Default: false.
Maximum records per file before rotation. Default: 10000.
Timestamp format string. Default: `yyyy-MM-dd HH:mm:ss`.
**Use environment variables for paths in production.** Keep deployment-specific values out of your route definitions:
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
WITH STORAGE_DIR GET ENV "STORAGE_PATH"
```
See [Environment Variables & Secrets](/latest/mqtt-broker/secrets-and-env) for setup and usage.
***
## CLEAN Query Format
File storage routes use the CLEAN format:
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
WITH QUERY "CLEAN:{table: , data: {value.json}}"
```
The `table` name is used in the file pattern with `{0}`.
***
## Writing Data
Log sensor data to daily CSV files:
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
DEFINE ROUTE CSVLogger WITH TYPE FILE_STORAGE
ADD FILE_STORAGE_CONFIG
WITH STORAGE_DIR "/data/logs"
WITH FORMAT "CSV"
WITH FILE_PATTERN "{0}_{date}.csv"
WITH APPEND "true"
WITH CSV_DELIMITER ","
WITH INCLUDE_TIMESTAMP "true"
WITH MAX_RECORDS "10000"
ADD EVENT LogSensorData
WITH SOURCE_TOPIC "sensors/#"
WITH DESTINATION_TOPIC "file/status"
WITH QUERY "CLEAN:{table: sensor_data, data: {value.json}}"
```
Creates files like: `sensor_data_2025-01-15.csv`
Store events as JSON lines:
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
DEFINE ROUTE JSONLogger WITH TYPE FILE_STORAGE
ADD FILE_STORAGE_CONFIG
WITH STORAGE_DIR "/data/json"
WITH FORMAT "JSON"
WITH FILE_PATTERN "{0}.json"
WITH APPEND "true"
WITH INCLUDE_TIMESTAMP "true"
WITH MAX_RECORDS "5000"
ADD EVENT LogEvents
WITH SOURCE_TOPIC "events/#"
WITH DESTINATION_TOPIC "file/json/status"
WITH QUERY "CLEAN:{table: events, data: {value.json}}"
```
Route to different files by data type:
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
DEFINE ROUTE MultiFileLogger WITH TYPE FILE_STORAGE
ADD FILE_STORAGE_CONFIG
WITH STORAGE_DIR "/data/logs"
WITH FORMAT "CSV"
WITH FILE_PATTERN "{0}_{date}.csv"
WITH APPEND "true"
WITH INCLUDE_TIMESTAMP "true"
ADD EVENT LogTemperature
WITH SOURCE_TOPIC "sensors/+/temperature"
WITH QUERY "CLEAN:{table: temperature, data: {value.json}}"
ADD EVENT LogPressure
WITH SOURCE_TOPIC "sensors/+/pressure"
WITH QUERY "CLEAN:{table: pressure, data: {value.json}}"
ADD EVENT LogAlerts
WITH SOURCE_TOPIC "alerts/#"
WITH QUERY "CLEAN:{table: alerts, data: {value.json}}"
```
Creates: `temperature_2025-01-15.csv`, `pressure_2025-01-15.csv`, `alerts_2025-01-15.csv`
Create backup files with rotation:
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
DEFINE ROUTE BackupStorage WITH TYPE FILE_STORAGE
ADD FILE_STORAGE_CONFIG
WITH STORAGE_DIR "/backup/mqtt"
WITH FORMAT "JSON"
WITH FILE_PATTERN "{0}_{date}.jsonl"
WITH APPEND "true"
WITH MAX_RECORDS "100000"
WITH INCLUDE_TIMESTAMP "true"
ADD EVENT BackupAll
WITH SOURCE_TOPIC "#"
WITH QUERY "CLEAN:{table: mqtt_backup, data: {value.json}}"
```
**Alternative: STORE IN with Models** — Instead of writing EVENT queries, you can bind a [model](/latest/lot-language/models/overview) directly to this route. Every `PUBLISH MODEL` call automatically inserts a row — no query needed:
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
DEFINE MODEL SensorReading
ADD STRING "sensor_id"
ADD DOUBLE "value"
STORE IN "DataLogger"
WITH TABLE "sensor_data"
```
See [Data Storage Overview](./overview#with-models-store-in) for the full STORE IN workflow.
***
## Reading Data
EVENTs also support read queries to retrieve data from stored files. Publish a message to the event's `SOURCE_TOPIC`, and the query result is published to `DESTINATION_TOPIC`. File Storage read queries use the CLEAN format with `filter`, `sort`, and `limit` instead of SQL:
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
DEFINE ROUTE DataLogger WITH TYPE FILE_STORAGE
ADD FILE_STORAGE_CONFIG
WITH STORAGE_DIR "/data/logs"
WITH FORMAT "CSV"
WITH FILE_PATTERN "{0}_{date}.csv"
WITH APPEND "true"
ADD EVENT GetLatestEntry
WITH SOURCE_TOPIC "file/query/latest"
WITH DESTINATION_TOPIC "file/result/latest"
WITH QUERY "CLEAN:{table: sensor_data, filter: {\"sensor_id\": \"{payload}\"}, sort: {\"timestamp\": -1}, limit: 1}"
```
To trigger this query, publish the sensor ID to the source topic:
```
Topic: file/query/latest
Payload: temp001
```
The matching record is published to `file/result/latest`, where your actions or external clients can consume it.
### CLEAN Read Operators
| SQL Equivalent | File Storage CLEAN Syntax |
| ----------------- | ------------------------------ |
| `SELECT *` | `{"table": "x"}` |
| `WHERE a = 1` | `filter: {"a": 1}` |
| `WHERE a > 10` | `filter: {"a": {"$gt": 10}}` |
| `ORDER BY a DESC` | `sort: {"a": -1}` |
| `LIMIT 10` | `limit: 10` |
| `OFFSET 5` | `skip: 5` |
| `SELECT a, b` | `projection: {"a": 1, "b": 1}` |
***
## File Pattern Placeholders
| Placeholder | Description | Example |
| ----------- | --------------------------- | ------------- |
| `{0}` | Table name from CLEAN query | `sensor_data` |
| `{date}` | Current date (yyyy-MM-dd) | `2025-01-15` |
***
## Output Examples
### CSV Output
```csv wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
timestamp,sensor_id,temperature,humidity
2025-01-15 10:30:00,temp001,23.5,65
2025-01-15 10:30:05,temp002,24.1,62
```
### JSON Output (JSON Lines format)
```json wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
{"timestamp":"2025-01-15T10:30:00Z","sensor_id":"temp001","temperature":23.5,"humidity":65}
{"timestamp":"2025-01-15T10:30:05Z","sensor_id":"temp002","temperature":24.1,"humidity":62}
```
***
## Troubleshooting
* Verify STORAGE\_DIR exists
* Check write permissions on directory
* Ensure Coreflux process has access
* Verify STORAGE\_DIR path is correct
* Check disk space is available
* Ensure directory is writable
* Verify SOURCE\_TOPIC matches incoming messages
* Check QUERY syntax is correct
* Verify APPEND is set correctly
* Check MAX\_RECORDS is set appropriately
* Verify FILE\_PATTERN includes `{date}` for daily rotation
***
## Next Steps
Compare all storage options.
Configure PostgreSQL database storage.
# MariaDB Route
Source: https://docs.coreflux.org/latest/lot-language/routes/data-storage/mariadb
Store MQTT data in MariaDB with automatic query execution and MySQL compatibility
## MariaDB Overview
The `MARIADB` route stores MQTT messages in MariaDB databases. MariaDB is a MySQL-compatible database with enhanced features and performance optimizations.
MariaDB offers drop-in MySQL compatibility with additional features like improved replication and storage engines. Use the same configuration as MySQL with the `MARIADB` route type.
## Basic Syntax
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
DEFINE ROUTE SensorDB WITH TYPE MARIADB
ADD SQL_CONFIG
WITH SERVER "mariadb.example.com"
WITH PORT '3306'
WITH DATABASE "iot_data"
WITH USERNAME "iot_user"
WITH PASSWORD "secure_password"
ADD EVENT StoreSensorReading
WITH SOURCE_TOPIC "sensors/+/reading"
WITH QUERY "INSERT INTO readings (ts, sensor_id, value) VALUES (NOW(), '{sensor_id}', '{value.json}')"
```
***
## Connection Configuration
### SQL\_CONFIG Parameters
MariaDB server hostname or IP address.
Target database name.
Database username.
Database password.
MariaDB port. Default: 3306.
Enable SSL connection. Default: false.
**Never hardcode credentials in production.** Use environment variables and encrypted secrets to keep sensitive values out of your route definitions:
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
WITH SERVER GET ENV "DB_HOST"
WITH USERNAME GET ENV "DB_USER"
WITH PASSWORD GET SECRET "DB_PASSWORD"
```
See [Environment Variables & Secrets](/latest/mqtt-broker/secrets-and-env) for setup and usage.
***
## Writing Data
Store sensor data in MariaDB:
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
DEFINE ROUTE SensorStorage WITH TYPE MARIADB
ADD SQL_CONFIG
WITH SERVER "mariadb.example.com"
WITH PORT '3306'
WITH DATABASE "iot_data"
WITH USERNAME "iot_user"
WITH PASSWORD "secure_password"
ADD EVENT StoreReading
WITH SOURCE_TOPIC "sensors/#"
WITH QUERY "INSERT INTO sensor_data (timestamp, topic, payload) VALUES (NOW(), '{source_topic}', '{value.json}')"
```
Secure connection:
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
DEFINE ROUTE SecureStorage WITH TYPE MARIADB
ADD SQL_CONFIG
WITH SERVER "mariadb.cloud.com"
WITH PORT '3306'
WITH DATABASE "production"
WITH USERNAME "app_user"
WITH PASSWORD "secure_password"
WITH USE_SSL "true"
ADD EVENT StoreEvent
WITH SOURCE_TOPIC "events/#"
WITH QUERY "INSERT INTO events (ts, topic, data) VALUES (NOW(), '{source_topic}', '{value.json}')"
```
Insert a record or update it if a duplicate key exists. MariaDB supports both `ON DUPLICATE KEY UPDATE` (MySQL-compatible) and `ON CONFLICT` (PostgreSQL-compatible in newer versions):
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
DEFINE ROUTE DeviceRegistry WITH TYPE MARIADB
ADD SQL_CONFIG
WITH SERVER "mariadb.example.com"
WITH PORT '3306'
WITH DATABASE "iot_data"
WITH USERNAME "iot_user"
WITH PASSWORD "secure_password"
ADD EVENT RegisterDevice
WITH SOURCE_TOPIC "devices/+/registered"
WITH QUERY "INSERT INTO devices (device_id, name, area, active, updated_at) VALUES ('{value.json.device_id}', '{value.json.name}', '{value.json.area}', 1, NOW()) ON DUPLICATE KEY UPDATE name = VALUES(name), area = VALUES(area), updated_at = NOW()"
```
Modify existing records when a status change arrives:
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
DEFINE ROUTE DeviceRegistry WITH TYPE MARIADB
ADD SQL_CONFIG
WITH SERVER "mariadb.example.com"
WITH PORT '3306'
WITH DATABASE "iot_data"
WITH USERNAME "iot_user"
WITH PASSWORD "secure_password"
ADD EVENT UpdateDeviceStatus
WITH SOURCE_TOPIC "devices/+/status"
WITH QUERY "UPDATE devices SET current_status = '{value.json.status}', last_seen = NOW() WHERE device_id = '{value.json.device_id}'"
```
Remove records on a decommission or cleanup event:
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
DEFINE ROUTE DeviceRegistry WITH TYPE MARIADB
ADD SQL_CONFIG
WITH SERVER "mariadb.example.com"
WITH PORT '3306'
WITH DATABASE "iot_data"
WITH USERNAME "iot_user"
WITH PASSWORD "secure_password"
ADD EVENT DecommissionDevice
WITH SOURCE_TOPIC "devices/+/decommissioned"
WITH QUERY "DELETE FROM devices WHERE device_id = '{value.json.device_id}' AND active = 0"
```
**Alternative: STORE IN with Models** — Instead of writing EVENT queries, you can bind a [model](/latest/lot-language/models/overview) directly to this route. Every `PUBLISH MODEL` call automatically inserts a row — no query needed:
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
DEFINE MODEL SensorReading
ADD STRING "sensor_id"
ADD DOUBLE "value"
STORE IN "SensorStorage"
WITH TABLE "sensor_data"
```
See [Data Storage Overview](./overview#with-models-store-in) for the full STORE IN workflow.
***
## Reading Data
EVENTs also support SELECT queries to read data back from the database. Publish a message to the event's `SOURCE_TOPIC`, and the query result is published to `DESTINATION_TOPIC`:
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
DEFINE ROUTE SensorDB WITH TYPE MARIADB
ADD SQL_CONFIG
WITH SERVER "mariadb.example.com"
WITH PORT '3306'
WITH DATABASE "iot_data"
WITH USERNAME "iot_user"
WITH PASSWORD "secure_password"
ADD EVENT GetLatestReading
WITH SOURCE_TOPIC "db/query/latest"
WITH DESTINATION_TOPIC "db/result/latest"
WITH QUERY "SELECT sensor_id, value FROM sensor_data WHERE sensor_id = '{payload}' ORDER BY timestamp DESC LIMIT 1"
```
To trigger this query, publish the sensor ID to the source topic:
```
Topic: db/query/latest
Payload: temp001
```
The query result is published to `db/result/latest`, where your actions or external clients can consume it.
### Timed Polling
EVENTs can also run SELECT queries on a fixed schedule using `WITH EVERY`:
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
DEFINE ROUTE DeviceRegistry WITH TYPE MARIADB
ADD SQL_CONFIG
WITH SERVER "mariadb.example.com"
WITH PORT '3306'
WITH DATABASE "iot_data"
WITH USERNAME "iot_user"
WITH PASSWORD "secure_password"
ADD EVENT PollActiveDevices
WITH EVERY 10 SECONDS
WITH DESTINATION_TOPIC "dashboard/devices/active"
WITH QUERY "SELECT device_id, name, current_status, last_seen FROM devices WHERE active = 1 ORDER BY name"
```
***
## Troubleshooting
* Verify SERVER and PORT are correct
* Check MariaDB is running and accepting connections
* Verify firewall allows connections on port 3306
* Verify USERNAME and PASSWORD are correct
* Check user has permissions on the DATABASE
* Verify user can connect from your host
***
## Next Steps
Compare all storage options.
Configure Microsoft SQL Server storage.
# MongoDB Route
Source: https://docs.coreflux.org/latest/lot-language/routes/data-storage/mongodb
Store MQTT data in MongoDB with flexible document schemas and CLEAN query format
## MongoDB Overview
The `MONGODB` route stores MQTT messages as documents in MongoDB collections. It uses the CLEAN query format for simplified JSON syntax and supports flexible schemas for IoT data.
MongoDB is ideal for IoT data with varying schemas. Store sensor readings, events, and logs without defining rigid table structures upfront.
## Basic Syntax
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
DEFINE ROUTE SensorDB WITH TYPE MONGODB
ADD MONGODB_CONFIG
WITH CONNECTION_STRING "mongodb://user:password@mongodb.example.com:27017/iot?authSource=admin"
WITH DATABASE "iot"
ADD EVENT StoreSensorReading
WITH SOURCE_TOPIC "sensors/+/reading"
WITH QUERY "CLEAN:{collection: readings, document: {sensor_id: {sensor_id}, value: {value.json}, timestamp: {timestamp}}}"
```
***
## Connection Configuration
### MONGODB\_CONFIG Parameters
MongoDB connection URI. Format: `mongodb://user:password@host:port/database?options`
Target database name (can also be specified in connection string).
**Never hardcode credentials in production.** Use environment variables and encrypted secrets to keep sensitive values out of your route definitions:
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
WITH CONNECTION_STRING GET SECRET "MONGO_CONNECTION_STRING"
```
See [Environment Variables & Secrets](/latest/mqtt-broker/secrets-and-env) for setup and usage.
***
## CLEAN Query Format
MongoDB routes use the CLEAN format for simplified document insertion:
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
WITH QUERY "CLEAN:{collection: , document: {: , ...}}"
```
### CLEAN Placeholders
| Placeholder | Description |
| -------------------- | -------------------------------------- |
| `{value.json}` | Full JSON payload as embedded document |
| `{value.json.field}` | Specific field from JSON payload |
| `{timestamp}` | Message timestamp |
| `{source_topic}` | Original MQTT topic |
| `{field}` | Field extracted from topic path |
***
## Event Configuration
### EVENT Parameters
MQTT topic pattern that triggers the INSERT. Supports wildcards.
CLEAN format query for document insertion.
MQTT topic to publish operation status.
***
## Writing Data
Store sensor readings as documents:
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
DEFINE ROUTE SensorStorage WITH TYPE MONGODB
ADD MONGODB_CONFIG
WITH CONNECTION_STRING "mongodb://iot_user:password@mongodb.example.com:27017/iot?authSource=admin"
WITH DATABASE "iot"
ADD EVENT StoreReading
WITH SOURCE_TOPIC "sensors/+/reading"
WITH DESTINATION_TOPIC "db/mongo/status"
WITH QUERY "CLEAN:{collection: readings, document: {timestamp: {timestamp}, topic: {source_topic}, data: {value.json}}}"
```
Create structured documents with specific fields:
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
DEFINE ROUTE StructuredStorage WITH TYPE MONGODB
ADD MONGODB_CONFIG
WITH CONNECTION_STRING "mongodb://user:pass@mongodb.example.com:27017/iot"
WITH DATABASE "iot"
ADD EVENT StoreSensor
WITH SOURCE_TOPIC "sensors/+/data"
WITH QUERY "CLEAN:{collection: sensor_data, document: {sensor_id: {sensor_id}, temperature: {value.json.temperature}, humidity: {value.json.humidity}, recorded_at: {timestamp}}}"
```
**Input at** `sensors/temp001/data`:
```json wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
{"temperature": 23.5, "humidity": 65}
```
**Stored document:**
```json wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
{
"sensor_id": "temp001",
"temperature": 23.5,
"humidity": 65,
"recorded_at": "2025-01-15T10:30:00Z"
}
```
Route to different collections based on topic:
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
DEFINE ROUTE MultiCollection WITH TYPE MONGODB
ADD MONGODB_CONFIG
WITH CONNECTION_STRING "mongodb://user:pass@mongodb.example.com:27017/iot"
WITH DATABASE "iot"
ADD EVENT StoreSensors
WITH SOURCE_TOPIC "sensors/#"
WITH QUERY "CLEAN:{collection: sensors, document: {topic: {source_topic}, data: {value.json}, ts: {timestamp}}}"
ADD EVENT StoreAlerts
WITH SOURCE_TOPIC "alerts/#"
WITH QUERY "CLEAN:{collection: alerts, document: {topic: {source_topic}, alert: {value.json}, ts: {timestamp}}}"
ADD EVENT StoreEvents
WITH SOURCE_TOPIC "events/#"
WITH QUERY "CLEAN:{collection: events, document: {topic: {source_topic}, event: {value.json}, ts: {timestamp}}}"
```
Connect to a MongoDB replica set:
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
DEFINE ROUTE ReplicaSetStorage WITH TYPE MONGODB
ADD MONGODB_CONFIG
WITH CONNECTION_STRING "mongodb://user:pass@mongo1.example.com:27017,mongo2.example.com:27017,mongo3.example.com:27017/iot?replicaSet=rs0&authSource=admin"
WITH DATABASE "iot"
ADD EVENT StoreData
WITH SOURCE_TOPIC "data/#"
WITH QUERY "CLEAN:{collection: data, document: {topic: {source_topic}, payload: {value.json}, timestamp: {timestamp}}}"
```
**Alternative: STORE IN with Models** — Instead of writing EVENT queries, you can bind a [model](/latest/lot-language/models/overview) directly to this route. Every `PUBLISH MODEL` call automatically inserts a row — no query needed:
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
DEFINE MODEL SensorReading
ADD STRING "sensor_id"
ADD DOUBLE "value"
STORE IN "SensorStorage"
WITH TABLE "readings"
```
See [Data Storage Overview](./overview#with-models-store-in) for the full STORE IN workflow.
***
## Reading Data
EVENTs also support read queries to retrieve data from the database. Publish a message to the event's `SOURCE_TOPIC`, and the query result is published to `DESTINATION_TOPIC`. MongoDB read queries use the CLEAN format with `filter`, `sort`, and `limit` instead of SQL:
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
DEFINE ROUTE SensorDB WITH TYPE MONGODB
ADD MONGODB_CONFIG
WITH CONNECTION_STRING "mongodb://user:password@mongodb.example.com:27017/iot?authSource=admin"
WITH DATABASE "iot"
ADD EVENT GetLatestReading
WITH SOURCE_TOPIC "db/query/latest"
WITH DESTINATION_TOPIC "db/result/latest"
WITH QUERY "CLEAN:{collection: readings, filter: \"{\"sensor_id\": \"{payload}\"}\", sort: \"timestamp desc\", limit: 1}"
```
To trigger this query, publish the sensor ID to the source topic:
```
Topic: db/query/latest
Payload: temp001
```
The matching document is published to `db/result/latest`, where your actions or external clients can consume it.
### CLEAN Read Operators
| SQL Equivalent | MongoDB CLEAN Syntax |
| ----------------- | ---------------------------------- |
| `SELECT *` | `{}` |
| `WHERE a = 1` | `filter: "{\"a\": 1}"` |
| `WHERE a > 10` | `filter: "{\"a\": {\"$gt\": 10}}"` |
| `ORDER BY a DESC` | `sort: "a desc"` |
| `LIMIT 10` | `limit: 10` |
| `SELECT a, b` | `projection: "a, b"` |
***
## Connection String Examples
| Scenario | Connection String |
| ------------- | --------------------------------------------------------- |
| Local | `mongodb://localhost:27017/iot` |
| Authenticated | `mongodb://user:password@host:27017/iot?authSource=admin` |
| Replica Set | `mongodb://host1:27017,host2:27017/iot?replicaSet=rs0` |
| Atlas | `mongodb+srv://user:password@cluster.mongodb.net/iot` |
***
## Troubleshooting
* Verify CONNECTION\_STRING is correct
* Check MongoDB is running and accessible
* Verify firewall allows connections on port 27017
* For Atlas, ensure IP whitelist includes your server
* Verify username and password in connection string
* Check authSource parameter matches authentication database
* Verify user has permissions on target database
* Verify collection name is valid
* Check placeholder names match topic structure
* Ensure document structure is valid JSON
***
## Next Steps
Compare all storage options.
Configure OpenSearch for search and analytics.
# MySQL Route
Source: https://docs.coreflux.org/latest/lot-language/routes/data-storage/mysql
Store MQTT data in MySQL with automatic query execution and SSL support
## MySQL Overview
The `MYSQL` route stores MQTT messages in MySQL databases. It supports SSL connections, parameterized queries, and automatic reconnection for reliable data persistence.
MySQL is ideal for web applications and general-purpose data storage. Its wide ecosystem support makes it a reliable choice for most IoT data persistence needs.
## Basic Syntax
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
DEFINE ROUTE SensorDB WITH TYPE MYSQL
ADD SQL_CONFIG
WITH SERVER "mysql.example.com"
WITH PORT '3306'
WITH DATABASE "iot_data"
WITH USERNAME "iot_user"
WITH PASSWORD "secure_password"
ADD EVENT StoreSensorReading
WITH SOURCE_TOPIC "sensors/+/reading"
WITH QUERY "INSERT INTO readings (ts, sensor_id, value) VALUES (NOW(), '{sensor_id}', '{value.json}')"
```
***
## Connection Configuration
### SQL\_CONFIG Parameters
MySQL server hostname or IP address.
Target database name.
Database username.
Database password.
MySQL port. Default: 3306.
Enable SSL connection. Default: false.
Connection timeout in seconds. Default: 30.
**Never hardcode credentials in production.** Use environment variables and encrypted secrets to keep sensitive values out of your route definitions:
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
WITH SERVER GET ENV "DB_HOST"
WITH USERNAME GET ENV "DB_USER"
WITH PASSWORD GET SECRET "DB_PASSWORD"
```
See [Environment Variables & Secrets](/latest/mqtt-broker/secrets-and-env) for setup and usage.
***
## Event Configuration
### EVENT Parameters
MQTT topic pattern that triggers the INSERT. Supports wildcards.
SQL INSERT statement with placeholders for dynamic values.
MQTT topic to publish query result status.
### Query Placeholders
| Placeholder | Description |
| -------------------- | ------------------------------- |
| `{value.json}` | Full JSON payload |
| `{value.json.field}` | Specific field from JSON |
| `{timestamp}` | Message timestamp |
| `{source_topic}` | Original MQTT topic |
| `{field}` | Field extracted from topic path |
***
## Writing Data
Store sensor readings with timestamps:
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
DEFINE ROUTE TemperatureDB WITH TYPE MYSQL
ADD SQL_CONFIG
WITH SERVER "mysql.example.com"
WITH PORT '3306'
WITH DATABASE "iot_data"
WITH USERNAME "iot_user"
WITH PASSWORD "secure_password"
ADD EVENT StoreTemperature
WITH SOURCE_TOPIC "sensors/+/temperature"
WITH QUERY "INSERT INTO temperature_readings (recorded_at, sensor_id, value) VALUES (NOW(), '{sensor_id}', {value.json.value})"
```
Secure connection with SSL:
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
DEFINE ROUTE SecureDB WITH TYPE MYSQL
ADD SQL_CONFIG
WITH SERVER "mysql.cloud.com"
WITH PORT '3306'
WITH DATABASE "production"
WITH USERNAME "app_user"
WITH PASSWORD "secure_password"
WITH USE_SSL "true"
ADD EVENT StoreEvent
WITH SOURCE_TOPIC "events/#"
WITH QUERY "INSERT INTO events (ts, topic, payload) VALUES (NOW(), '{source_topic}', '{value.json}')"
```
Different tables for different data types:
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
DEFINE ROUTE MultiTableDB WITH TYPE MYSQL
ADD SQL_CONFIG
WITH SERVER "mysql.example.com"
WITH PORT '3306'
WITH DATABASE "iot_data"
WITH USERNAME "iot_user"
WITH PASSWORD "secure_password"
ADD EVENT StoreSensorData
WITH SOURCE_TOPIC "sensors/+/data"
WITH QUERY "INSERT INTO sensor_data (ts, sensor_id, value) VALUES (NOW(), '{sensor_id}', '{value.json}')"
ADD EVENT StoreAlerts
WITH SOURCE_TOPIC "alerts/+"
WITH QUERY "INSERT INTO alerts (ts, alert_type, message) VALUES (NOW(), '{alert_type}', '{value.json}')"
```
Insert a record or update it if a duplicate key exists. MySQL uses `ON DUPLICATE KEY UPDATE`:
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
DEFINE ROUTE DeviceRegistry WITH TYPE MYSQL
ADD SQL_CONFIG
WITH SERVER "mysql.example.com"
WITH PORT '3306'
WITH DATABASE "iot_data"
WITH USERNAME "iot_user"
WITH PASSWORD "secure_password"
ADD EVENT RegisterDevice
WITH SOURCE_TOPIC "devices/+/registered"
WITH QUERY "INSERT INTO devices (device_id, name, area, active, updated_at) VALUES ('{value.json.device_id}', '{value.json.name}', '{value.json.area}', 1, NOW()) ON DUPLICATE KEY UPDATE name = VALUES(name), area = VALUES(area), updated_at = NOW()"
```
`ON DUPLICATE KEY UPDATE` fires only when a row with the same unique key already exists — the existing row is updated instead of inserting a duplicate.
Modify existing records when a status or value change arrives:
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
DEFINE ROUTE DeviceRegistry WITH TYPE MYSQL
ADD SQL_CONFIG
WITH SERVER "mysql.example.com"
WITH PORT '3306'
WITH DATABASE "iot_data"
WITH USERNAME "iot_user"
WITH PASSWORD "secure_password"
ADD EVENT UpdateDeviceStatus
WITH SOURCE_TOPIC "devices/+/status"
WITH QUERY "UPDATE devices SET current_status = '{value.json.status}', last_seen = NOW() WHERE device_id = '{value.json.device_id}'"
```
Remove records on a decommission or cleanup event:
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
DEFINE ROUTE DeviceRegistry WITH TYPE MYSQL
ADD SQL_CONFIG
WITH SERVER "mysql.example.com"
WITH PORT '3306'
WITH DATABASE "iot_data"
WITH USERNAME "iot_user"
WITH PASSWORD "secure_password"
ADD EVENT DecommissionDevice
WITH SOURCE_TOPIC "devices/+/decommissioned"
WITH QUERY "DELETE FROM devices WHERE device_id = '{value.json.device_id}' AND active = 0"
```
**Alternative: STORE IN with Models** — Instead of writing EVENT queries, you can bind a [model](/latest/lot-language/models/overview) directly to this route. Every `PUBLISH MODEL` call automatically inserts a row — no query needed:
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
DEFINE MODEL SensorReading
ADD STRING "sensor_id"
ADD DOUBLE "value"
STORE IN "TemperatureDB"
WITH TABLE "temperature_readings"
```
See [Data Storage Overview](./overview#with-models-store-in) for the full STORE IN workflow.
***
## Reading Data
EVENTs also support SELECT queries to read data back from the database. Publish a message to the event's `SOURCE_TOPIC`, and the query result is published to `DESTINATION_TOPIC`:
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
DEFINE ROUTE SensorDB WITH TYPE MYSQL
ADD SQL_CONFIG
WITH SERVER "mysql.example.com"
WITH PORT '3306'
WITH DATABASE "iot_data"
WITH USERNAME "iot_user"
WITH PASSWORD "secure_password"
ADD EVENT GetLatestReading
WITH SOURCE_TOPIC "db/query/latest"
WITH DESTINATION_TOPIC "db/result/latest"
WITH QUERY "SELECT sensor_id, value FROM readings WHERE sensor_id = '{payload}' ORDER BY ts DESC LIMIT 1"
```
To trigger this query, publish the sensor ID to the source topic:
```
Topic: db/query/latest
Payload: temp001
```
The query result is published to `db/result/latest`, where your actions or external clients can consume it.
### Timed Polling
EVENTs can also run SELECT queries on a fixed schedule using `WITH EVERY`:
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
DEFINE ROUTE DeviceRegistry WITH TYPE MYSQL
ADD SQL_CONFIG
WITH SERVER "mysql.example.com"
WITH PORT '3306'
WITH DATABASE "iot_data"
WITH USERNAME "iot_user"
WITH PASSWORD "secure_password"
ADD EVENT PollActiveDevices
WITH EVERY 10 SECONDS
WITH DESTINATION_TOPIC "dashboard/devices/active"
WITH QUERY "SELECT device_id, name, current_status, last_seen FROM devices WHERE active = 1 ORDER BY name"
```
***
## Table Schema Examples
```sql wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
-- Basic sensor readings
CREATE TABLE sensor_readings (
id INT AUTO_INCREMENT PRIMARY KEY,
recorded_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
sensor_id VARCHAR(50),
value DOUBLE
);
-- Event log
CREATE TABLE events (
id INT AUTO_INCREMENT PRIMARY KEY,
ts TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
topic VARCHAR(255),
payload JSON
);
```
***
## Troubleshooting
* Verify SERVER and PORT are correct
* Check MySQL is running and accepting connections
* Verify firewall allows connections on port 3306
* Check `bind-address` in MySQL configuration
* Verify USERNAME and PASSWORD are correct
* Check user has permissions on the DATABASE
* Verify user can connect from your host
* Check placeholder names match topic structure
* Verify table and column names exist
* Test query manually in MySQL client first
***
## Next Steps
Compare all storage options.
Configure MariaDB storage (MySQL-compatible).
# OpenSearch Route
Source: https://docs.coreflux.org/latest/lot-language/routes/data-storage/opensearch
Index MQTT data in OpenSearch for real-time search, analytics, and visualization
## OpenSearch Overview
The `OPENSEARCH` route indexes MQTT messages in OpenSearch for full-text search, real-time analytics, and dashboard visualization. It uses the CLEAN query format for simplified document indexing.
OpenSearch is ideal for log aggregation, metrics visualization, and full-text search across IoT data. Connect it to OpenSearch Dashboards for powerful visualizations.
## Basic Syntax
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
DEFINE ROUTE SensorIndex WITH TYPE OPENSEARCH
ADD OPENSEARCH_CONFIG
WITH BASE_URL "https://opensearch.example.com:9200"
WITH USERNAME "admin"
WITH PASSWORD "secure_password"
WITH USE_SSL "true"
ADD EVENT IndexSensorReading
WITH SOURCE_TOPIC "sensors/+/data"
WITH QUERY "CLEAN:{index: sensor-data, body: {@timestamp: {timestamp}, sensor: {sensor_id}, reading: {value.json}}}"
```
***
## Connection Configuration
### OPENSEARCH\_CONFIG Parameters
OpenSearch endpoint URL including port (e.g., `https://opensearch.example.com:9200`).
OpenSearch username.
OpenSearch password.
Enable HTTPS. Default: false.
Ignore certificate validation errors. Default: false.
**Never hardcode credentials in production.** Use environment variables and encrypted secrets to keep sensitive values out of your route definitions:
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
WITH BASE_URL GET ENV "OPENSEARCH_URL"
WITH USERNAME GET ENV "OPENSEARCH_USER"
WITH PASSWORD GET SECRET "OPENSEARCH_PASSWORD"
```
See [Environment Variables & Secrets](/latest/mqtt-broker/secrets-and-env) for setup and usage.
***
## CLEAN Query Format
OpenSearch routes use the CLEAN format for document indexing:
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
WITH QUERY "CLEAN:{index: , body: {: , ...}}"
```
### CLEAN Placeholders
| Placeholder | Description |
| -------------------- | ----------------------------------------------- |
| `{value.json}` | Full JSON payload as embedded object |
| `{value.json.field}` | Specific field from JSON payload |
| `{timestamp}` | Message timestamp (use `@timestamp` for Kibana) |
| `{source_topic}` | Original MQTT topic |
| `{field}` | Field extracted from topic path |
***
## Writing Data
Index sensor data for search:
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
DEFINE ROUTE SensorIndex WITH TYPE OPENSEARCH
ADD OPENSEARCH_CONFIG
WITH BASE_URL "https://opensearch.example.com:9200"
WITH USERNAME "admin"
WITH PASSWORD "secure_password"
WITH USE_SSL "true"
ADD EVENT IndexReading
WITH SOURCE_TOPIC "sensors/+/data"
WITH DESTINATION_TOPIC "search/status"
WITH QUERY "CLEAN:{index: sensor-data, body: {@timestamp: {timestamp}, sensor_id: {sensor_id}, data: {value.json}}}"
```
Aggregate application logs:
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
DEFINE ROUTE LogIndex WITH TYPE OPENSEARCH
ADD OPENSEARCH_CONFIG
WITH BASE_URL "https://opensearch.example.com:9200"
WITH USERNAME "admin"
WITH PASSWORD "admin_password"
WITH USE_SSL "true"
ADD EVENT IndexLogs
WITH SOURCE_TOPIC "logs/+/+"
WITH QUERY "CLEAN:{index: application-logs, body: {@timestamp: {timestamp}, source: {source_topic}, level: {value.json.level}, message: {value.json.message}}}"
```
Use date-based index names for time-series:
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
DEFINE ROUTE TimeSeriesIndex WITH TYPE OPENSEARCH
ADD OPENSEARCH_CONFIG
WITH BASE_URL "https://opensearch.example.com:9200"
WITH USERNAME "admin"
WITH PASSWORD "secure_password"
WITH USE_SSL "true"
ADD EVENT IndexMetrics
WITH SOURCE_TOPIC "metrics/#"
WITH QUERY "CLEAN:{index: metrics, body: {@timestamp: {timestamp}, topic: {source_topic}, metrics: {value.json}}}"
```
Configure Index Lifecycle Management (ILM) in OpenSearch for automatic rollover.
Connect with certificate validation disabled:
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
DEFINE ROUTE DevIndex WITH TYPE OPENSEARCH
ADD OPENSEARCH_CONFIG
WITH BASE_URL "https://localhost:9200"
WITH USERNAME "admin"
WITH PASSWORD "admin"
WITH USE_SSL "true"
WITH IGNORE_CERT_ERRORS "true"
ADD EVENT IndexData
WITH SOURCE_TOPIC "data/#"
WITH QUERY "CLEAN:{index: dev-data, body: {@timestamp: {timestamp}, topic: {source_topic}, payload: {value.json}}}"
```
Only use IGNORE\_CERT\_ERRORS in development environments.
**Alternative: STORE IN with Models** — Instead of writing EVENT queries, you can bind a [model](/latest/lot-language/models/overview) directly to this route. Every `PUBLISH MODEL` call automatically inserts a row — no query needed:
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
DEFINE MODEL SensorReading
ADD STRING "sensor_id"
ADD DOUBLE "value"
STORE IN "SensorIndex"
WITH TABLE "sensor-data"
```
See [Data Storage Overview](./overview#with-models-store-in) for the full STORE IN workflow.
***
## Reading Data
EVENTs also support search queries to retrieve data from the index. Publish a message to the event's `SOURCE_TOPIC`, and the query result is published to `DESTINATION_TOPIC`. OpenSearch read queries use the CLEAN format with `query`, `sort`, and `size` instead of SQL:
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
DEFINE ROUTE SensorIndex WITH TYPE OPENSEARCH
ADD OPENSEARCH_CONFIG
WITH BASE_URL "https://opensearch.example.com:9200"
WITH USERNAME "admin"
WITH PASSWORD "secure_password"
WITH USE_SSL "true"
ADD EVENT GetLatestReading
WITH SOURCE_TOPIC "search/query/latest"
WITH DESTINATION_TOPIC "search/result/latest"
WITH QUERY "CLEAN:{index: sensor-data, query: {\"match\": {\"sensor_id\": \"{payload}\"}}, sort: [{\"@timestamp\": {\"order\": \"desc\"}}], size: 1}"
```
To trigger this query, publish the sensor ID to the source topic:
```
Topic: search/query/latest
Payload: temp001
```
The matching document is published to `search/result/latest`, where your actions or external clients can consume it.
### CLEAN Read Operators
| SQL Equivalent | OpenSearch CLEAN Syntax |
| ----------------- | ------------------------------------- |
| `SELECT *` | `query: {"match_all": {}}` |
| `WHERE a = 1` | `query: {"match": {"a": 1}}` |
| `WHERE a > 10` | `query: {"range": {"a": {"gt": 10}}}` |
| `ORDER BY a DESC` | `sort: [{"a": {"order": "desc"}}]` |
| `LIMIT 10` | `size: 10` |
| `OFFSET 5` | `from: 5` |
***
## Index Mapping Example
Create an index with explicit mappings for better search and aggregation:
```json wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
PUT /sensor-data
{
"mappings": {
"properties": {
"@timestamp": { "type": "date" },
"sensor_id": { "type": "keyword" },
"data": {
"properties": {
"temperature": { "type": "float" },
"humidity": { "type": "float" }
}
}
}
}
}
```
***
## Troubleshooting
* Verify BASE\_URL is correct including port
* Check OpenSearch is running and accessible
* Verify firewall allows connections on port 9200
* Verify USERNAME and PASSWORD are correct
* Check user has permissions to index documents
* Ensure USE\_SSL matches the endpoint (http vs https)
* For self-signed certs, set IGNORE\_CERT\_ERRORS "true"
* Verify certificate chain is complete
* Verify index name is valid (lowercase, no special chars)
* Check field mappings don't conflict
* Review OpenSearch logs for detailed errors
***
## Next Steps
Compare all storage options.
Configure CrateDB for time-series data.
# Data Storage Routes Overview
Source: https://docs.coreflux.org/latest/lot-language/routes/data-storage/overview
Store MQTT data in databases including PostgreSQL, MySQL, MongoDB, OpenSearch, Snowflake, and file storage
## Data Storage Integration
Data storage routes enable automatic persistence of MQTT messages to databases and files. Store sensor data for analytics, create historical archives, aggregate logs, and build time-series datasets—all configured through LoT syntax.
**Like a filing cabinet with an assistant.** You can hand your assistant a form (STORE IN with models) and they file it automatically, or you can write specific filing instructions yourself (EVENTs with queries). Either way, your data ends up organized in the right drawer.
## Supported Databases
Open-source relational database for structured data storage.
Popular relational database with wide ecosystem support.
MySQL-compatible database with enhanced features.
Microsoft SQL Server for enterprise environments.
Document database for flexible JSON storage.
Real-time analytics, search, and visualization.
Distributed SQL optimized for time-series data.
Cloud data warehouse for large-scale analytics.
CSV and JSON file storage for simple persistence.
***
## Database Comparison
| Database | Type | Best For | Query Format |
| ------------ | ---------------- | --------------------------------- | ------------ |
| PostgreSQL | Relational | Structured data, complex queries | SQL |
| MySQL | Relational | Web applications, general purpose | SQL |
| MariaDB | Relational | MySQL compatibility with extras | SQL |
| SQL Server | Relational | Enterprise, Windows environments | SQL |
| MongoDB | Document | Flexible schemas, JSON data | CLEAN |
| OpenSearch | Search/Analytics | Full-text search, dashboards | CLEAN |
| CrateDB | Time-Series | High-volume sensor data | SQL |
| Snowflake | Cloud Warehouse | Large-scale analytics | SQL |
| File Storage | File | Simple logging, backups | CLEAN |
**SQL** databases use standard SQL queries. **CLEAN** is a simplified document-style syntax used by non-relational databases and file storage — see [Query Formats](#query-formats) below for details.
***
## How to Build a Data Storage Route
A data storage route needs two things: a connection to your database and at least one rule for what to store. You define the connection once, then add events that listen for MQTT messages and run queries — or use a model with `STORE IN` to save data automatically without writing queries at all.
### Quick Example
This complete route connects to a PostgreSQL database and automatically stores every sensor reading that arrives on `sensors/+/reading`:
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
DEFINE ROUTE SensorDB WITH TYPE POSTGRESQL
ADD SQL_CONFIG
WITH SERVER "postgres.example.com"
WITH PORT '5432'
WITH DATABASE "iot_data"
WITH USERNAME "iot_user"
WITH PASSWORD "secure_password"
ADD EVENT StoreSensorReading
WITH SOURCE_TOPIC "sensors/+/reading"
WITH DESTINATION_TOPIC "db/status"
WITH QUERY "INSERT INTO readings (ts, sensor_id, value) VALUES (NOW(), '{sensor_id}', '{value.json}')"
```
That's it — the broker handles the rest. The sections below break down each part in detail.
### Step 1: Define the Route Connection
Every data storage route starts with a connection configuration. Define the route type and provide the connection details for your database:
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
DEFINE ROUTE SensorDB WITH TYPE POSTGRESQL
ADD SQL_CONFIG
WITH SERVER "postgres.example.com"
WITH PORT '5432'
WITH DATABASE "iot_data"
WITH USERNAME "iot_user"
WITH PASSWORD "secure_password"
```
Each database type has its own config block (`SQL_CONFIG`, `MONGODB_CONFIG`, `OPENSEARCH_CONFIG`, etc.). See the specific database page for exact parameters.
The examples on this page use hardcoded credentials for clarity. In production, use environment variables and encrypted secrets to keep sensitive values out of your LoT definitions:
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
DEFINE ROUTE SensorDB WITH TYPE POSTGRESQL
ADD SQL_CONFIG
WITH SERVER GET ENV "DB_HOST"
WITH PORT GET ENV "DB_PORT"
WITH DATABASE GET ENV "DB_NAME"
WITH USERNAME GET ENV "DB_USER"
WITH PASSWORD GET SECRET "DB_PASSWORD"
```
See [Environment Variables and Secrets](/latest/mqtt-broker/secrets-and-env) for setup instructions across Windows, Linux, Docker, and Kubernetes.
### Step 2: Store Data
There are two ways to write data into your database. Choose the approach that fits your use case:
The `STORE IN` approach ties a model directly to a database route. Every time an action publishes this model, the record is automatically inserted into the specified table — no EVENT or query needed.
**1. Define a model with STORE IN:**
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
DEFINE MODEL ProductionCount
ADD "f_event_time" WITH TIMESTAMP "UNIX"
ADD STRING "machine_id"
ADD STRING "product_type"
ADD INT "count"
STORE IN "ProductionDB"
WITH TABLE "production_counts"
```
**2. Define the route the model stores into:**
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
DEFINE ROUTE ProductionDB WITH TYPE POSTGRESQL
ADD SQL_CONFIG
WITH SERVER "localhost"
WITH PORT '5432'
WITH DATABASE "factory"
WITH USERNAME "postgres"
WITH PASSWORD "secure_password"
```
**3. Publish the model from an action:**
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
DEFINE ACTION RecordProduction
ON TOPIC "factory/+/production" DO
SET "machine" WITH TOPIC POSITION 2
SET "product" WITH (GET JSON "product_type" IN PAYLOAD AS STRING)
SET "qty" WITH (GET JSON "count" IN PAYLOAD AS INT)
PUBLISH MODEL ProductionCount TO "factory/production/recorded" WITH
machine_id = {machine}
product_type = {product}
count = {qty}
```
Every `PUBLISH MODEL` call automatically inserts a row into the `production_counts` table with all the model fields as columns.
The EVENT approach uses explicit SQL or CLEAN queries triggered by MQTT messages arriving on a source topic. You write the query yourself, giving full control over column mapping and transformations.
**Define an EVENT inside the route:**
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
DEFINE ROUTE SensorDB WITH TYPE POSTGRESQL
ADD SQL_CONFIG
WITH SERVER "postgres.example.com"
WITH PORT '5432'
WITH DATABASE "iot_data"
WITH USERNAME "iot_user"
WITH PASSWORD "secure_password"
ADD EVENT StoreSensorReading
WITH SOURCE_TOPIC "sensors/+/reading"
WITH DESTINATION_TOPIC "db/status"
WITH QUERY "INSERT INTO readings (ts, sensor_id, value) VALUES (NOW(), '{sensor_id}', '{value.json}')"
```
When a message arrives on any topic matching `sensors/+/reading`, the INSERT query runs with placeholders replaced by actual values from the message.
### Step 3: Read Data (SELECT)
EVENTs also support SELECT queries to read data back from the database. Publish a message to the event's `SOURCE_TOPIC` with your query parameters, and the result is published to `DESTINATION_TOPIC`:
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
DEFINE ROUTE ProductionDB WITH TYPE POSTGRESQL
ADD SQL_CONFIG
WITH SERVER "localhost"
WITH PORT '5432'
WITH DATABASE "factory"
WITH USERNAME "postgres"
WITH PASSWORD "secure_password"
ADD EVENT GetLatestCount
WITH QUERY "SELECT MAX(count) as count FROM production_counts WHERE machine_id = '{payload}'"
WITH SOURCE_TOPIC "factory/production/getLatestCount"
WITH DESTINATION_TOPIC "factory/production/latest_count_from_db"
```
To trigger this query, publish the machine ID to the source topic:
```
Topic: factory/production/getLatestCount
Payload: MACHINE-001
```
The query result is published to `factory/production/latest_count_from_db`, where your actions or external clients can consume it.
***
## Common Query Placeholders
These placeholders work across all database routes:
| Placeholder | Description |
| -------------------- | ---------------------------------------------------------- |
| `{payload}` | Raw MQTT payload (useful in SELECT queries as a parameter) |
| `{value}` | Full MQTT payload as string |
| `{value.json}` | Parsed JSON payload |
| `{value.json.field}` | Specific field from JSON |
| `{timestamp}` | Message timestamp |
| `{source_topic}` | Original MQTT topic |
| `{field}` | Field extracted from topic path |
### Extracting Fields from Topics
For topic `sensors/temp001/reading`, use these placeholders:
| Placeholder | Value |
| ---------------- | ------------------------------- |
| `{source_topic}` | `sensors/temp001/reading` |
| `{sensor_id}` | `temp001` (position 2 in topic) |
***
## Query Formats
### SQL Format
For relational databases (PostgreSQL, MySQL, MariaDB, SQL Server, CrateDB, Snowflake):
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
WITH QUERY "INSERT INTO sensor_readings (timestamp, sensor_id, value) VALUES ('{timestamp}', '{sensor_id}', '{value.json}')"
```
### CLEAN Format
For document databases and search engines (MongoDB, OpenSearch, File Storage):
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
WITH QUERY "CLEAN:{collection: readings, document: {sensor_id: {sensor_id}, value: {value.json}, timestamp: {timestamp}}}"
```
The CLEAN format provides simplified JSON syntax without requiring proper escaping.
***
## Common EVENT Parameters
MQTT topic pattern that triggers the query. For INSERT operations, incoming message data is used to populate placeholders. For SELECT operations, the payload can be used as a query parameter via `{payload}`.
MQTT topic to publish query results. Required for SELECT queries so the result can be consumed by actions or external clients. Optional for INSERT queries (publishes operation status).
SQL statement or CLEAN format query with placeholders. Supports both write operations (`INSERT INTO ...`) and read operations (`SELECT ... FROM ...`).
***
## Best Practices
Always enable SSL for database connections. Most database routes support SSL configuration through the config block:
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
WITH USE_SSL "true"
WITH TRUST_SERVER_CERTIFICATE "false"
```
When using certificate-based authentication, store certificate passphrases as encrypted secrets rather than in plain text. See [Environment Variables and Secrets](/latest/mqtt-broker/secrets-and-env#secrets) for details.
Never hardcode passwords or API keys in route definitions. Use `GET SECRET` for sensitive values and `GET ENV` for non-sensitive configuration like hostnames and ports. Create dedicated database users with minimal permissions for each route.
See [Environment Variables and Secrets](/latest/mqtt-broker/secrets-and-env) for full setup and deployment instructions.
Implement data retention policies:
* Index lifecycle management
* Time-based partitioning
* Automatic cleanup jobs
When your data has a consistent schema, use `STORE IN` on a model to automatically insert records every time the model is published. This eliminates the need to write INSERT queries manually:
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
DEFINE MODEL SensorReading
ADD STRING "sensor_id"
ADD DOUBLE "value"
ADD STRING "timestamp" WITH TIMESTAMP "UTC"
STORE IN "SensorDB"
WITH TABLE "sensor_readings"
```
For high-throughput scenarios, configure connection pools where supported.
***
## Next Steps
Choose the database that matches your requirements:
Start with the most popular open-source database.
Store flexible JSON documents.
# PostgreSQL Route
Source: https://docs.coreflux.org/latest/lot-language/routes/data-storage/postgresql
Store MQTT data in PostgreSQL and PostgreSQL-compatible databases with automatic query execution and SSL support
## PostgreSQL Overview
The `POSTGRESQL` route stores MQTT messages in PostgreSQL databases. It supports SSL connections, parameterized queries, and automatic reconnection for reliable data persistence.
Because the route speaks the **PostgreSQL wire protocol**, it also works with any service built on PostgreSQL — you use the same `POSTGRESQL` route type and point `SQL_CONFIG` at that provider's host, port, database, and credentials.
| Platform | Typical use |
| ----------------------------------- | --------------------------------------------------------------------------------------- |
| **PostgreSQL** | Self-hosted, on-premises, or VM deployments |
| **Tiger Data** | Managed Postgres with TimescaleDB (Tiger Cloud) for time-series and IoT workloads |
| **TimescaleDB** | PostgreSQL extension for hypertables and time-series SQL (self-hosted or on Tiger Data) |
| **Amazon RDS / Aurora PostgreSQL** | AWS-managed relational storage |
| **Azure Database for PostgreSQL** | Microsoft Azure managed Postgres |
| **Google Cloud SQL for PostgreSQL** | GCP managed Postgres |
| **Supabase** | Managed Postgres with realtime APIs, often used for app and dashboard backends |
PostgreSQL is ideal for structured data that requires complex queries, joins, and ACID compliance. Use it when you need relational integrity and powerful SQL capabilities.
For **TimescaleDB** or **Tiger Data** (Tiger Cloud), use standard PostgreSQL connection settings — host, port `5432`, database, user, and password from your service dashboard. Create hypertables and time-series schemas in SQL as you would in any Postgres client; the route executes your `QUERY` strings against that database.
## Basic Syntax
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
DEFINE ROUTE SensorDB WITH TYPE POSTGRESQL
ADD SQL_CONFIG
WITH SERVER "postgres.example.com"
WITH PORT '5432'
WITH DATABASE "iot_data"
WITH USERNAME "iot_user"
WITH PASSWORD "secure_password"
ADD EVENT StoreSensorReading
WITH SOURCE_TOPIC "sensors/+/reading"
WITH QUERY "INSERT INTO readings (ts, sensor_id, value) VALUES (NOW(), '{sensor_id}', '{value.json}')"
```
***
## Connection Configuration
### SQL\_CONFIG Parameters
PostgreSQL server hostname or IP address.
Target database name.
Database username.
Database password.
PostgreSQL port. Default: 5432.
Enable SSL connection. Default: false.
Trust server certificate without validation. Default: false.
Connection timeout in seconds. Default: 30.
Command timeout in seconds. Default: 30.
**Never hardcode credentials in production.** Use environment variables and encrypted secrets to keep sensitive values out of your route definitions:
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
WITH SERVER GET ENV "DB_HOST"
WITH USERNAME GET ENV "DB_USER"
WITH PASSWORD GET SECRET "DB_PASSWORD"
```
See [Environment Variables & Secrets](/latest/mqtt-broker/secrets-and-env) for setup and usage.
***
## Event Configuration
### EVENT Parameters
MQTT topic pattern that triggers the INSERT. Supports wildcards.
SQL INSERT statement with placeholders for dynamic values.
MQTT topic to publish query result status.
### Query Placeholders
| Placeholder | Description |
| -------------------- | ------------------------------- |
| `{value.json}` | Full JSON payload |
| `{value.json.field}` | Specific field from JSON |
| `{timestamp}` | Message timestamp |
| `{source_topic}` | Original MQTT topic |
| `{field}` | Field extracted from topic path |
***
## Writing Data
Store temperature readings with timestamps:
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
DEFINE ROUTE TemperatureDB WITH TYPE POSTGRESQL
ADD SQL_CONFIG
WITH SERVER "postgres.example.com"
WITH PORT '5432'
WITH DATABASE "iot_data"
WITH USERNAME "iot_user"
WITH PASSWORD "secure_password"
ADD EVENT StoreTemperature
WITH SOURCE_TOPIC "sensors/+/temperature"
WITH DESTINATION_TOPIC "db/status/temperature"
WITH QUERY "INSERT INTO temperature_readings (recorded_at, sensor_id, value, unit) VALUES (NOW(), '{sensor_id}', {value.json.value}, '{value.json.unit}')"
```
**Input at** `sensors/temp001/temperature`:
```json wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
{"value": 23.5, "unit": "celsius"}
```
Secure connection with SSL:
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
DEFINE ROUTE SecureDB WITH TYPE POSTGRESQL
ADD SQL_CONFIG
WITH SERVER "postgres.cloud.com"
WITH PORT '5432'
WITH DATABASE "production"
WITH USERNAME "app_user"
WITH PASSWORD "secure_password"
WITH USE_SSL "true"
WITH TRUST_SERVER_CERTIFICATE "false"
ADD EVENT StoreEvent
WITH SOURCE_TOPIC "events/#"
WITH QUERY "INSERT INTO events (ts, topic, payload) VALUES (NOW(), '{source_topic}', '{value.json}')"
```
Different tables for different data types:
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
DEFINE ROUTE MultiTableDB WITH TYPE POSTGRESQL
ADD SQL_CONFIG
WITH SERVER "postgres.example.com"
WITH PORT '5432'
WITH DATABASE "iot_data"
WITH USERNAME "iot_user"
WITH PASSWORD "secure_password"
ADD EVENT StoreSensorData
WITH SOURCE_TOPIC "sensors/+/data"
WITH QUERY "INSERT INTO sensor_data (ts, sensor_id, value) VALUES (NOW(), '{sensor_id}', '{value.json}')"
ADD EVENT StoreAlerts
WITH SOURCE_TOPIC "alerts/+"
WITH QUERY "INSERT INTO alerts (ts, alert_type, message) VALUES (NOW(), '{alert_type}', '{value.json}')"
ADD EVENT StoreMetrics
WITH SOURCE_TOPIC "metrics/#"
WITH QUERY "INSERT INTO metrics (ts, topic, data) VALUES (NOW(), '{source_topic}', '{value.json}')"
```
Store full payload in JSONB column:
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
DEFINE ROUTE JSONStorage WITH TYPE POSTGRESQL
ADD SQL_CONFIG
WITH SERVER "postgres.example.com"
WITH PORT '5432'
WITH DATABASE "iot_data"
WITH USERNAME "iot_user"
WITH PASSWORD "secure_password"
ADD EVENT StoreJSON
WITH SOURCE_TOPIC "devices/#"
WITH QUERY "INSERT INTO device_data (ts, topic, payload) VALUES (NOW(), '{source_topic}', '{value.json}'::jsonb)"
```
This allows powerful JSON queries in PostgreSQL:
```sql wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
SELECT * FROM device_data WHERE payload->>'temperature' > '25';
```
Insert a record, or update it if a conflict on the unique key occurs. Useful for device registration or status tables where duplicate messages are possible:
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
DEFINE ROUTE DeviceRegistry WITH TYPE POSTGRESQL
ADD SQL_CONFIG
WITH SERVER "postgres.example.com"
WITH PORT '5432'
WITH DATABASE "iot_data"
WITH USERNAME "iot_user"
WITH PASSWORD "secure_password"
ADD EVENT RegisterDevice
WITH SOURCE_TOPIC "devices/+/registered"
WITH QUERY "INSERT INTO devices (device_id, name, area, active, updated_at) VALUES ('{value.json.device_id}', '{value.json.name}', '{value.json.area}', TRUE, NOW()) ON CONFLICT (device_id) DO UPDATE SET name = EXCLUDED.name, area = EXCLUDED.area, updated_at = NOW()"
```
`ON CONFLICT (device_id) DO UPDATE SET` updates only the changed columns when the device already exists — the row is never duplicated.
Modify existing records when a status change or update arrives:
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
DEFINE ROUTE DeviceRegistry WITH TYPE POSTGRESQL
ADD SQL_CONFIG
WITH SERVER "postgres.example.com"
WITH PORT '5432'
WITH DATABASE "iot_data"
WITH USERNAME "iot_user"
WITH PASSWORD "secure_password"
ADD EVENT UpdateDeviceStatus
WITH SOURCE_TOPIC "devices/+/status"
WITH QUERY "UPDATE devices SET current_status = '{value.json.status}', last_seen = NOW() WHERE device_id = '{value.json.device_id}'"
```
Only rows matching the `WHERE` clause are modified. Unmatched messages produce no change — no error is raised.
Remove records when a decommission or cleanup event arrives:
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
DEFINE ROUTE DeviceRegistry WITH TYPE POSTGRESQL
ADD SQL_CONFIG
WITH SERVER "postgres.example.com"
WITH PORT '5432'
WITH DATABASE "iot_data"
WITH USERNAME "iot_user"
WITH PASSWORD "secure_password"
ADD EVENT DecommissionDevice
WITH SOURCE_TOPIC "devices/+/decommissioned"
WITH QUERY "DELETE FROM devices WHERE device_id = '{value.json.device_id}' AND active = FALSE"
```
The `AND active = FALSE` guard prevents accidental deletes if a message arrives before the device is properly deactivated.
**Alternative: STORE IN with Models** — Instead of writing EVENT queries, you can bind a [model](/latest/lot-language/models/overview) directly to this route. Every `PUBLISH MODEL` call automatically inserts a row — no query needed:
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
DEFINE MODEL SensorReading
ADD STRING "sensor_id"
ADD DOUBLE "value"
STORE IN "SensorDB"
WITH TABLE "sensor_readings"
```
See [Data Storage Overview](./overview#with-models-store-in) for the full STORE IN workflow.
***
## Reading Data
EVENTs also support SELECT queries to read data back from the database. Publish a message to the event's `SOURCE_TOPIC`, and the query result is published to `DESTINATION_TOPIC`:
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
DEFINE ROUTE SensorDB WITH TYPE POSTGRESQL
ADD SQL_CONFIG
WITH SERVER "postgres.example.com"
WITH PORT '5432'
WITH DATABASE "iot_data"
WITH USERNAME "iot_user"
WITH PASSWORD "secure_password"
ADD EVENT GetLatestReading
WITH SOURCE_TOPIC "db/query/latest"
WITH DESTINATION_TOPIC "db/result/latest"
WITH QUERY "SELECT sensor_id, value FROM readings WHERE sensor_id = '{payload}' ORDER BY ts DESC LIMIT 1"
```
To trigger this query, publish the sensor ID to the source topic:
```
Topic: db/query/latest
Payload: temp001
```
The query result is published to `db/result/latest`, where your actions or external clients can consume it.
### Timed Polling
EVENTs can also run SELECT queries on a schedule instead of waiting for an MQTT trigger. Use `WITH EVERY` to publish fresh data at a fixed interval:
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
DEFINE ROUTE DeviceRegistry WITH TYPE POSTGRESQL
ADD SQL_CONFIG
WITH SERVER "postgres.example.com"
WITH PORT '5432'
WITH DATABASE "iot_data"
WITH USERNAME "iot_user"
WITH PASSWORD "secure_password"
ADD EVENT PollActiveDevices
WITH EVERY 10 SECONDS
WITH DESTINATION_TOPIC "dashboard/devices/active"
WITH QUERY "SELECT device_id, name, current_status, last_seen FROM devices WHERE active = TRUE ORDER BY name"
```
The query result is published to `dashboard/devices/active` every 10 seconds — no trigger message needed.
***
## Table Schema Examples
Create tables to match your data:
```sql wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
-- Basic sensor readings
CREATE TABLE sensor_readings (
id SERIAL PRIMARY KEY,
recorded_at TIMESTAMP DEFAULT NOW(),
sensor_id VARCHAR(50),
value DOUBLE PRECISION,
unit VARCHAR(20)
);
-- Full JSON storage
CREATE TABLE device_data (
id SERIAL PRIMARY KEY,
ts TIMESTAMP DEFAULT NOW(),
topic VARCHAR(255),
payload JSONB
);
-- Alerts table
CREATE TABLE alerts (
id SERIAL PRIMARY KEY,
ts TIMESTAMP DEFAULT NOW(),
alert_type VARCHAR(50),
message TEXT
);
```
***
## Troubleshooting
* Verify SERVER and PORT are correct
* Check PostgreSQL is running and accepting connections
* Verify firewall allows connections on port 5432
* Check `pg_hba.conf` allows your IP address
* Verify USERNAME and PASSWORD are correct
* Check user has permissions on the DATABASE
* Verify authentication method in `pg_hba.conf`
* Ensure USE\_SSL matches server requirements
* For self-signed certs, set TRUST\_SERVER\_CERTIFICATE "true"
* Verify SSL is enabled on PostgreSQL server
* Check placeholder names match topic structure
* Verify table and column names exist
* Test query manually in psql first
* Ensure proper escaping for special characters
* Increase CONNECTION\_TIMEOUT for slow networks
* Increase COMMAND\_TIMEOUT for complex queries
* Check server load and query performance
***
## Next Steps
Compare all storage options.
Configure MySQL database storage.
# Snowflake Route
Source: https://docs.coreflux.org/latest/lot-language/routes/data-storage/snowflake
Store MQTT data in Snowflake cloud data warehouse for large-scale analytics
## Snowflake Overview
The `SNOWFLAKE` route stores MQTT messages in Snowflake, a cloud-native data warehouse. It supports high-volume data ingestion with automatic scaling and powerful SQL analytics.
Snowflake is ideal for large-scale analytics, data lakes, and enterprise reporting. Use it when you need to analyze IoT data alongside other business data in the cloud.
## Basic Syntax
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
DEFINE ROUTE SensorWarehouse WITH TYPE SNOWFLAKE
ADD SNOWFLAKE_CONFIG
WITH CONNECTION_STRING "account=myaccount;user=myuser;password=mypassword"
WITH WAREHOUSE "COMPUTE_WH"
WITH DATABASE "IOT_DATA"
WITH SCHEMA "SENSORS"
ADD EVENT StoreSensorReading
WITH SOURCE_TOPIC "sensors/+/reading"
WITH QUERY "INSERT INTO SENSOR_READINGS (TIMESTAMP, SENSOR_ID, VALUE) VALUES (CURRENT_TIMESTAMP(), '{sensor_id}', PARSE_JSON('{value.json}'))"
```
***
## Connection Configuration
### SNOWFLAKE\_CONFIG Parameters
Snowflake connection string with account, user, and password.
Snowflake compute warehouse name.
Target database name.
Target schema name. Default: PUBLIC.
Snowflake user role.
Query timeout in seconds. Default: 60.
Connection pool size. Default: 10.
**Never hardcode credentials in production.** Use environment variables and encrypted secrets to keep sensitive values out of your route definitions:
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
WITH CONNECTION_STRING GET SECRET "SNOWFLAKE_CONNECTION_STRING"
```
See [Environment Variables & Secrets](/latest/mqtt-broker/secrets-and-env) for setup and usage.
***
## Writing Data
Store sensor data in Snowflake:
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
DEFINE ROUTE SensorWarehouse WITH TYPE SNOWFLAKE
ADD SNOWFLAKE_CONFIG
WITH CONNECTION_STRING "account=myaccount;user=iot_user;password=secure_password"
WITH WAREHOUSE "IOT_WH"
WITH DATABASE "IOT_DATA"
WITH SCHEMA "SENSORS"
WITH ROLE "IOT_WRITER"
ADD EVENT StoreReading
WITH SOURCE_TOPIC "sensors/+/reading"
WITH DESTINATION_TOPIC "snowflake/status"
WITH QUERY "INSERT INTO SENSOR_READINGS (TIMESTAMP, SENSOR_ID, VALUE) VALUES (CURRENT_TIMESTAMP(), '{sensor_id}', PARSE_JSON('{value.json}'))"
```
Optimized for high throughput:
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
DEFINE ROUTE HighVolumeWarehouse WITH TYPE SNOWFLAKE
ADD SNOWFLAKE_CONFIG
WITH CONNECTION_STRING "account=myaccount;user=iot_user;password=secure_password"
WITH WAREHOUSE "IOT_WH"
WITH DATABASE "IOT_DATA"
WITH SCHEMA "EVENTS"
WITH QUERY_TIMEOUT '60'
WITH POOL_SIZE '20'
ADD EVENT StoreEvents
WITH SOURCE_TOPIC "events/#"
WITH QUERY "INSERT INTO EVENT_LOG (TIMESTAMP, TOPIC, PAYLOAD) VALUES (CURRENT_TIMESTAMP(), '{source_topic}', PARSE_JSON('{value.json}'))"
```
Route to different tables:
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
DEFINE ROUTE MultiTableWarehouse WITH TYPE SNOWFLAKE
ADD SNOWFLAKE_CONFIG
WITH CONNECTION_STRING "account=myaccount;user=iot_user;password=secure_password"
WITH WAREHOUSE "IOT_WH"
WITH DATABASE "IOT_DATA"
WITH SCHEMA "PUBLIC"
ADD EVENT StoreSensors
WITH SOURCE_TOPIC "sensors/#"
WITH QUERY "INSERT INTO SENSORS (TS, TOPIC, DATA) VALUES (CURRENT_TIMESTAMP(), '{source_topic}', PARSE_JSON('{value.json}'))"
ADD EVENT StoreAlerts
WITH SOURCE_TOPIC "alerts/#"
WITH QUERY "INSERT INTO ALERTS (TS, TOPIC, DATA) VALUES (CURRENT_TIMESTAMP(), '{source_topic}', PARSE_JSON('{value.json}'))"
```
Snowflake uses `MERGE INTO` to insert or update depending on whether a matching row exists:
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
DEFINE ROUTE DeviceRegistry WITH TYPE SNOWFLAKE
ADD SNOWFLAKE_CONFIG
WITH CONNECTION_STRING "account=myaccount;user=iot_user;password=secure_password"
WITH WAREHOUSE "IOT_WH"
WITH DATABASE "IOT_DATA"
WITH SCHEMA "DEVICES"
ADD EVENT RegisterDevice
WITH SOURCE_TOPIC "devices/+/registered"
WITH QUERY "MERGE INTO DEVICES AS target USING (SELECT '{value.json.device_id}' AS device_id, '{value.json.name}' AS name, '{value.json.area}' AS area) AS source ON target.DEVICE_ID = source.device_id WHEN MATCHED THEN UPDATE SET NAME = source.name, AREA = source.area, UPDATED_AT = CURRENT_TIMESTAMP() WHEN NOT MATCHED THEN INSERT (DEVICE_ID, NAME, AREA, ACTIVE, UPDATED_AT) VALUES (source.device_id, source.name, source.area, TRUE, CURRENT_TIMESTAMP())"
```
Modify existing records when a status change arrives:
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
DEFINE ROUTE DeviceRegistry WITH TYPE SNOWFLAKE
ADD SNOWFLAKE_CONFIG
WITH CONNECTION_STRING "account=myaccount;user=iot_user;password=secure_password"
WITH WAREHOUSE "IOT_WH"
WITH DATABASE "IOT_DATA"
WITH SCHEMA "DEVICES"
ADD EVENT UpdateDeviceStatus
WITH SOURCE_TOPIC "devices/+/status"
WITH QUERY "UPDATE DEVICES SET CURRENT_STATUS = '{value.json.status}', LAST_SEEN = CURRENT_TIMESTAMP() WHERE DEVICE_ID = '{value.json.device_id}'"
```
Remove records on a decommission or cleanup event:
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
DEFINE ROUTE DeviceRegistry WITH TYPE SNOWFLAKE
ADD SNOWFLAKE_CONFIG
WITH CONNECTION_STRING "account=myaccount;user=iot_user;password=secure_password"
WITH WAREHOUSE "IOT_WH"
WITH DATABASE "IOT_DATA"
WITH SCHEMA "DEVICES"
ADD EVENT DecommissionDevice
WITH SOURCE_TOPIC "devices/+/decommissioned"
WITH QUERY "DELETE FROM DEVICES WHERE DEVICE_ID = '{value.json.device_id}' AND ACTIVE = FALSE"
```
**Alternative: STORE IN with Models** — Instead of writing EVENT queries, you can bind a [model](/latest/lot-language/models/overview) directly to this route. Every `PUBLISH MODEL` call automatically inserts a row — no query needed:
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
DEFINE MODEL SensorReading
ADD STRING "sensor_id"
ADD DOUBLE "value"
STORE IN "SensorWarehouse"
WITH TABLE "SENSOR_READINGS"
```
See [Data Storage Overview](./overview#with-models-store-in) for the full STORE IN workflow.
***
## Reading Data
EVENTs also support SELECT queries to read data back from the database. Publish a message to the event's `SOURCE_TOPIC`, and the query result is published to `DESTINATION_TOPIC`:
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
DEFINE ROUTE SensorWarehouse WITH TYPE SNOWFLAKE
ADD SNOWFLAKE_CONFIG
WITH CONNECTION_STRING "account=myaccount;user=myuser;password=mypassword"
WITH WAREHOUSE "COMPUTE_WH"
WITH DATABASE "IOT_DATA"
WITH SCHEMA "SENSORS"
ADD EVENT GetLatestReading
WITH SOURCE_TOPIC "db/query/latest"
WITH DESTINATION_TOPIC "db/result/latest"
WITH QUERY "SELECT SENSOR_ID, VALUE FROM SENSOR_READINGS WHERE SENSOR_ID = '{payload}' ORDER BY TIMESTAMP DESC LIMIT 1"
```
To trigger this query, publish the sensor ID to the source topic:
```
Topic: db/query/latest
Payload: temp001
```
The query result is published to `db/result/latest`, where your actions or external clients can consume it.
### Timed Polling
EVENTs can also run SELECT queries on a fixed schedule using `WITH EVERY`:
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
DEFINE ROUTE DeviceRegistry WITH TYPE SNOWFLAKE
ADD SNOWFLAKE_CONFIG
WITH CONNECTION_STRING "account=myaccount;user=iot_user;password=secure_password"
WITH WAREHOUSE "IOT_WH"
WITH DATABASE "IOT_DATA"
WITH SCHEMA "DEVICES"
ADD EVENT PollActiveDevices
WITH EVERY 30 SECONDS
WITH DESTINATION_TOPIC "dashboard/devices/active"
WITH QUERY "SELECT DEVICE_ID, NAME, CURRENT_STATUS, LAST_SEEN FROM DEVICES WHERE ACTIVE = TRUE ORDER BY NAME"
```
***
## Table Schema Examples
```sql wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
-- Sensor readings with VARIANT for JSON
CREATE TABLE SENSOR_READINGS (
ID NUMBER AUTOINCREMENT,
TIMESTAMP TIMESTAMP_NTZ DEFAULT CURRENT_TIMESTAMP(),
SENSOR_ID VARCHAR(50),
VALUE VARIANT
);
-- Event log
CREATE TABLE EVENT_LOG (
ID NUMBER AUTOINCREMENT,
TIMESTAMP TIMESTAMP_NTZ DEFAULT CURRENT_TIMESTAMP(),
TOPIC VARCHAR(255),
PAYLOAD VARIANT
);
```
***
## Connection String Format
```
account=;user=;password=
```
| Component | Description |
| ---------- | -------------------------------------------------------- |
| `account` | Snowflake account identifier (e.g., `xy12345.us-east-1`) |
| `user` | Snowflake username |
| `password` | User password |
***
## Troubleshooting
* Verify account identifier is correct
* Check username and password
* Ensure warehouse exists and is running
* Verify network allows Snowflake connections
* Verify ROLE has INSERT permissions
* Check user has access to WAREHOUSE
* Ensure user can access DATABASE and SCHEMA
* Increase QUERY\_TIMEOUT value
* Check warehouse size is adequate
* Verify warehouse is not suspended
***
## Next Steps
Compare all storage options.
Configure local file storage.
# SQL Server Route
Source: https://docs.coreflux.org/latest/lot-language/routes/data-storage/sqlserver
Store MQTT data in Microsoft SQL Server with automatic query execution and enterprise features
## SQL Server Overview
The `SQLSERVER` route stores MQTT messages in Microsoft SQL Server databases. It supports Windows and SQL authentication, SSL connections, and enterprise features.
SQL Server is ideal for enterprise environments, especially those integrated with Microsoft ecosystems. It offers advanced features like Always On availability and native JSON support.
## Basic Syntax
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
DEFINE ROUTE SensorDB WITH TYPE SQLSERVER
ADD SQL_CONFIG
WITH SERVER "sqlserver.example.com"
WITH PORT '1433'
WITH DATABASE "IoTData"
WITH USERNAME "iot_user"
WITH PASSWORD "secure_password"
ADD EVENT StoreSensorReading
WITH SOURCE_TOPIC "sensors/+/reading"
WITH QUERY "INSERT INTO Readings (RecordedAt, SensorId, Value) VALUES (GETDATE(), '{sensor_id}', '{value.json}')"
```
***
## Connection Configuration
### SQL\_CONFIG Parameters
SQL Server hostname or IP address. Can include instance name (e.g., `server\instance`).
Target database name.
SQL Server username.
SQL Server password.
SQL Server port. Default: 1433.
Enable SSL connection. Default: false.
Trust server certificate without validation. Default: false.
**Never hardcode credentials in production.** Use environment variables and encrypted secrets to keep sensitive values out of your route definitions:
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
WITH SERVER GET ENV "DB_HOST"
WITH USERNAME GET ENV "DB_USER"
WITH PASSWORD GET SECRET "DB_PASSWORD"
```
See [Environment Variables & Secrets](/latest/mqtt-broker/secrets-and-env) for setup and usage.
***
## Writing Data
Store sensor data in SQL Server:
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
DEFINE ROUTE SensorStorage WITH TYPE SQLSERVER
ADD SQL_CONFIG
WITH SERVER "sqlserver.example.com"
WITH PORT '1433'
WITH DATABASE "IoTData"
WITH USERNAME "iot_user"
WITH PASSWORD "secure_password"
ADD EVENT StoreReading
WITH SOURCE_TOPIC "sensors/+/reading"
WITH QUERY "INSERT INTO SensorReadings (Timestamp, SensorId, Value) VALUES (GETDATE(), '{sensor_id}', '{value.json}')"
```
Secure connection with trusted certificate:
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
DEFINE ROUTE SecureStorage WITH TYPE SQLSERVER
ADD SQL_CONFIG
WITH SERVER "sqlserver.cloud.com"
WITH PORT '1433'
WITH DATABASE "Production"
WITH USERNAME "app_user"
WITH PASSWORD "secure_password"
WITH USE_SSL "true"
WITH TRUST_SERVER_CERTIFICATE "true"
ADD EVENT StoreEvent
WITH SOURCE_TOPIC "events/#"
WITH QUERY "INSERT INTO Events (Timestamp, Topic, Payload) VALUES (GETDATE(), '{source_topic}', '{value.json}')"
```
Connect to a named SQL Server instance:
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
DEFINE ROUTE InstanceStorage WITH TYPE SQLSERVER
ADD SQL_CONFIG
WITH SERVER "server.example.com\\MSSQLSERVER"
WITH DATABASE "IoTData"
WITH USERNAME "iot_user"
WITH PASSWORD "secure_password"
ADD EVENT StoreData
WITH SOURCE_TOPIC "data/#"
WITH QUERY "INSERT INTO DataLog (Timestamp, Topic, Data) VALUES (GETDATE(), '{source_topic}', '{value.json}')"
```
SQL Server uses the `MERGE` statement for upsert logic — insert a row if it doesn't exist, update it if it does:
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
DEFINE ROUTE DeviceRegistry WITH TYPE SQLSERVER
ADD SQL_CONFIG
WITH SERVER "sqlserver.example.com"
WITH PORT '1433'
WITH DATABASE "IoTData"
WITH USERNAME "iot_user"
WITH PASSWORD "secure_password"
ADD EVENT RegisterDevice
WITH SOURCE_TOPIC "devices/+/registered"
WITH QUERY "MERGE Devices AS target USING (SELECT '{value.json.device_id}' AS device_id, '{value.json.name}' AS name, '{value.json.area}' AS area) AS source ON (target.DeviceId = source.device_id) WHEN MATCHED THEN UPDATE SET Name = source.name, Area = source.area, UpdatedAt = GETDATE() WHEN NOT MATCHED THEN INSERT (DeviceId, Name, Area, Active, UpdatedAt) VALUES (source.device_id, source.name, source.area, 1, GETDATE());"
```
Modify existing records when a status change arrives:
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
DEFINE ROUTE DeviceRegistry WITH TYPE SQLSERVER
ADD SQL_CONFIG
WITH SERVER "sqlserver.example.com"
WITH PORT '1433'
WITH DATABASE "IoTData"
WITH USERNAME "iot_user"
WITH PASSWORD "secure_password"
ADD EVENT UpdateDeviceStatus
WITH SOURCE_TOPIC "devices/+/status"
WITH QUERY "UPDATE Devices SET CurrentStatus = '{value.json.status}', LastSeen = GETDATE() WHERE DeviceId = '{value.json.device_id}'"
```
Remove records on a decommission or cleanup event:
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
DEFINE ROUTE DeviceRegistry WITH TYPE SQLSERVER
ADD SQL_CONFIG
WITH SERVER "sqlserver.example.com"
WITH PORT '1433'
WITH DATABASE "IoTData"
WITH USERNAME "iot_user"
WITH PASSWORD "secure_password"
ADD EVENT DecommissionDevice
WITH SOURCE_TOPIC "devices/+/decommissioned"
WITH QUERY "DELETE FROM Devices WHERE DeviceId = '{value.json.device_id}' AND Active = 0"
```
**Alternative: STORE IN with Models** — Instead of writing EVENT queries, you can bind a [model](/latest/lot-language/models/overview) directly to this route. Every `PUBLISH MODEL` call automatically inserts a row — no query needed:
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
DEFINE MODEL SensorReading
ADD STRING "sensor_id"
ADD DOUBLE "value"
STORE IN "SensorStorage"
WITH TABLE "SensorReadings"
```
See [Data Storage Overview](./overview#with-models-store-in) for the full STORE IN workflow.
***
## Reading Data
EVENTs also support SELECT queries to read data back from the database. Publish a message to the event's `SOURCE_TOPIC`, and the query result is published to `DESTINATION_TOPIC`:
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
DEFINE ROUTE SensorDB WITH TYPE SQLSERVER
ADD SQL_CONFIG
WITH SERVER "sqlserver.example.com"
WITH PORT '1433'
WITH DATABASE "IoTData"
WITH USERNAME "iot_user"
WITH PASSWORD "secure_password"
ADD EVENT GetLatestReading
WITH SOURCE_TOPIC "db/query/latest"
WITH DESTINATION_TOPIC "db/result/latest"
WITH QUERY "SELECT TOP 1 SensorId, Value FROM Readings WHERE SensorId = '{payload}' ORDER BY RecordedAt DESC"
```
To trigger this query, publish the sensor ID to the source topic:
```
Topic: db/query/latest
Payload: temp001
```
The query result is published to `db/result/latest`, where your actions or external clients can consume it.
### Timed Polling
EVENTs can also run SELECT queries on a fixed schedule using `WITH EVERY`:
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
DEFINE ROUTE DeviceRegistry WITH TYPE SQLSERVER
ADD SQL_CONFIG
WITH SERVER "sqlserver.example.com"
WITH PORT '1433'
WITH DATABASE "IoTData"
WITH USERNAME "iot_user"
WITH PASSWORD "secure_password"
ADD EVENT PollActiveDevices
WITH EVERY 10 SECONDS
WITH DESTINATION_TOPIC "dashboard/devices/active"
WITH QUERY "SELECT DeviceId, Name, CurrentStatus, LastSeen FROM Devices WHERE Active = 1 ORDER BY Name"
```
***
## Table Schema Examples
```sql wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
-- Basic sensor readings
CREATE TABLE SensorReadings (
Id INT IDENTITY(1,1) PRIMARY KEY,
Timestamp DATETIME2 DEFAULT GETDATE(),
SensorId NVARCHAR(50),
Value FLOAT
);
-- JSON storage (SQL Server 2016+)
CREATE TABLE Events (
Id INT IDENTITY(1,1) PRIMARY KEY,
Timestamp DATETIME2 DEFAULT GETDATE(),
Topic NVARCHAR(255),
Payload NVARCHAR(MAX)
);
```
***
## Troubleshooting
* Verify SERVER and PORT are correct
* Check SQL Server is running and accepting TCP connections
* Enable TCP/IP in SQL Server Configuration Manager
* Verify firewall allows connections on port 1433
* Verify USERNAME and PASSWORD are correct
* Ensure SQL Server authentication is enabled (mixed mode)
* Check user has permissions on the DATABASE
* Set TRUST\_SERVER\_CERTIFICATE "true" for self-signed certs
* Verify SSL is configured on SQL Server
***
## Next Steps
Compare all storage options.
Configure MongoDB document storage.
# Route Examples
Source: https://docs.coreflux.org/latest/lot-language/routes/examples
Complete examples of LoT route configurations for common integration scenarios
## Examples Index
Find the right example for your use case:
| Example | Route Type | Key Features |
| ------------------------------------------------------ | ------------ | ----------------------- |
| [Edge-to-Cloud Sync](#edge-to-cloud-sensor-sync) | MQTT\_BRIDGE | TLS, outbound sync |
| [Cloud Commands](#cloud-command-reception) | MQTT\_BRIDGE | Inbound direction |
| [Multi-Site Network](#multi-site-synchronization) | MQTT\_BRIDGE | Multiple mappings |
| [MongoDB Sensor Storage](#mongodb-sensor-storage) | MONGODB | CLEAN query format |
| [REST API Integration](#rest-api-integration) | REST\_API | HTTP requests from MQTT |
| [Critical Alerts Email](#critical-alert-notifications) | EMAIL | Template placeholders |
| [OpenSearch Analytics](#opensearch-sensor-archive) | OPENSEARCH | STORE keyword |
| [Complete Integration](#complete-integration-example) | Multiple | Combined setup |
***
## MQTT Bridge Examples
### Edge-to-Cloud Sensor Sync
This example demonstrates synchronizing sensor data from a local edge device to a cloud broker. The `out` direction ensures data flows from your local broker to the cloud, while TLS encryption secures the connection:
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
DEFINE ROUTE EdgeToCloudSync WITH TYPE MQTT_BRIDGE
ADD SOURCE_CONFIG
WITH BROKER SELF
ADD DESTINATION_CONFIG
WITH BROKER_ADDRESS "iot.cloudprovider.com"
WITH BROKER_PORT '8883'
WITH CLIENT_ID "EdgeDevice-Factory1"
WITH USERNAME "edge_device_001"
WITH PASSWORD "secure_cloud_password"
WITH USE_TLS true
ADD MAPPING allSensors
WITH SOURCE_TOPIC "local/sensors/#"
WITH DESTINATION_TOPIC "factory1/edge/sensors/#"
WITH DIRECTION "out"
ADD MAPPING equipmentStatus
WITH SOURCE_TOPIC "local/equipment/+/status"
WITH DESTINATION_TOPIC "factory1/edge/equipment/+/status"
WITH DIRECTION "out"
```
### Cloud Command Reception
Receive commands from cloud systems and distribute them to local devices. The `in` direction means data flows from the remote broker to your local broker:
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
DEFINE ROUTE CloudCommands WITH TYPE MQTT_BRIDGE
ADD SOURCE_CONFIG
WITH BROKER SELF
ADD DESTINATION_CONFIG
WITH BROKER_ADDRESS "iot.cloudprovider.com"
WITH BROKER_PORT '8883'
WITH CLIENT_ID "EdgeDevice-Factory1-Commands"
WITH USE_TLS true
ADD MAPPING deviceCommands
WITH SOURCE_TOPIC "local/commands/devices/+"
WITH DESTINATION_TOPIC "factory1/commands/devices/+"
WITH DIRECTION "in"
ADD MAPPING configUpdates
WITH SOURCE_TOPIC "local/config/#"
WITH DESTINATION_TOPIC "factory1/config/#"
WITH DIRECTION "in"
```
### Multi-Site Synchronization
Connect multiple factory locations to a central broker. This pattern enables enterprise-wide visibility while keeping local operations independent:
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
DEFINE ROUTE FactoryNetwork WITH TYPE MQTT_BRIDGE
ADD SOURCE_CONFIG
WITH BROKER SELF
ADD DESTINATION_CONFIG
WITH BROKER_ADDRESS "central-broker.company.com"
WITH BROKER_PORT '8883'
WITH CLIENT_ID "Factory-Chicago"
WITH USE_TLS true
ADD MAPPING productionData
WITH SOURCE_TOPIC "production/#"
WITH DESTINATION_TOPIC "chicago/production/#"
WITH DIRECTION "out"
ADD MAPPING qualityData
WITH SOURCE_TOPIC "quality/#"
WITH DESTINATION_TOPIC "chicago/quality/#"
WITH DIRECTION "out"
ADD MAPPING centralCommands
WITH SOURCE_TOPIC "local/commands/#"
WITH DESTINATION_TOPIC "chicago/commands/#"
WITH DIRECTION "in"
```
***
## MongoDB Examples
### MongoDB Sensor Storage
Store sensor readings in MongoDB using the CLEAN query format. This example captures all sensor readings and stores them with metadata including the original topic and timestamp:
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
DEFINE ROUTE SensorStorage WITH TYPE MONGODB
ADD MONGODB_CONFIG
WITH CONNECTION_STRING "mongodb://user:password@mongodb.example.com:27017/iot?authSource=admin"
WITH DATABASE "iot"
ADD EVENT StoreSensorReading
WITH QUERY "CLEAN:{collection: sensor_readings, document: {sensor_id: {sensor_id}, value: {value.json}, topic: {source_topic}, timestamp: {timestamp}}}"
WITH SOURCE_TOPIC "sensors/+/reading"
```
### MongoDB Event Logging
Create an event log collection for system events:
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
DEFINE ROUTE EventLogger WITH TYPE MONGODB
ADD MONGODB_CONFIG
WITH CONNECTION_STRING "mongodb://localhost:27017/logs"
WITH DATABASE "logs"
ADD EVENT LogSystemEvent
WITH QUERY "CLEAN:{collection: system_events, document: {event_type: {event_type}, source: {source_topic}, data: {value.json}, created_at: {timestamp}}}"
WITH SOURCE_TOPIC "events/+/+"
```
***
## REST API Examples
### REST API Integration
Connect your MQTT broker to external REST APIs. This example calls a weather API when a request arrives on the trigger topic:
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
DEFINE ROUTE WeatherService WITH TYPE REST_API
ADD REST_API_CONFIG
WITH ENABLE_CLIENT "true"
WITH BASE_ADDRESS "https://api.openweathermap.org"
ADD MAPPING GetWeather
WITH SOURCE_TOPIC "weather/request"
WITH DESTINATION_TOPIC "weather/response"
WITH QUERY '{
"method": "GET",
"endpoint": "/data/2.5/weather",
"headers": {
"Content-Type": "application/json"
}
}'
```
### REST API with Authentication
Call authenticated APIs with headers extracted from the MQTT payload:
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
DEFINE ROUTE NotificationService WITH TYPE REST_API
ADD REST_API_CONFIG
WITH ENABLE_CLIENT "true"
WITH BASE_ADDRESS "https://api.notification-service.com"
ADD MAPPING SendNotification
WITH SOURCE_TOPIC "notifications/send"
WITH DESTINATION_TOPIC "notifications/result"
WITH QUERY '{
"method": "POST",
"endpoint": "/v1/notify",
"headers": {
"Content-Type": "application/json",
"Authorization": "Bearer {api_key}"
},
"content": "{payload.json}"
}'
```
***
## Email Notification Examples
### Critical Alert Notifications
Send emails for critical system alerts. Template placeholders like `{value.json.equipment_id}` are replaced with values from the MQTT payload:
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
DEFINE ROUTE CriticalAlerts WITH TYPE EMAIL
ADD MQTT_CONFIG
WITH BROKER SELF
ADD SMTP_CONFIG
WITH HOST "smtp.gmail.com"
WITH PORT '587'
WITH USERNAME "alerts@mycompany.com"
WITH PASSWORD "gmail-app-password"
WITH USE_TLS true
ADD MAPPING criticalEquipment
WITH SOURCE_TOPIC "alerts/critical/equipment/+"
WITH TEMPLATE_PATH "/templates/equipment_alert.html"
WITH SUBJECT "CRITICAL: Equipment {value.json.equipment_id} - {value.json.alert_type}"
WITH RECIPIENT "ops-team@mycompany.com"
ADD MAPPING criticalSafety
WITH SOURCE_TOPIC "alerts/critical/safety/+"
WITH TEMPLATE_PATH "/templates/safety_alert.html"
WITH SUBJECT "SAFETY ALERT: {value.json.location}"
WITH RECIPIENT "safety-team@mycompany.com"
```
### Dynamic User Notifications
Send to recipients specified in the MQTT payload using `{value.json.email}`:
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
DEFINE ROUTE UserNotifications WITH TYPE EMAIL
ADD MQTT_CONFIG
WITH BROKER SELF
ADD SMTP_CONFIG
WITH HOST "smtp.office365.com"
WITH PORT '587'
WITH USERNAME "noreply@company.com"
WITH PASSWORD "office365-password"
WITH USE_TLS true
ADD MAPPING userAlert
WITH SOURCE_TOPIC "users/+/notifications"
WITH TEMPLATE_PATH "/templates/user_notification.html"
WITH SUBJECT "Hello {value.json.username}, {value.json.notification_type}"
WITH RECIPIENT "{value.json.email}"
```
***
## Data Storage Examples
### OpenSearch Sensor Archive
Index all sensor data for real-time analytics and historical queries:
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
DEFINE ROUTE SensorArchive WITH TYPE OPENSEARCH
ADD OPENSEARCH_CONFIG
WITH BASE_URL "https://opensearch.company.com:9200"
WITH USERNAME "sensor_writer"
WITH PASSWORD "opensearch_password"
WITH USE_SSL true
ADD EVENT IndexSensorReading
WITH QUERY "CLEAN:{index: sensor-readings, body: {@timestamp: {timestamp}, sensor_id: {sensor_id}, data: {value.json}}}"
WITH SOURCE_TOPIC "sensors/+/data"
```
### Combined Bridge and Storage
Bridge data from a remote broker and simultaneously store it in OpenSearch using the `STORE` keyword:
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
DEFINE ROUTE ProcessAndArchive WITH TYPE MQTT_BRIDGE
ADD SOURCE_CONFIG
WITH BROKER_ADDRESS "factory-broker.local"
WITH BROKER_PORT '1883'
WITH CLIENT_ID "ArchiveBridge"
ADD DESTINATION_CONFIG
WITH BROKER SELF
ADD MAPPING sensorData
WITH SOURCE_TOPIC "factory/sensors/+/data"
WITH DESTINATION_TOPIC "archived/sensors/+/data"
WITH DIRECTION "in"
STORE SensorArchive WITH INDEX_NAME "factory_sensors"
```
***
## Complete Integration Example
This comprehensive example combines multiple route types into a complete IoT integration setup. It demonstrates how different routes work together:
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
// 1. MongoDB storage for historical data
DEFINE ROUTE DataWarehouse WITH TYPE MONGODB
ADD MONGODB_CONFIG
WITH CONNECTION_STRING "mongodb://user:pass@mongodb.company.com:27017/iot?ssl=true"
WITH DATABASE "iot"
ADD EVENT StoreProduction
WITH QUERY "CLEAN:{collection: production_data, document: {timestamp: {timestamp}, data: {value.json}}}"
WITH SOURCE_TOPIC "production/#"
// 2. Cloud synchronization for remote monitoring
DEFINE ROUTE CloudSync WITH TYPE MQTT_BRIDGE
ADD SOURCE_CONFIG
WITH BROKER SELF
ADD DESTINATION_CONFIG
WITH BROKER_ADDRESS "iot.cloud.com"
WITH BROKER_PORT '8883'
WITH CLIENT_ID "Factory-Main"
WITH USE_TLS true
ADD MAPPING allData
WITH SOURCE_TOPIC "production/#"
WITH DESTINATION_TOPIC "factory-main/production/#"
WITH DIRECTION "out"
// 3. REST API for external integrations
DEFINE ROUTE ERPIntegration WITH TYPE REST_API
ADD REST_API_CONFIG
WITH ENABLE_CLIENT "true"
WITH BASE_ADDRESS "https://erp.company.com/api"
ADD MAPPING UpdateInventory
WITH SOURCE_TOPIC "production/completed"
WITH DESTINATION_TOPIC "erp/response"
WITH QUERY '{"method": "POST", "endpoint": "/inventory/update", "content": "{payload.json}"}'
// 4. Email alerts for critical events
DEFINE ROUTE CriticalAlerts WITH TYPE EMAIL
ADD MQTT_CONFIG
WITH BROKER SELF
ADD SMTP_CONFIG
WITH HOST "smtp.company.com"
WITH PORT '587'
WITH USERNAME "alerts@company.com"
WITH PASSWORD "email_password"
WITH USE_TLS true
ADD MAPPING critical
WITH SOURCE_TOPIC "alerts/critical/#"
WITH TEMPLATE_PATH "/templates/critical.html"
WITH SUBJECT "CRITICAL: {value.json.type}"
WITH RECIPIENT "ops@company.com"
```
***
## Pattern Summary
Choose the right route pattern for your scenario:
| Scenario | Route Type | Key Configuration |
| ------------------ | ------------ | ----------------------- |
| Edge to Cloud | MQTT\_BRIDGE | `WITH DIRECTION "out"` |
| Cloud Commands | MQTT\_BRIDGE | `WITH DIRECTION "in"` |
| Bidirectional Sync | MQTT\_BRIDGE | `WITH DIRECTION "both"` |
| Document Storage | MONGODB | CLEAN query format |
| Search/Analytics | OPENSEARCH | Index configuration |
| API Integration | REST\_API | HTTP method + endpoint |
| Email Alerts | EMAIL | Template + placeholders |
## Next Steps
Run multiple Coreflux brokers as a fault-tolerant cluster.
Connect to industrial equipment via Modbus.
# ADS Route (Beckhoff)
Source: https://docs.coreflux.org/latest/lot-language/routes/industrial/ads
Connect to Beckhoff TwinCAT PLCs using ADS protocol with AutoDiscovery and comprehensive data type support
## ADS Overview
The `ADS` route enables communication with Beckhoff TwinCAT PLCs and systems using the ADS (Automation Device Specification) protocol. It supports automatic symbol discovery, bidirectional communication, and complex data types including arrays and structures.
ADS provides direct, high-performance access to TwinCAT variables. The AutoDiscovery feature can automatically find and expose all PLC symbols as MQTT topics.
The ADS route is on beta testing. It is planned for a future release.
## Basic Syntax
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
DEFINE ROUTE BeckhoffPLC WITH TYPE ADS
ADD ADS_CONFIG
WITH LOCAL_NET_ID "192.168.1.10.1.1"
WITH LOCAL_ROUTE_TO_TARGET "CX_12345"
WITH TARGET_NET_ID "192.168.1.100.1.1"
WITH TARGET_IP "192.168.1.100"
WITH TARGET_PORT 851
WITH TARGET_ROUTE_TO_LOCAL "MQTTBridge"
ADD MAPPING ProcessData
WITH EVERY 500 MILLISECONDS
ADD TAG Temperature
WITH ADDRESS "GVL_Process.rTemperature"
WITH DATA_TYPE "REAL"
WITH SOURCE_TOPIC "plc/temperature"
```
***
## AMS Net ID Basics
ADS communication requires AMS Net IDs for both local and target systems. The AMS Net ID is typically the IP address followed by `.1.1`.
| Component | Format | Example |
| ------------ | -------------- | ------------------- |
| AMS Net ID | `x.x.x.x.y.y` | `192.168.1.100.1.1` |
| IP portion | First 4 octets | `192.168.1.100` |
| Node portion | Last 2 octets | `.1.1` (usually) |
You can find your TwinCAT system's AMS Net ID in TwinCAT XAE under **SYSTEM** → **Routes** or by running `adsInfo` on the target.
***
## Connection Configuration
### ADS\_CONFIG Parameters
Local AMS Net ID (format: x.x.x.x.x.x). This is typically your machine's IP + .1.1.
Route name on local machine to reach the target PLC.
Local host name (optional).
Target PLC's AMS Net ID (format: x.x.x.x.x.x).
Target PLC's IP address.
ADS port. Typically 851 for TwinCAT 3, 801 for TwinCAT 2.
Route name on target PLC to reach the local machine.
Username for route creation (typically Administrator).
Password for route creation.
Connection timeout in milliseconds. Default: 5000.
### Connection Example
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
ADD ADS_CONFIG
WITH LOCAL_NET_ID "192.168.1.10.1.1"
WITH LOCAL_ROUTE_TO_TARGET "PlcRoute"
WITH TARGET_NET_ID "192.168.1.100.1.1"
WITH TARGET_IP "192.168.1.100"
WITH TARGET_PORT 851
WITH TARGET_ROUTE_TO_LOCAL "MQTTServer"
WITH TARGET_USER "Administrator"
WITH TARGET_PASSWORD "1"
WITH TIMEOUT 5000
```
***
## AutoDiscovery
AutoDiscovery automatically finds all PLC symbols and exposes them as MQTT topics.
### AutoDiscovery Parameters
Enable automatic symbol discovery. Default: false.
Base MQTT topic for discovered symbols. Default: ads/discovery.
Default QoS for discovered symbols (0-2). Default: 0.
Default retain flag for discovered symbols. Default: false.
Default trigger mode:
* `0` - OnStart (read once at connection)
* `1` - OnChange (publish on value change)
* `2` - Cyclic (poll at interval)
Default: 2.
Default polling cycle in milliseconds. Default: 1000.
Include patterns (comma-separated, \* wildcard). Default: \* (all symbols).
Exclude patterns (comma-separated, \* wildcard).
Structure expansion depth. Default: 3.
Enable parallel reading for large projects. Default: false.
Batch size for parallel reading. Default: 50.
### AutoDiscovery Example
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
DEFINE ROUTE BeckhoffAutoDiscover WITH TYPE ADS
ADD ADS_CONFIG
WITH LOCAL_NET_ID "192.168.1.10.1.1"
WITH LOCAL_ROUTE_TO_TARGET "CX_Route"
WITH TARGET_NET_ID "192.168.1.100.1.1"
WITH TARGET_IP "192.168.1.100"
WITH TARGET_PORT 851
WITH TARGET_ROUTE_TO_LOCAL "MQTTBridge"
WITH AUTO_DISCOVERY "true"
WITH DISCOVERY_BASE_TOPIC "beckhoff/plc"
WITH DISCOVERY_FILTER_INCLUDES "GVL_*"
WITH DISCOVERY_FILTER_EXCLUDES "*_Internal*"
WITH DISCOVERY_DEFAULT_CYCLE 500
WITH DISCOVERY_MAX_DEPTH 3
```
***
## TwinCAT Data Types
| Data Type | TwinCAT Type | Size | Description |
| ------------------ | ------------ | -------- | ------------------------------------------------- |
| `BOOL` | BOOL | 1 bit | Boolean value |
| `BYTE` | BYTE | 8 bits | Unsigned 8-bit |
| `SINT` | SINT | 8 bits | Signed 8-bit integer |
| `USINT` | USINT | 8 bits | Unsigned 8-bit integer |
| `WORD` | WORD | 16 bits | Unsigned 16-bit |
| `INT` | INT | 16 bits | Signed 16-bit integer |
| `UINT` | UINT | 16 bits | Unsigned 16-bit integer |
| `DWORD` | DWORD | 32 bits | Unsigned 32-bit |
| `DINT` | DINT | 32 bits | Signed 32-bit integer |
| `UDINT` | UDINT | 32 bits | Unsigned 32-bit integer |
| `LWORD` | LWORD | 64 bits | Unsigned 64-bit |
| `LINT` | LINT | 64 bits | Signed 64-bit integer |
| `ULINT` | ULINT | 64 bits | Unsigned 64-bit integer |
| `REAL` / `FLOAT` | REAL | 32 bits | 32-bit floating point |
| `LREAL` / `DOUBLE` | LREAL | 64 bits | 64-bit floating point |
| `CHAR` | CHAR | 8 bits | Single character |
| `WCHAR` | WCHAR | 16 bits | Wide character |
| `TIME` | TIME | 32 bits | Time duration |
| `DATE` | DATE | 16 bits | Date value |
| `TOD` | TOD | 32 bits | Time of day |
| `STRING` | STRING | Variable | ASCII string; sized with `LENGTH` |
| `ARRAY` | ARRAY | Variable | Array of elements (requires `ARRAY_ELEMENT_TYPE`) |
| `STRUCT` | STRUCT | Variable | Structured data type |
### String sizing
For `STRING` tags, `WITH LENGTH` is the total size in **bytes**, **including the ADS length byte**. It defaults to **256** bytes for reads; writes use an **80**-byte default when no size is supplied. `STRING_SIZE` is accepted as an alias for the same field.
`ARRAY_ELEMENT_LENGTH` is a separate setting for `ARRAY` tags whose elements are `STRING` or `STRUCT` — it does not replace `LENGTH` on scalar string tags.
Read a TwinCAT `STRING` symbol with an explicit read size (256 bytes here; writes default to 80 bytes when `LENGTH` is omitted):
```lot wrap focus={15} theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
DEFINE ROUTE TwinCATStrings WITH TYPE ADS
ADD ADS_CONFIG
WITH LOCAL_NET_ID "192.168.1.10.1.1"
WITH LOCAL_ROUTE_TO_TARGET "CX_Route"
WITH TARGET_NET_ID "192.168.1.100.1.1"
WITH TARGET_IP "192.168.1.100"
WITH TARGET_PORT 851
WITH TARGET_ROUTE_TO_LOCAL "MQTTBridge"
ADD MAPPING Labels
WITH EVERY 1 SECOND
ADD TAG ProductLabel
WITH ADDRESS "GVL_Process.sProductName"
WITH ADDRESS_TYPE "SYMBOL"
WITH DATA_TYPE "STRING"
WITH LENGTH 256
WITH SOURCE_TOPIC "production/label"
```
***
## TAG Configuration
### Symbol-Based Addressing
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
ADD TAG Temperature
WITH ADDRESS "GVL_Process.rTemperature"
WITH ADDRESS_TYPE "SYMBOL"
WITH DATA_TYPE "REAL"
WITH SOURCE_TOPIC "plc/temperature"
```
### Index-Based Addressing
For direct memory access using index group and offset:
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
ADD TAG DirectValue
WITH ADDRESS "16448.0"
WITH ADDRESS_TYPE "INDEX"
WITH DATA_TYPE "DINT"
WITH SOURCE_TOPIC "plc/direct"
```
### Array Handling
Array element type (required for ARRAY): BOOL, INT, REAL, STRING, STRUCT, etc.
Number of elements in the array.
Length of each element in bytes (for STRING/STRUCT arrays).
Offset in bytes to skip before array data. Default: 0.
Header bytes to skip per element. Default: 0.
### Structure Handling
Structure field definitions in format: `name:offset:type:size` (comma-separated).
### String sizing parameter
Total string size in bytes, including the ADS length byte. Default: **256** for reads; **80** for writes when omitted. Alias: `STRING_SIZE`.
***
## Event-Based Operations
For on-demand ADS operations (not polling), use the EVENT syntax. Publish a message to SOURCE\_TOPIC to trigger the operation; the route executes it and publishes the result to DESTINATION\_TOPIC.
### Supported Operations
| Operation | Description | Query Parameters |
| --------- | ----------------------------- | ------------------------------ |
| `READ` | Read a symbol value on demand | `symbol`, `data_type` |
| `WRITE` | Write a value to a symbol | `symbol`, `data_type`, `value` |
### Query Parameters
| Parameter | Description | Example |
| ----------- | --------------------------------- | --------------------------------------------- |
| `operation` | Operation type: `READ` or `WRITE` | `READ` |
| `symbol` | TwinCAT symbol path | `GVL_Process.rTemperature`, `GVL_Control.rSP` |
| `data_type` | TwinCAT data type | `REAL`, `LREAL`, `DINT`, `BOOL`, `INT` |
| `value` | Value to write (WRITE only) | `75.0`, `42`, `true` |
### Read Example
Read a process variable on demand:
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
ADD EVENT ReadSymbol
WITH SOURCE_TOPIC "ads/commands/read"
WITH DESTINATION_TOPIC "ads/responses/read"
WITH QUERY "{operation: READ, symbol: GVL_Process.rTemperature, data_type: REAL}"
```
### Write Example
Write a setpoint on demand:
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
ADD EVENT WriteSetpoint
WITH SOURCE_TOPIC "ads/commands/write"
WITH DESTINATION_TOPIC "ads/responses/write"
WITH QUERY "{operation: WRITE, symbol: GVL_Control.rSP, data_type: REAL, value: 75.0}"
```
***
## Complete Examples
Read PLC variables using symbol names:
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
DEFINE ROUTE TwinCATBasic WITH TYPE ADS
ADD ADS_CONFIG
WITH LOCAL_NET_ID "192.168.1.10.1.1"
WITH LOCAL_ROUTE_TO_TARGET "CX_Route"
WITH TARGET_NET_ID "192.168.1.100.1.1"
WITH TARGET_IP "192.168.1.100"
WITH TARGET_PORT 851
WITH TARGET_ROUTE_TO_LOCAL "MQTTBridge"
ADD MAPPING ProcessVariables
WITH EVERY 500 MILLISECONDS
ADD TAG Temperature
WITH ADDRESS "GVL_Process.rTemperature"
WITH ADDRESS_TYPE "SYMBOL"
WITH DATA_TYPE "REAL"
WITH SOURCE_TOPIC "plc/process/temperature"
WITH UNIT "°C"
ADD TAG Pressure
WITH ADDRESS "GVL_Process.rPressure"
WITH ADDRESS_TYPE "SYMBOL"
WITH DATA_TYPE "REAL"
WITH SOURCE_TOPIC "plc/process/pressure"
WITH UNIT "bar"
ADD TAG RunningState
WITH ADDRESS "GVL_Process.bRunning"
WITH ADDRESS_TYPE "SYMBOL"
WITH DATA_TYPE "BOOL"
WITH SOURCE_TOPIC "plc/process/running"
```
Automatically discover and expose all GVL variables:
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
DEFINE ROUTE TwinCATAutoDiscover WITH TYPE ADS
ADD ADS_CONFIG
WITH LOCAL_NET_ID "192.168.1.10.1.1"
WITH LOCAL_ROUTE_TO_TARGET "CX_Route"
WITH TARGET_NET_ID "192.168.1.100.1.1"
WITH TARGET_IP "192.168.1.100"
WITH TARGET_PORT 851
WITH TARGET_ROUTE_TO_LOCAL "MQTTBridge"
WITH AUTO_DISCOVERY "true"
WITH DISCOVERY_BASE_TOPIC "twincat"
WITH DISCOVERY_FILTER_INCLUDES "GVL_Production.*,GVL_Process.*"
WITH DISCOVERY_FILTER_EXCLUDES "*_Debug*,*_Test*"
WITH DISCOVERY_DEFAULT_CYCLE 500
WITH DISCOVERY_MAX_DEPTH 2
WITH DISCOVERY_PARALLEL_READING "true"
WITH DISCOVERY_BATCH_SIZE 100
```
Read process values and write setpoints:
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
DEFINE ROUTE TwinCATControl WITH TYPE ADS
ADD ADS_CONFIG
WITH LOCAL_NET_ID "192.168.1.10.1.1"
WITH LOCAL_ROUTE_TO_TARGET "CX_Route"
WITH TARGET_NET_ID "192.168.1.100.1.1"
WITH TARGET_IP "192.168.1.100"
WITH TARGET_PORT 851
WITH TARGET_ROUTE_TO_LOCAL "MQTTBridge"
ADD MAPPING ControlLoop
WITH EVERY 200 MILLISECONDS
ADD TAG ProcessValue
WITH ADDRESS "GVL_Control.rPV"
WITH DATA_TYPE "REAL"
WITH SOURCE_TOPIC "control/pv"
WITH UNIT "°C"
ADD TAG Setpoint
WITH ADDRESS "GVL_Control.rSP"
WITH DATA_TYPE "REAL"
WITH SOURCE_TOPIC "control/sp"
WITH WRITABLE "true"
WITH DESTINATION_TOPIC "control/sp/write"
WITH MIN_VALUE 0
WITH MAX_VALUE 100
ADD TAG Output
WITH ADDRESS "GVL_Control.rOutput"
WITH DATA_TYPE "REAL"
WITH SOURCE_TOPIC "control/output"
WITH UNIT "%"
ADD TAG AutoMode
WITH ADDRESS "GVL_Control.bAutoMode"
WITH DATA_TYPE "BOOL"
WITH SOURCE_TOPIC "control/auto"
WITH WRITABLE "true"
WITH DESTINATION_TOPIC "control/auto/write"
```
Read array values:
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
DEFINE ROUTE TwinCATArrays WITH TYPE ADS
ADD ADS_CONFIG
WITH LOCAL_NET_ID "192.168.1.10.1.1"
WITH LOCAL_ROUTE_TO_TARGET "CX_Route"
WITH TARGET_NET_ID "192.168.1.100.1.1"
WITH TARGET_IP "192.168.1.100"
WITH TARGET_PORT 851
WITH TARGET_ROUTE_TO_LOCAL "MQTTBridge"
ADD MAPPING ArrayData
WITH EVERY 1 SECOND
ADD TAG TemperatureArray
WITH ADDRESS "GVL_Data.arTemperatures"
WITH DATA_TYPE "ARRAY"
WITH ARRAY_ELEMENT_TYPE "REAL"
WITH ARRAY_SIZE 10
WITH SOURCE_TOPIC "plc/temperatures"
WITH PUBLISH_MODE "JSON"
ADD TAG StatusBits
WITH ADDRESS "GVL_Data.arStatus"
WITH DATA_TYPE "ARRAY"
WITH ARRAY_ELEMENT_TYPE "BOOL"
WITH ARRAY_SIZE 16
WITH SOURCE_TOPIC "plc/status"
```
Read structured data:
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
DEFINE ROUTE TwinCATStruct WITH TYPE ADS
ADD ADS_CONFIG
WITH LOCAL_NET_ID "192.168.1.10.1.1"
WITH LOCAL_ROUTE_TO_TARGET "CX_Route"
WITH TARGET_NET_ID "192.168.1.100.1.1"
WITH TARGET_IP "192.168.1.100"
WITH TARGET_PORT 851
WITH TARGET_ROUTE_TO_LOCAL "MQTTBridge"
ADD MAPPING StructData
WITH EVERY 500 MILLISECONDS
ADD TAG MachineData
WITH ADDRESS "GVL_Machine.stMachineData"
WITH DATA_TYPE "STRUCT"
WITH STRUCT_FIELD_DEFINITIONS "Speed:0:REAL:4,Current:4:REAL:4,Status:8:INT:2,ErrorCode:10:WORD:2"
WITH SOURCE_TOPIC "plc/machine"
WITH PUBLISH_MODE "JSON"
```
Continuous monitoring with on-demand read and write events in the same route:
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
DEFINE ROUTE FullADSSetup WITH TYPE ADS
ADD ADS_CONFIG
WITH LOCAL_NET_ID "192.168.1.10.1.1"
WITH LOCAL_ROUTE_TO_TARGET "CX_Route"
WITH TARGET_NET_ID "192.168.1.100.1.1"
WITH TARGET_IP "192.168.1.100"
WITH TARGET_PORT 851
WITH TARGET_ROUTE_TO_LOCAL "MQTTBridge"
ADD MAPPING ProcessVariables
WITH EVERY 500 MILLISECONDS
ADD TAG Temperature
WITH ADDRESS "GVL_Process.rTemperature"
WITH ADDRESS_TYPE "SYMBOL"
WITH DATA_TYPE "REAL"
WITH SOURCE_TOPIC "plc/temperature"
WITH UNIT "°C"
ADD TAG Pressure
WITH ADDRESS "GVL_Process.rPressure"
WITH ADDRESS_TYPE "SYMBOL"
WITH DATA_TYPE "REAL"
WITH SOURCE_TOPIC "plc/pressure"
WITH UNIT "bar"
ADD EVENT ReadSymbol
WITH SOURCE_TOPIC "ads/commands/read"
WITH DESTINATION_TOPIC "ads/responses/read"
WITH QUERY "{operation: READ, symbol: GVL_Process.rOutput, data_type: REAL}"
ADD EVENT WriteSetpoint
WITH SOURCE_TOPIC "ads/commands/write"
WITH DESTINATION_TOPIC "ads/responses/write"
WITH QUERY "{operation: WRITE, symbol: GVL_Control.rSP, data_type: REAL, value: 0}"
```
The MAPPING continuously reads temperature and pressure. To trigger an on-demand read of the output value, publish any message to `ads/commands/read`. To write a setpoint, publish to `ads/commands/write`.
***
## Route Setup Process
To establish ADS communication, routes must be configured on both sides:
### 1. On TwinCAT System
1. Open TwinCAT XAE
2. Go to **SYSTEM** → **Routes**
3. Add a static route:
* **Route Name**: Match `TARGET_ROUTE_TO_LOCAL` parameter
* **AMS Net ID**: Your Coreflux machine's AMS Net ID
* **Transport Type**: TCP/IP
* **Address**: Your Coreflux machine's IP
### 2. In Coreflux Configuration
The route automatically creates a corresponding route on the local machine using the configured parameters.
Both route names (`LOCAL_ROUTE_TO_TARGET` and `TARGET_ROUTE_TO_LOCAL`) must match the actual route names configured on each system.
***
## Troubleshooting
* Verify AMS Net IDs are correct on both sides
* Check routes are properly configured in TwinCAT
* Ensure TARGET\_PORT is correct (851 for TC3, 801 for TC2)
* Verify firewall allows ADS port (typically 48898)
* Check TARGET\_USER/PASSWORD for route creation
* Verify symbol path matches exactly (case-sensitive)
* Check symbol exists in PLC project
* Ensure PLC is in RUN mode
* Use AutoDiscovery to find correct symbol names
* Check DISCOVERY\_FILTER\_INCLUDES patterns
* Verify DISCOVERY\_MAX\_DEPTH is sufficient
* For large projects, enable DISCOVERY\_PARALLEL\_READING
* Check PLC project is compiled and deployed
* Ensure WRITABLE is set to "true"
* Verify PLC variable is not constant
* Check value is within MIN\_VALUE/MAX\_VALUE
* Verify PLC is in RUN mode
* Increase TIMEOUT value
* Check network connectivity
* Verify TwinCAT runtime is running
* Reduce DISCOVERY\_BATCH\_SIZE if using AutoDiscovery
***
## Next Steps
Connect to Rockwell Automation PLCs.
Cross-platform OPC UA communication.
# Allen-Bradley Route
Source: https://docs.coreflux.org/latest/lot-language/routes/industrial/allen-bradley
Connect to Rockwell Automation PLCs over EtherNet/IP with symbolic tag names, program scope, UDTs, arrays, and bidirectional writes.
## Connect to Rockwell PLCs by Tag Name
The `ALLEN_BRADLEY` route connects Coreflux to Rockwell Automation PLCs using **symbolic tag names** over EtherNet/IP. Define an `ALLEN_BRADLEY_CONFIG` block, add TAGs with PLC tag addresses, and publish each data point to its own MQTT topic — with optional scaling, units, deadband, and bidirectional writes.
**Like reading labels on a control panel.** Instead of numeric register addresses, you use the same tag names you see in Studio 5000 — `MainCounter`, `Program:MainProgram.Temperature`, `Motor1.Speed`.
For **explicit CIP paths** (`4.100.3.0`), use the [EtherNet/IP route](./ethernetip) instead. This route is for symbolic tag-name access; EtherNet/IP is for Class/Instance/Attribute addressing.
### Route and config aliases
| Alias | Canonical form |
| -------------------------------- | -------------------------- |
| `TYPE AB` or `TYPE ALLENBRADLEY` | `TYPE ALLEN_BRADLEY` |
| `ADD AB_CONFIG` | `ADD ALLEN_BRADLEY_CONFIG` |
### Key capabilities
* **Symbolic tag names** — controller-scoped, program-scoped, UDT members, and arrays
* **Multiple PLC families** — ControlLogix, CompactLogix, Micro800, MicroLogix, PLC5, SLC500
* **TAG-based polling** — per-point MQTT topics with configurable intervals
* **Bidirectional writes** — `WRITABLE` tags with `DESTINATION_TOPIC`
* **Engineering units** — `SCALING`, `OFFSET`, `UNIT`, `DECIMAL_PLACES`, min/max clamping
* **Publish modes** — `VALUE_ONLY` (default) or `JSON` with quality metadata
***
## Quick Start
Read a controller-scoped DINT counter every second:
```lot wrap focus={9-13} theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
DEFINE ROUTE ControlLogixPLC WITH TYPE ALLEN_BRADLEY
ADD ALLEN_BRADLEY_CONFIG
WITH IP "192.168.1.10"
WITH DEVICE_FAMILY 0
WITH PATH "1,0"
ADD MAPPING ProductionData
WITH EVERY 1 SECOND
ADD TAG ProductCounter
WITH ADDRESS "MainCounter"
WITH DATA_TYPE "DINT"
WITH SOURCE_TOPIC "production/counter"
```
Read tags inside a specific program with units and deadband:
```lot wrap focus={9-16} theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
DEFINE ROUTE ControlLogix WITH TYPE ALLEN_BRADLEY
ADD ALLEN_BRADLEY_CONFIG
WITH IP "192.168.1.20"
WITH DEVICE_FAMILY 0
ADD MAPPING ProcessControl
WITH EVERY 500 MILLISECONDS
ADD TAG ReactorTemp
WITH ADDRESS "Program:MainProgram.Temperature"
WITH DATA_TYPE "REAL"
WITH UNIT "°C"
WITH DECIMAL_PLACES 2
WITH DEADBAND 0.5
WITH SOURCE_TOPIC "process/reactor/temperature"
```
Poll a setpoint and accept MQTT writes to update it:
```lot wrap focus={9-18} theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
DEFINE ROUTE TemperatureControl WITH TYPE ALLEN_BRADLEY
ADD ALLEN_BRADLEY_CONFIG
WITH IP "192.168.1.60"
WITH DEVICE_FAMILY 0
ADD MAPPING ControlLoop
WITH EVERY 1 SECOND
ADD TAG TempSetpoint
WITH ADDRESS "TempSetpoint"
WITH DATA_TYPE "REAL"
WITH UNIT "°C"
WITH MIN_VALUE 0.0
WITH MAX_VALUE 100.0
WITH SOURCE_TOPIC "control/temperature/setpoint"
WITH WRITABLE "true"
WITH DESTINATION_TOPIC "control/temperature/setpoint/write"
```
Read built-in I/O on a Micro800 controller:
```lot wrap focus={9-14} theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
DEFINE ROUTE Micro800PLC WITH TYPE ALLEN_BRADLEY
ADD ALLEN_BRADLEY_CONFIG
WITH IP "192.168.1.70"
WITH DEVICE_FAMILY 4
ADD MAPPING Micro800Data
WITH EVERY 1 SECOND
ADD TAG DigitalIn
WITH ADDRESS "_IO_EM_DI_00"
WITH DATA_TYPE "BOOL"
WITH SOURCE_TOPIC "micro800/input0"
```
***
## Connection Configuration
### ALLEN\_BRADLEY\_CONFIG Parameters
| Parameter | Required | Default | Description |
| --------------- | -------- | ------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `IP` | Yes | — | PLC IP address |
| `DEVICE_FAMILY` | No | `0` | Device family code (0–6, see table below) |
| `PORT` | No | `44818` | EtherNet/IP port. Only honored for **Micro800** (`DEVICE_FAMILY 4`), which connects at `IP:PORT` — other families always use the default EtherNet/IP port. |
| `SLOT` | No | `0` | Parsed for legacy parity only. Backplane routing uses **`PATH`, not `SLOT`** — set the CPU slot in `PATH` (for example `"1,0"`). |
| `PATH` | No | `"1,0"` | Routing path to the PLC (format: `backplane,slot`). |
| `TIMEOUT` | No | `5000` | Per-tag read/write timeout in milliseconds |
The config block may be declared as `ADD ALLEN_BRADLEY_CONFIG` or `ADD AB_CONFIG` — both are equivalent.
Loading the route marks it ready; a transport is opened per tag read/write. `-checkRouteConnection` confirms the route is loaded but **does not probe the PLC** — verify connectivity with polling or a test read.
### Device Family Codes
| Code | Family | Use when |
| ---- | --------------------------- | ------------------------------------------ |
| `0` | ControlLogix / CompactLogix | 1756-L6x, L7x, L8x, 1769-Lxx (most common) |
| `1` | PLC5 | 1785-L series (legacy) |
| `2` | SLC500 | 1747-L series (legacy) |
| `3` | ControlLogix PLC5 | Mixed environment |
| `4` | Micro800 | 2080-LC series |
| `5` | MicroLogix | 1766, 1764 series |
| `6` | Omron | NJ/NX with CIP support |
### Connection Examples
Standard backplane path to CPU in slot 0:
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
ADD ALLEN_BRADLEY_CONFIG
WITH IP "192.168.1.100"
WITH DEVICE_FAMILY 0
WITH PATH "1,0"
WITH TIMEOUT 5000
```
MicroLogix 1100/1400:
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
ADD ALLEN_BRADLEY_CONFIG
WITH IP "192.168.1.50"
WITH DEVICE_FAMILY 5
WITH TIMEOUT 5000
```
CPU in slot 2 on backplane 1:
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
ADD ALLEN_BRADLEY_CONFIG
WITH IP "192.168.1.100"
WITH DEVICE_FAMILY 0
WITH PATH "1,2"
WITH TIMEOUT 5000
```
Micro800 — `PATH` is usually omitted:
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
ADD ALLEN_BRADLEY_CONFIG
WITH IP "192.168.1.70"
WITH DEVICE_FAMILY 4
WITH TIMEOUT 5000
```
***
## Tag Addressing
Allen-Bradley PLCs use **tag-based addressing** (not register numbers). Tag names are case-sensitive.
| Scope | Format | Example |
| ------------- | ----------------------------- | ----------------------------- |
| Controller | `TagName` | `MainCounter` |
| Program | `Program:ProgramName.TagName` | `Program:MainProgram.Counter` |
| Array element | `TagName[index]` | `Temps[5]` |
| UDT member | `UDTInstance.Member` | `Valve1.Status` |
| Nested UDT | `Parent.Child.GrandChild` | `Tank1.Level.Current` |
| I/O tags | `Local:Slot:Type.Data[index]` | `Local:1:I.Data[0]` |
### Tag naming rules
* Letters, numbers, and underscore only
* Must start with a letter or underscore
* Case-sensitive
* No spaces
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
# Valid
WITH ADDRESS "MainCounter"
WITH ADDRESS "Program:MainProgram.Temperature"
WITH ADDRESS "Valve1.Status"
WITH ADDRESS "Temps[5]"
```
***
## Data Types
| Coreflux `DATA_TYPE` | AB type | Notes |
| -------------------- | ------- | ------------------------------------------------------------------ |
| `BOOL` | BOOL | Boolean |
| `SINT` | SINT | Signed 8-bit |
| `USINT` / `BYTE` | USINT | Unsigned 8-bit |
| `INT` | INT | Signed 16-bit |
| `UINT` / `WORD` | UINT | Unsigned 16-bit |
| `DINT` | DINT | Signed 32-bit |
| `UDINT` / `DWORD` | UDINT | Unsigned 32-bit |
| `LINT` | LINT | Signed 64-bit |
| `ULINT` / `LWORD` | ULINT | Unsigned 64-bit |
| `REAL` / `FLOAT` | REAL | 32-bit float |
| `LREAL` / `DOUBLE` | LREAL | 64-bit float — MQTT values round to `DECIMAL_PLACES` (default `2`) |
| `CHAR` | CHAR | Single character |
| `WCHAR` | WCHAR | Wide character |
| `STRING` | STRING | Fixed 88-byte CIP layout — see encoding notes below |
| `TIME` | TIME | Duration in ms — published as `T#XdXhXmXsXms` |
| `DATE` | DATE | Date value |
| `TOD` | TOD | Time of day |
### STRING encoding
The route uses a **fixed 88-byte** CIP `STRING` layout. Neither `LENGTH` nor `STRING_SIZE` resizes this layout — `STRING_SIZE` is accepted as an alias for the same field and has the same limitation.
Encoding still depends on `DEVICE_FAMILY`:
* **ControlLogix / CompactLogix (default):** 4-byte DINT length prefix + up to 82 ASCII characters
* **Micro800:** 1-byte length prefix + characters
### TIME encoding
Raw value is a signed 32-bit millisecond count. MQTT publishes Allen-Bradley-style duration strings (for example `T#1h1m1s1ms`).
***
## TAG Configuration
Every polled data point is an `ADD TAG` inside an `ADD MAPPING` block.
### Complete TAG Example
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
ADD TAG MotorSpeed
WITH ADDRESS "Motor1.ActualSpeed"
WITH ADDRESS_TYPE "SYMBOL"
WITH DATA_TYPE "REAL"
WITH SOURCE_TOPIC "plc/motor1/speed"
WITH SCALING 1
WITH OFFSET 0
WITH UNIT "RPM"
WITH DECIMAL_PLACES 2
WITH MIN_VALUE 0
WITH MAX_VALUE 3600
WITH DEADBAND 10
WITH PUBLISH_MODE "JSON"
WITH DESCRIPTION "Motor 1 actual speed in RPM"
```
### TAG Parameters
PLC tag name — controller-scoped (`MainCounter`), program-scoped (`Program:MainProgram.Counter`), UDT member (`Valve1.Status`), or array (`Temps[0]`).
`SYMBOL` (default) or `PATH`.
Logix data type: `BOOL`, `SINT`, `INT`, `DINT`, `LINT`, `USINT`, `UINT`, `UDINT`, `ULINT`, `REAL`, `LREAL`, `STRING`, `TIME`, etc.
Multiply the parsed value after read. Alias: `SCALING_FACTOR`. Default: `1.0`.
Add after scaling. Default: `0.0`.
Engineering unit (for example `°C`, `bar`, `RPM`). Included in JSON publish mode.
Rounding precision for float MQTT payloads. Default: `2`.
Clamp minimum allowed value (especially for writable tags).
Clamp maximum allowed value.
Suppress MQTT publish when the change is below this threshold. Default: `0.0`.
MQTT topic where polled values are published.
`VALUE_ONLY` (default) or `JSON` (value, unit, quality, timestamp, description).
When `"true"`, subscribe to `DESTINATION_TOPIC` and write incoming MQTT values to the PLC.
MQTT topic to receive write commands. Required when `WRITABLE` is `"true"`.
Human-readable description (included in JSON publish mode).
### Publish Modes
**VALUE\_ONLY** (default) — publishes just the scaled value:
```
12345
```
**JSON** — metadata wrapper when explicitly set:
```json wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
{
"tag_name": "SafetyPressure",
"value": 8.456,
"unit": "bar",
"quality": "GOOD",
"timestamp": "2025-12-17T10:30:45.123Z",
"description": "Critical safety pressure sensor"
}
```
***
## Event-Based Operations
For on-demand tag reads and writes (not continuous polling), add an `ADD EVENT` block. Publish any message to `SOURCE_TOPIC` to trigger the operation; results appear on `DESTINATION_TOPIC`.
### Query Parameters
| Parameter | Description | Example |
| ----------- | --------------------------- | -------------------------------------------- |
| `operation` | `READ` or `WRITE` | `READ` |
| `variable` | PLC tag name | `Program:MainProgram.Counter`, `MainCounter` |
| `data_type` | Logix data type | `DINT`, `REAL`, `BOOL` |
| `value` | Value to write (WRITE only) | `42`, `75.0` |
### Read Example
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
ADD EVENT ReadOnDemand
WITH SOURCE_TOPIC "ab/commands/read"
WITH DESTINATION_TOPIC "ab/responses/read"
WITH QUERY "{operation: READ, variable: Program:MainProgram.Counter, data_type: DINT}"
```
### Write Example
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
ADD EVENT WriteSetpoint
WITH SOURCE_TOPIC "ab/commands/write"
WITH DESTINATION_TOPIC "ab/responses/write"
WITH QUERY "{operation: WRITE, variable: TIC100.SP, data_type: REAL, value: 150.0}"
```
***
## Examples
Monitor BOOL, DINT, and REAL tags in one mapping:
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
DEFINE ROUTE ProductionLine WITH TYPE ALLEN_BRADLEY
ADD ALLEN_BRADLEY_CONFIG
WITH IP "192.168.1.30"
WITH DEVICE_FAMILY 0
ADD MAPPING MachineMonitor
WITH EVERY 1 SECOND
ADD TAG MachineRunning
WITH ADDRESS "MachineStatus.Running"
WITH DATA_TYPE "BOOL"
WITH SOURCE_TOPIC "machine/status/running"
ADD TAG ProductCount
WITH ADDRESS "ProductionCounters.TotalCount"
WITH DATA_TYPE "DINT"
WITH SOURCE_TOPIC "machine/production/total"
ADD TAG LineSpeed
WITH ADDRESS "LineSpeed"
WITH DATA_TYPE "REAL"
WITH UNIT "units/min"
WITH SOURCE_TOPIC "machine/speed"
```
Read User-Defined Type structure members:
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
DEFINE ROUTE UDTAccess WITH TYPE ALLEN_BRADLEY
ADD ALLEN_BRADLEY_CONFIG
WITH IP "192.168.1.40"
WITH DEVICE_FAMILY 0
ADD MAPPING ValveControl
WITH EVERY 500 MILLISECONDS
ADD TAG Valve1Position
WITH ADDRESS "Valve1.Position"
WITH DATA_TYPE "INT"
WITH UNIT "%"
WITH SOURCE_TOPIC "valves/valve1/position"
ADD TAG Valve1Status
WITH ADDRESS "Valve1.Status"
WITH DATA_TYPE "BOOL"
WITH SOURCE_TOPIC "valves/valve1/status"
```
Read specific array indices:
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
DEFINE ROUTE ArrayData WITH TYPE ALLEN_BRADLEY
ADD ALLEN_BRADLEY_CONFIG
WITH IP "192.168.1.50"
WITH DEVICE_FAMILY 0
ADD MAPPING ArrayMonitor
WITH EVERY 1 SECOND
ADD TAG Zone1Temp
WITH ADDRESS "ZoneTemperatures[0]"
WITH DATA_TYPE "REAL"
WITH UNIT "°C"
WITH SOURCE_TOPIC "zones/zone1/temperature"
ADD TAG Zone2Temp
WITH ADDRESS "ZoneTemperatures[1]"
WITH DATA_TYPE "REAL"
WITH UNIT "°C"
WITH SOURCE_TOPIC "zones/zone2/temperature"
```
Read process value and write setpoint from MQTT:
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
DEFINE ROUTE ControlLoop WITH TYPE ALLEN_BRADLEY
ADD ALLEN_BRADLEY_CONFIG
WITH IP "192.168.1.100"
WITH DEVICE_FAMILY 0
ADD MAPPING TempControl
WITH EVERY 200 MILLISECONDS
ADD TAG PV
WITH ADDRESS "TIC100.PV"
WITH DATA_TYPE "REAL"
WITH SOURCE_TOPIC "control/tic100/pv"
WITH UNIT "°F"
ADD TAG SP
WITH ADDRESS "TIC100.SP"
WITH DATA_TYPE "REAL"
WITH SOURCE_TOPIC "control/tic100/sp"
WITH UNIT "°F"
WITH WRITABLE "true"
WITH DESTINATION_TOPIC "control/tic100/sp/set"
WITH MIN_VALUE 32
WITH MAX_VALUE 212
```
Scale a 4–20 mA sensor to engineering units:
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
DEFINE ROUTE AnalogSensors WITH TYPE ALLEN_BRADLEY
ADD ALLEN_BRADLEY_CONFIG
WITH IP "192.168.1.80"
WITH DEVICE_FAMILY 0
ADD MAPPING AnalogData
WITH EVERY 500 MILLISECONDS
ADD TAG Pressure
WITH ADDRESS "AI_Pressure"
WITH DATA_TYPE "REAL"
WITH UNIT "bar"
WITH DECIMAL_PLACES 3
WITH DEADBAND 0.01
WITH SOURCE_TOPIC "sensors/pressure"
```
***
## Best Practices
Use `DEVICE_FAMILY 0` for ControlLogix and CompactLogix (most common), `4` for Micro800, and `5` for MicroLogix. Legacy PLC5/SLC500 families (`1`/`2`) only for existing installations.
Controller-scoped tags use the name alone (`GlobalCounter`). Program-scoped tags need the full prefix: `Program:MainProgram.LocalCounter`.
Set the CPU slot in `PATH` (`"1,0"` for backplane 1, slot 0). `SLOT` is kept for legacy parity, but routing is always resolved from `PATH`.
Use `WRITABLE "true"` with `MIN_VALUE` / `MAX_VALUE` on setpoints. Ensure the PLC is in Remote Run mode for writes to succeed.
Use `DEADBAND` on noisy analog values. Match `EVERY` to how fast the value changes — 500 ms for critical data, 1–5 s for slow status.
***
## Troubleshooting
* Verify IP with `ping`
* Confirm port 44818 is open
* Ensure PLC is powered on and in **RUN** mode
* Check `DEVICE_FAMILY` matches your controller type
* Increase `TIMEOUT` on slow networks
* Remember: `-checkRouteConnection` does not probe the PLC — use a polling TAG to verify
* Tag names are **case-sensitive** — match Studio 5000 exactly
* Program-scoped tags need `Program:ProgramName.TagName` (not `ProgramName:Tag`)
* Verify the tag exists in Controller Tags or Program Tags in Studio 5000
* For UDTs, include the member: `Valve1.Status` not `Valve1`
* For arrays, include the index: `Temps[0]` not `Temps`
* Use `PATH "1,0"` for standard backplane routing (backplane, slot)
* For Micro800, omit `PATH` or leave it unset
* Multi-hop paths use comma-separated segments: `"1,2,2,1"` through bridge modules
* Set `DEVICE_FAMILY 4`
* Use Micro800 I/O tag names (`_IO_EM_DI_00`, `_IO_EM_AI_00`)
* STRING uses 1-byte length prefix (not ControlLogix 4-byte format)
* Set `WRITABLE "true"` and provide `DESTINATION_TOPIC`
* PLC must be in **Remote Run** mode
* Verify `DATA_TYPE` matches the PLC tag definition
* Check value is within `MIN_VALUE` / `MAX_VALUE` if set
* Match PLC types exactly: DINT → `DINT`, REAL → `REAL`, BOOL → `BOOL`
* `TIME` tags publish as `T#...` duration strings on MQTT
***
## Next Steps
Explicit CIP Class/Instance/Attribute addressing for assembly objects and legacy devices.
Compare OT routes and choose the right protocol for your devices.
# BACnet/IP Route
Source: https://docs.coreflux.org/latest/lot-language/routes/industrial/bacnet
Connect Coreflux to building automation controllers over BACnet/IP with TAG polling, COV subscriptions, alarms, and bidirectional writes.
## Connect Coreflux to Building Automation
The BACnet route connects Coreflux to building automation controllers (ASHRAE 135) using the unified OT TAG system. Define the BACnet configuration, add Tags for continuous polling or COV subscriptions, and handle alarm operations.
BACnet is the dominant protocol for building management systems. If your controller supports BACnet/IP, this route maps each data point to an MQTT topic using the same TAG model as every other Coreflux industrial route.
### When to use this
Use the BACnet route when you need to integrate HVAC, lighting, metering, or access-control controllers that expose **BACnet/IP** — read live values on a schedule, subscribe to change notifications, write setpoints from MQTT, receive intrinsic alarms, or run ad-hoc reads and writes triggered by MQTT commands.
### Key capabilities
* **BACnet/IP over UDP** — standard port 47808 (0xBAC0) with broadcast or direct-IP discovery
* **TAG-based polling** — per-point MQTT topics with a configurable polling interval
* **Change-of-Value (COV)** — push subscriptions per tag (`WITH COV true`) without polling
* **Event alarms** — BACnet Event-Notifications to MQTT with per-tag or device-wide streams
* **Bidirectional writes** — MQTT-triggered writes to analog, binary, and value objects
* **On-demand events** — `ADD EVENT` with `READ`, `WRITE`, and `ACK` queries triggered by any MQTT publish
* **Operator ACK workflow** — auto-registered ACK handler per alarming tag
* **Direct IP mode** — connect to controllers that do not respond to Who-Is/I-Am
* **Docker and hostname support** — resolve container service names before opening the BACnet socket
***
## Quick Start
Pick the starting point that matches what you need. Each tag publishes to `{SOURCE_TOPIC}/{tagName}` unless the tag sets its own `SOURCE_TOPIC`.
Connect to a controller and read a room temperature every five seconds:
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
DEFINE ROUTE BMS WITH TYPE BACNET
ADD BACNET_CONFIG
WITH IP "192.168.1.10"
WITH DEVICE_ID 1234
ADD MAPPING RoomSensors
WITH SOURCE_TOPIC "bacnet/bms"
WITH EVERY 5 SECONDS
ADD TAG RoomTemp
WITH ADDRESS "AI:0"
WITH DATA_TYPE "FLOAT"
WITH UNIT "degC"
```
Read a setpoint and allow MQTT writes back to the device:
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
DEFINE ROUTE HVAC WITH TYPE BACNET
ADD BACNET_CONFIG
WITH IP "10.0.0.50"
WITH DEVICE_ID 2000
WITH WRITE_PRIORITY 8
ADD MAPPING Setpoints
WITH SOURCE_TOPIC "bacnet/hvac"
WITH EVERY 10 SECONDS
ADD TAG CoolingSetpoint
WITH ADDRESS "AV:1"
WITH DATA_TYPE "FLOAT"
WITH UNIT "degC"
WITH WRITABLE true
WITH DESTINATION_TOPIC "bacnet/hvac/CoolingSetpoint/set"
```
Subscribe to Change-of-Value so the device pushes updates only when values change — no polling interval needed:
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
DEFINE ROUTE BMS WITH TYPE BACNET
ADD BACNET_CONFIG
WITH IP "192.168.1.10"
WITH DEVICE_ID 1234
WITH COV_LIFETIME 600
ADD MAPPING CovPoints
WITH SOURCE_TOPIC "bacnet/bms/cov"
ADD TAG RoomTempCov
WITH ADDRESS "AI:0"
WITH DATA_TYPE "FLOAT"
WITH COV true
```
When the controller does not answer Who-Is/I-Am, connect straight to its IP address:
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
DEFINE ROUTE BMS WITH TYPE BACNET
ADD BACNET_CONFIG
WITH IP "10.0.0.50"
WITH PORT 47808
WITH DEVICE_ID 1234
WITH DIRECT_IP true
ADD MAPPING RoomSensors
WITH SOURCE_TOPIC "bacnet/bms"
WITH EVERY 5 SECONDS
ADD TAG RoomTemp
WITH ADDRESS "AI:0"
WITH DATA_TYPE "FLOAT"
WITH UNIT "degC"
```
Trigger a single BACnet read or write by publishing to an MQTT command topic:
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
DEFINE ROUTE BMS WITH TYPE BACNET
ADD BACNET_CONFIG
WITH IP "192.168.1.10"
WITH DEVICE_ID 1234
ADD EVENT ReadProperty
WITH SOURCE_TOPIC "bacnet/cmd/read"
WITH DESTINATION_TOPIC "bacnet/cmd/read/result"
WITH QUERY "READ:AI:0:PRESENT_VALUE"
ADD EVENT WriteProperty
WITH SOURCE_TOPIC "bacnet/cmd/write"
WITH DESTINATION_TOPIC "bacnet/cmd/write/result"
WITH QUERY "{payload}"
```
Each alarming point gets its own MQTT alarm stream and auto-registered ACK topic:
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
DEFINE ROUTE BMS WITH TYPE BACNET
ADD BACNET_CONFIG
WITH IP "192.168.1.10"
WITH DEVICE_ID 1234
WITH LOCAL_DEVICE_ID 4194300
ADD MAPPING LimitPoints
WITH SOURCE_TOPIC "bacnet/bms"
ADD TAG RoomTemp
WITH ADDRESS "AI:0"
WITH DATA_TYPE "FLOAT"
WITH COV true
WITH ALARM true
```
***
## Protocol Overview
BACnet (Building Automation and Control networks) is an open protocol for building automation, standardized as ANSI/ASHRAE 135 and ISO 16484-5. It enables interoperability between devices from different manufacturers in HVAC, lighting, fire detection, access control, and energy management.
| Concept | Description |
| ---------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **Object model** | Every data point is a typed object (Analog Input, Binary Value, etc.) identified by instance number, accessed by properties (Present Value, High Limit, Event State, …) |
| **Service model** | Clients read and write properties using BACnet services (ReadProperty, WriteProperty, SubscribeCOV, AcknowledgeAlarm) |
| **Device discovery** | Who-Is/I-Am broadcast locates devices by Device Instance without knowing the IP address |
| **Intrinsic alarming** | Controllers generate Event-Notifications when values cross configured limits — alarm logic lives in the device |
### BACnet variants
| Variant | Transport | Port / Layer | Notes |
| ------------------------ | ------------------ | -------------- | ---------------------------- |
| **BACnet/IP** | UDP/IP | 47808 (0xBAC0) | Most common; fully supported |
| **BACnet MSTP** | Serial RS-485 | N/A | Not supported |
| **BACnet over Ethernet** | Ethernet (Annex H) | — | Not supported |
| **BACnet/SC** | TLS/WebSocket | — | Not currently implemented |
Coreflux supports **BACnet/IP** (UDP). MS/TP parameters are parsed but the transport is not validated. Use BACnet/IP for all production deployments.
### Supported controllers and platforms
| Category | Examples |
| ------------------------- | ---------------------------------------------------------------------- |
| **HVAC controllers** | Siemens Desigo, Johnson Controls Metasys, Honeywell WEBs, Carrier i-Vu |
| **DDC / field panels** | Distech Controls ECB, Alerton Ascent, Delta Controls ORCAview |
| **Meters and sensors** | Veris BMeter, Onset HOBO, Accuenergy Acuvim |
| **Gateways** | Contemporary Controls BASRT-B, Babel Buster (Modbus→BACnet) |
| **Simulators** | IOTech BACnet device simulator (Docker); VTS (Visual Test Shell) |
| **Integration platforms** | Niagara (Tridium), EBO (Schneider) |
***
## Configuration
Only `DEVICE_ID` is strictly required. For a working route, also set `IP` and add at least one `ADD MAPPING` with tags.
```lot wrap focus={2-4} theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
DEFINE ROUTE BMS WITH TYPE BACNET
ADD BACNET_CONFIG
WITH IP "192.168.1.10"
WITH DEVICE_ID 1234
ADD MAPPING RoomSensors
WITH SOURCE_TOPIC "bacnet/bms"
WITH EVERY 5 SECONDS
ADD TAG RoomTemp
WITH ADDRESS "AI:0"
WITH DATA_TYPE "FLOAT"
WITH UNIT "degC"
```
### All configuration options
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
DEFINE ROUTE BMS WITH TYPE BACNET
ADD BACNET_CONFIG
WITH IP "192.168.1.10"
WITH PORT 47808
WITH LOCAL_PORT 0
WITH DEVICE_ID 1234
WITH TIMEOUT 3
WITH DISCOVERY_TIMEOUT 5
WITH DIRECT_IP false
WITH WRITE_PRIORITY 8
WITH RETRIES 3
WITH MAX_APDU 1476
WITH COV_LIFETIME 300
WITH COV_CONFIRMED false
WITH LOCAL_DEVICE_ID 4194300
WITH ALARMS false
WITH ALARMS_TOPIC "bacnet/bms/alarms"
```
### Configuration parameters
| Setting | Required | Default | Description |
| ------------------------- | ----------- | --------------------------- | ---------------------------------------------------------------------------------------------------------------------------- |
| `DEVICE_ID` | **Yes** | `1` | Target BACnet device instance (Who-Is/I-Am and ReadProperty) |
| `IP` / `HOST` / `ADDRESS` | Conditional | `192.168.1.1` | Remote device IPv4 or hostname. **Required** when `WITH DIRECT_IP true`; strongly recommended for Docker and routed networks |
| `PORT` | No | `47808` | Remote device BACnet/IP UDP listen port |
| `LOCAL_PORT` | No | `0` | Local UDP port the broker binds on this host (`0` = OS ephemeral) |
| `TRANSPORT` | No | `IP` | `IP` or `MSTP` (MS/TP not validated — use BACnet/IP) |
| `TIMEOUT` | No | `3` | Read/write timeout in seconds |
| `DISCOVERY_TIMEOUT` | No | `5` | Who-Is/I-Am wait in seconds |
| `DIRECT_IP` | No | `false` | Skip discovery; connect to `IP:PORT` and verify with ReadProperty |
| `WRITE_PRIORITY` | No | `8` | BACnet command priority (1 = highest, 16 = lowest) |
| `RETRIES` | No | `3` | Retry count on failure |
| `MAX_APDU` | No | `1476` | Maximum BACnet/IP frame size in bytes; this is **not** a string-size setting |
| `COV_LIFETIME` | No | `300` | Default COV subscription lifetime in seconds (`0` = infinite) |
| `COV_CONFIRMED` | No | `false` | Request confirmed COV notifications by default |
| `LOCAL_DEVICE_ID` | Conditional | `0` | Local device instance for directed I-Am. **Required** when `WITH ALARMS true` or any TAG has `WITH ALARM true` |
| `ALARMS` | No | `false` | Publish all device Event-Notifications to `ALARMS_TOPIC` |
| `ALARMS_TOPIC` | No | `bacnet/{routeName}/alarms` | MQTT topic for device-wide alarms when `WITH ALARMS true` |
| `ALARM_TOPIC` | No | — | Alias for `ALARMS_TOPIC` on `BACNET_CONFIG` |
### Network setup: IP, PORT, and LOCAL\_PORT
BACnet/IP is UDP. Three settings control two different endpoints — the BACnet device (remote) and the Coreflux broker (local).
| Setting | Side | What it is | Typical value |
| ------------ | ------ | ----------------------------------------------------- | --------------------------------------- |
| `IP` | Remote | IPv4 address or hostname of the BACnet controller | `192.168.1.10`, `coreflux-route-bacnet` |
| `PORT` | Remote | UDP port where the device accepts BACnet/IP traffic | `47808` |
| `LOCAL_PORT` | Local | UDP port the broker binds on before sending/receiving | `0` (OS-assigned) |
| Scenario | `IP` | `PORT` | `LOCAL_PORT` |
| ------------------------------------------------ | ----------------------------------------------- | ------- | ------------ |
| Sandbox simulator on the same host | `127.0.0.1` | `47808` | `0` |
| Simulator in Docker, broker in another container | `coreflux-route-bacnet` (docker container name) | `47808` | `0` |
| Device on non-standard port | `10.0.0.5` | `47809` | `0` |
| Firewall requires fixed source port | `10.0.0.5` | `47808` | `47808` |
`WITH IP` accepts hostnames and Docker service names (for example `coreflux-route-bacnet`, `host.docker.internal`). The broker resolves them to IPv4 before opening the BACnet socket.
### Direct IP mode
Use when the controller does not respond to Who-Is/I-Am but accepts ReadProperty at a known UDP endpoint:
```lot wrap focus={6} theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
DEFINE ROUTE BMS WITH TYPE BACNET
ADD BACNET_CONFIG
WITH IP "10.0.0.50"
WITH PORT 47808
WITH DEVICE_ID 1234
WITH DIRECT_IP true
```
### Write priority
Present-value writes use BACnet priority slots (1 = highest, 16 = lowest). If a device holds a value at a higher priority, a write at a lower priority will not override the active present value. The IOTech sandbox profile seeds `AO:0` at priority **1** — use `WITH WRITE_PRIORITY 1` for that test device.
If a write appears to succeed but the present value does not change, the device may be holding the value at a higher priority. Try `WITH WRITE_PRIORITY 1` and check whether a BMS already owns the point.
***
## TAG System
### Mapping parameters
```lot wrap focus={1,2,3,4} theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
ADD MAPPING RoomSensors
WITH SOURCE_TOPIC "bacnet/bms"
WITH EVERY 5 SECONDS
ADD TAG RoomTemp
WITH ADDRESS "AI:0"
WITH DATA_TYPE "FLOAT"
WITH UNIT "degC"
WITH WRITABLE false
WITH COV false
WITH ALARM false
```
| Parameter | Required | Default | Description |
| -------------- | ----------- | ------- | ------------------------------------------------------------------------------------------------------------------------------ |
| `SOURCE_TOPIC` | **Yes**\* | — | Base MQTT topic; each tag publishes to `{SOURCE_TOPIC}/{tagName}` unless the tag sets its own |
| `EVERY` | Conditional | — | Polling interval (e.g. `1 SECONDS`, `200 MILLISECONDS`). Required when the mapping has polled tags; omit for COV-only mappings |
| `ADD TAG` | **Yes** | — | At least one tag per mapping |
\* You may set `SOURCE_TOPIC` on each tag instead of on the mapping, but one of the two is required.
### Tag parameters
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
ADD TAG RoomTemp
WITH ADDRESS "AI:0"
WITH DATA_TYPE "FLOAT"
WITH UNIT "degC"
WITH WRITABLE false
WITH COV false
WITH ALARM false
```
| Parameter | Required | Default | Description |
| ------------------------- | ----------- | ---------------------------------- | -------------------------------------------------------------------------------- |
| `ADDRESS` | **Yes** | — | BACnet object `TYPE:INSTANCE` (e.g. `AI:0`, `AV:3`, `BI:0`) |
| `DATA_TYPE` | **Yes** | — | `BOOL`, `INT16`, `UINT16`, `INT32`, `UINT32`, `FLOAT`, `DOUBLE`, `STRING` |
| `SOURCE_TOPIC` | Conditional | `{mapping SOURCE_TOPIC}/{tagName}` | MQTT topic for tag data. Required on the tag **or** on the parent mapping |
| `DESTINATION_TOPIC` | Conditional | — | MQTT topic to subscribe for writes. Required when `WITH WRITABLE true` |
| `ADDRESS_TYPE` | No | `PRESENT_VALUE` | BACnet property to read/write (e.g. `OBJECT_NAME`, `HIGH_LIMIT`) |
| `LENGTH` | No | `256` | Maximum string size in bytes when `DATA_TYPE` is `STRING`. Alias: `STRING_SIZE`. |
| `WRITABLE` | No | `false` | Allow MQTT writes to this object |
| `SCALING` | No | `1.0` | Multiply raw value |
| `OFFSET` | No | `0.0` | Add after scaling |
| `UNIT` | No | `""` | Engineering unit label (e.g. `degC`, `%RH`) |
| `DECIMAL_PLACES` | No | `2` | Rounding precision |
| `DEADBAND` | No | `0.0` | Minimum change before republishing |
| `MIN_VALUE` / `MAX_VALUE` | No | — | Clamp published values |
| `PUBLISH_MODE` | No | `VALUE_ONLY` | `VALUE_ONLY` or `JSON` |
| `DESCRIPTION` | No | `""` | Documentation string only |
| `COV` | No | `false` | Subscribe to BACnet Change-of-Value instead of polling |
| `COV_LIFETIME` | No | route `COV_LIFETIME` | Per-tag COV subscription lifetime in seconds |
| `COV_CONFIRMED` | No | route `COV_CONFIRMED` | Request confirmed COV for this tag |
| `ALARM` | No | `false` | Enable MQTT alarm stream on `{tag_topic}/alarms` |
| `ALARM_TOPIC` | No | `{tag_topic}/alarms` | Override alarm MQTT topic (only when `WITH ALARM true`) |
### Object address format
| Shorthand | Full name | Example |
| --------- | ------------------ | ------- |
| `AI` | `ANALOG_INPUT` | `AI:0` |
| `AO` | `ANALOG_OUTPUT` | `AO:1` |
| `AV` | `ANALOG_VALUE` | `AV:0` |
| `BI` | `BINARY_INPUT` | `BI:0` |
| `BO` | `BINARY_OUTPUT` | `BO:0` |
| `BV` | `BINARY_VALUE` | `BV:0` |
| `MSV` | `MULTISTATE_VALUE` | `MSV:0` |
`ADDRESS_TYPE` is the **BACnet property** (e.g. `PRESENT_VALUE`, `OBJECT_NAME`), not the object type.
### String sizing
For `STRING` tags, `WITH LENGTH` is the maximum string size in bytes and defaults to **256**. `STRING_SIZE` is accepted as an alias for the same field. Route-level `MAX_APDU` limits the BACnet/IP **frame** size — it does not size strings.
Read an object's name property as a STRING. `WITH LENGTH 64` overrides the 256-byte default to match a shorter BACnet property allocation:
```lot wrap focus={12} theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
DEFINE ROUTE BMS WITH TYPE BACNET
ADD BACNET_CONFIG
WITH IP "192.168.1.10"
WITH DEVICE_ID 1234
ADD MAPPING DeviceInfo
WITH SOURCE_TOPIC "bacnet/bms"
WITH EVERY 30 SECONDS
ADD TAG RoomName
WITH ADDRESS "AI:0"
WITH ADDRESS_TYPE "OBJECT_NAME"
WITH DATA_TYPE "STRING"
WITH LENGTH 64
WITH SOURCE_TOPIC "bacnet/bms/RoomName"
```
### Change-of-Value (COV)
Tags with `WITH COV true` subscribe to BACnet Change-of-Value notifications instead of polling. A mapping with only COV tags does not need `WITH EVERY`. Mixed mappings poll non-COV tags on `EVERY` and receive COV tags via push.
COV requires support on the device side. If you receive no change notifications, confirm the controller supports COV and that `WITH COV true` is set on the tag.
***
## Alarms and Event Notifications
The broker receives BACnet **Event-Notification** messages from the field device and publishes them to MQTT. Alarm limits and Notification Class membership are configured on the controller — the broker only needs the device to send events to its `LOCAL_DEVICE_ID`.
### Alarm message format
Every time the BACnet device reports an alarm or state change, the broker publishes **one JSON object** to the alarm topic. Subscribe to that topic to see events as they happen, or subscribe to `{alarm_topic}/active` for a retained snapshot of alarms that are still open.
**Example** — a room temperature crosses its high limit (`bacnet/bms/RoomTemp/alarms`):
```json wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
{
"route_name": "BMS",
"device_id": 1234,
"address": "AI:0",
"tag_name": "RoomTemp",
"tag_topic": "bacnet/bms/RoomTemp",
"alarm_topic": "bacnet/bms/RoomTemp/alarms",
"object_type": "ANALOG_INPUT",
"instance": 0,
"event_type": "EVENT_OUT_OF_RANGE",
"from_state_short": "NORMAL",
"to_state_short": "HIGH_LIMIT",
"fundamental_state": "OFFNORMAL",
"transition": "TO_OFFNORMAL",
"event_state_to_ack": "HIGH_LIMIT",
"ack_required": true,
"present_value": 28.5,
"bacnet_timestamp": "2026-06-10T15:28:21.000Z",
"timestamp": "2026-06-10T16:28:21.003Z"
}
```
**Fields to know:**
| Field | What it tells you |
| ------------------------------------- | ----------------------------------------------------------------------------------------- |
| `address` | Which BACnet point raised the alarm (e.g. `AI:0` = Analog Input 0) |
| `tag_name` / `tag_topic` | The route TAG and MQTT topic for live values on that point |
| `from_state_short` → `to_state_short` | State change just reported (e.g. `NORMAL` → `HIGH_LIMIT`) |
| `transition` | Direction of the change: `TO_OFFNORMAL` (new alarm), `TO_NORMAL` (cleared), or `TO_FAULT` |
| `fundamental_state` | Overall condition: `OFFNORMAL` (limit alarm), `FAULT`, or `NORMAL` |
| `present_value` | Value on the device when the event fired (when provided) |
| `ack_required` | If `true`, an operator must acknowledge the alarm on the device |
| `event_state_to_ack` | State to send when acknowledging — use this in ACK payloads |
| `bacnet_timestamp` | When the device recorded the event — include when acknowledging |
**Common `to_state_short` values:**
| Value | Meaning |
| ------------ | ------------------------------------------ |
| `HIGH_LIMIT` | Value rose above the configured high limit |
| `LOW_LIMIT` | Value fell below the configured low limit |
| `FAULT` | Device reported a fault condition |
| `NORMAL` | Point returned to normal (alarm cleared) |
### Setting Alarms on BACnet ROUTE
| Mode | Configuration | Default MQTT topic | Coverage |
| --------------- | ------------------------------------- | --------------------------------------------------- | ---------------------------------------- |
| **Device-wide** | `WITH ALARMS true` on `BACNET_CONFIG` | `bacnet/{routeName}/alarms` (+ retained `…/active`) | Every Event-Notification from the device |
| **Per-tag** | `WITH ALARM true` on TAG | `{tag_topic}/alarms` (+ retained `…/active`) | Only events matching the TAG's `ADDRESS` |
Both modes can coexist. When both are enabled for the same object, the event is published to both topics.
Publish every Event-Notification from the device to a single MQTT topic — useful for commissioning and catch-all monitoring:
```lot wrap focus={6-7} theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
DEFINE ROUTE BMS WITH TYPE BACNET
ADD BACNET_CONFIG
WITH IP "192.168.1.10"
WITH DEVICE_ID 1234
WITH LOCAL_DEVICE_ID 4194300
WITH ALARMS true
WITH ALARMS_TOPIC "bacnet/bms/alarms"
ADD MAPPING CovPoints
WITH SOURCE_TOPIC "bacnet/bms"
ADD TAG CovCounter
WITH ADDRESS "AV:0"
WITH DATA_TYPE "FLOAT"
WITH COV true
```
Alarm events appear on `bacnet/bms/alarms`. Operator ACK via `bacnet/bms/alarms/ack`.
Each alarming point gets its own MQTT alarm stream and ACK topic:
```lot wrap focus={12,17} theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
DEFINE ROUTE BMS WITH TYPE BACNET
ADD BACNET_CONFIG
WITH IP "192.168.1.10"
WITH DEVICE_ID 1234
WITH LOCAL_DEVICE_ID 4194300
ADD MAPPING LimitPoints
WITH SOURCE_TOPIC "bacnet/bms"
ADD TAG RoomTemp
WITH ADDRESS "AI:0"
WITH DATA_TYPE "FLOAT"
WITH COV true
WITH ALARM true
ADD TAG ChillerPressure
WITH ADDRESS "AI:2"
WITH DATA_TYPE "FLOAT"
WITH EVERY 5 SECONDS
WITH ALARM true
```
| Data topic | Alarm topic | ACK topic |
| ---------------------------- | ----------------------------------- | --------------------------------------- |
| `bacnet/bms/RoomTemp` | `bacnet/bms/RoomTemp/alarms` | `bacnet/bms/RoomTemp/alarms/ack` |
| `bacnet/bms/ChillerPressure` | `bacnet/bms/ChillerPressure/alarms` | `bacnet/bms/ChillerPressure/alarms/ack` |
On connect, when alarms are enabled and `LOCAL_DEVICE_ID` is set, the broker sends a **directed I-Am** so Notification Class recipients learn the broker's UDP endpoint.
Set a non-zero `WITH LOCAL_DEVICE_ID` before enabling alarms. Without it, Event-Notifications are unlikely to arrive because the field device does not know where to send them.
### Active alarm list
In addition to live events on `{alarm_topic}`, the broker maintains a **retained** snapshot on `{alarm_topic}/active`. Subscribe here to build an alarm panel — late subscribers receive the current state immediately without waiting for the next transition.
| Topic | Retain | Use for |
| ---------------------- | ------- | -------------------------------------------------------- |
| `{alarm_topic}` | No | Live transitions as they happen (logging, notifications) |
| `{alarm_topic}/active` | **Yes** | Current open alarms (dashboards, alarm panels) |
**How the list updates:**
| Event | Effect on `/active` |
| ------------------------------------- | ---------------------------------------------------------- |
| `TO_OFFNORMAL` or `TO_FAULT` | Add or update the entry for that BACnet `address` |
| `TO_NORMAL` | Remove the entry for that `address` |
| Successful ACK on `{alarm_topic}/ack` | Remove the entry matching `address` + `event_state_to_ack` |
Per-tag lists usually hold **0 or 1** entry (one BACnet object per tag). Device-wide lists hold **one entry per alarming object** on the device.
**Example** — device-wide active list on `bacnet/bms/alarms/active`:
```json wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
{
"route_name": "BMS",
"device_id": 1234,
"alarm_topic": "bacnet/bms/alarms",
"active_topic": "bacnet/bms/alarms/active",
"updated_at": "2026-06-16T12:00:00.000Z",
"count": 2,
"alarms": [
{
"address": "AI:0",
"tag_name": "RoomTemp",
"to_state_short": "HIGH_LIMIT",
"fundamental_state": "OFFNORMAL",
"transition": "TO_OFFNORMAL",
"event_state_to_ack": "HIGH_LIMIT",
"ack_required": true,
"present_value": 28.5,
"bacnet_timestamp": "2026-06-10T15:28:21.000Z"
},
{
"address": "AI:2",
"tag_name": "ChillerPressure",
"to_state_short": "HIGH_LIMIT",
"fundamental_state": "OFFNORMAL",
"transition": "TO_OFFNORMAL",
"event_state_to_ack": "HIGH_LIMIT",
"ack_required": false,
"present_value": 95.2,
"bacnet_timestamp": "2026-06-10T15:30:05.000Z"
}
]
}
```
Each object in the `alarms` array uses the same fields as a single live alarm event. When no alarms are active, `count` is `0` and `alarms` is `[]` — the broker still publishes valid retained JSON.
### Acknowledging alarms
When a BACnet device raises an alarm, the broker publishes it to MQTT. To clear or acknowledge it on the device, an operator publishes to the **ACK topic**. The broker sends `AcknowledgeAlarm` to the controller and replies with a result.
**How it works:**
1. **Subscribe** to the alarm topic (live events) or `{alarm_topic}/active` (current alarm list).
2. **Publish** your acknowledgement to `{alarm_topic}/ack`.
3. **Check** `{alarm_topic}/ack/result` for `success: true` or `false`.
No additional configuration is needed for ACK — the broker registers the handler automatically when you enable device-wide alarms or per-tag `WITH ALARM true`.
| Topic | What you see |
| -------------------------- | --------------------------------------------- |
| `{alarm_topic}` | Live alarm events as they happen |
| `{alarm_topic}/active` | Retained list of alarms that are still active |
| `{alarm_topic}/ack` | **Publish here** to acknowledge |
| `{alarm_topic}/ack/result` | Success or failure of your acknowledgement |
Copy the alarm JSON you received, add an `ack_text` note, and publish it to the ACK topic. This is the safest approach because you keep all fields the device expects (`event_state_to_ack`, `bacnet_timestamp`, and so on).
**1. An alarm arrives** on `bacnet/bms/RoomTemp/alarms`:
```json wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
{
"address": "AI:0",
"tag_name": "RoomTemp",
"to_state_short": "HIGH_LIMIT",
"event_state_to_ack": "HIGH_LIMIT",
"ack_required": true,
"present_value": 28.5,
"bacnet_timestamp": "2026-06-10T15:28:21.000Z"
}
```
**2. Publish to** `bacnet/bms/RoomTemp/alarms/ack` — same JSON, plus your note:
```json wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
{
"address": "AI:0",
"tag_name": "RoomTemp",
"to_state_short": "HIGH_LIMIT",
"event_state_to_ack": "HIGH_LIMIT",
"ack_required": true,
"present_value": 28.5,
"bacnet_timestamp": "2026-06-10T15:28:21.000Z",
"ack_text": "Checked by operator — HVAC team notified"
}
```
Use any MQTT client (such as MQTT Explorer) to publish the payload above to the ACK topic.
**3. Result on** `bacnet/bms/RoomTemp/alarms/ack/result`:
```json wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
{
"success": true,
"route_name": "BMS",
"operation": "ACK",
"address": "AI:0",
"event_state": "HIGH_LIMIT",
"timestamp": "2026-06-10T16:30:00.000Z"
}
```
If `success` is `true`, the device accepted the acknowledgement. The alarm is removed from `{alarm_topic}/active`.
For scripts or integrations that do not store the full alarm JSON, publish a short text command to the ACK topic.
**Publish to** `bacnet/bms/RoomTemp/alarms/ack`:
```text wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
ACK:AI:0:HIGH_LIMIT:Checked by operator
```
Format: `ACK:OBJECT:INSTANCE:STATE:optional note`
| Part | Example | Meaning |
| -------- | --------------------- | --------------------------------- |
| Object | `AI` | Analog Input |
| Instance | `0` | Object instance number |
| State | `HIGH_LIMIT` | Active alarm state to acknowledge |
| Note | `Checked by operator` | Optional operator comment |
Use the **state name** from the alarm (`HIGH_LIMIT`, `LOW_LIMIT`, `FAULT`, or `NORMAL`) — not labels like `OFFNORMAL`.
**Result on** `bacnet/bms/RoomTemp/alarms/ack/result`:
```json wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
{
"success": true,
"route_name": "BMS",
"operation": "ACK",
"address": "AI:0",
"event_state": "HIGH_LIMIT",
"timestamp": "2026-06-10T16:30:00.000Z"
}
```
If `success` is `false`, the state or timestamp may not match what the device expects. Prefer the recommended method when possible — echo the alarm JSON and add `ack_text`.
Before expecting MQTT alarms, confirm on the BMS or engineering tool:
1. **Notification Class** — recipient list includes `LOCAL_DEVICE_ID`
2. **Intrinsic alarming** on each object — `NOTIFICATION_CLASS`, `HIGH_LIMIT` / `LOW_LIMIT`, `EVENT_ENABLE`, `LIMIT_ENABLE`
3. **Present value** actually crosses a configured limit
COV working does **not** guarantee alarms — they use separate BACnet services.
***
## On-Demand Events
Use `ADD EVENT` when you need a single read, write, or alarm acknowledgement triggered by an MQTT publish — instead of continuous TAG polling.
| Parameter | Required | Description |
| ------------------- | -------- | -------------------------------------------------------- |
| `SOURCE_TOPIC` | **Yes** | MQTT topic that triggers the BACnet operation |
| `DESTINATION_TOPIC` | **Yes** | MQTT topic where the JSON result is published |
| `QUERY` | **Yes** | BACnet operation: `READ:…`, `WRITE:…`, `ACK:…`, or JSON |
| `EVERY` | No | Not supported on BACnet events — use TAG polling instead |
### Query formats
| Style | Example | Notes |
| --------------------- | ----------------------------------------------------------------------------------- | ----------------------------------------------------------------------------- |
| Colon-separated read | `READ:AI:0:PRESENT_VALUE` | Shorthand or full type name |
| Colon-separated write | `WRITE:AV:0:PRESENT_VALUE:72.5` | Value is everything after the property |
| JSON | `{"operation":"READ","object_type":"AI","instance":"0","property":"PRESENT_VALUE"}` | Also supports `"operation":"WRITE"` with `"value"` |
| Topic placeholder | `READ:{topic[0]}:0:PRESENT_VALUE` | `{topic[0]}` replaced from the matched MQTT topic segment |
| Dynamic payload | `{payload}` | Use the MQTT message body as the query (e.g. `WRITE:AV:0:PRESENT_VALUE:21.5`) |
Supported operations: **READ**, **WRITE**, and **ACK** (alarm acknowledgement).
```lot wrap focus={5-9} theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
DEFINE ROUTE BMS WITH TYPE BACNET
ADD BACNET_CONFIG
WITH IP "192.168.1.10"
WITH DEVICE_ID 1234
ADD EVENT ReadProperty
WITH SOURCE_TOPIC "bacnet/cmd/read"
WITH DESTINATION_TOPIC "bacnet/cmd/read/result"
WITH QUERY "READ:AI:0:PRESENT_VALUE"
```
Publish any message to `bacnet/cmd/read` and the result appears on `bacnet/cmd/read/result`:
```json wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
{
"success": true,
"route_name": "BMS",
"operation": "READ",
"address": "AI:0",
"property": "PRESENT_VALUE",
"value": 22.5,
"timestamp": "2026-05-31T10:00:00.000Z"
}
```
The route must be connected before events run. If an event returns `"success": false`, check that the device is reachable and that the object and property in the query are correct.
### When to use what
| Need | Recommended approach |
| ---------------------------- | -------------------------------------------------------------------------- |
| Periodic telemetry | `ADD MAPPING` + `ADD TAG` + `WITH EVERY` |
| Write from MQTT | `WITH WRITABLE true` + `WITH DESTINATION_TOPIC` on a tag |
| Ad-hoc read/write on command | `ADD EVENT` + `WITH QUERY` |
| Alarm acknowledgement | Auto-registered handler on `{tag_topic}/alarms/ack`, or custom `ADD EVENT` |
***
## Examples
Reads two analog inputs and one analog value every five seconds:
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
DEFINE ROUTE BMS WITH TYPE BACNET
ADD BACNET_CONFIG
WITH IP "192.168.1.10"
WITH DEVICE_ID 1234
ADD MAPPING RoomSensors
WITH SOURCE_TOPIC "bacnet/bms"
WITH EVERY 5 SECONDS
ADD TAG RoomTemp
WITH ADDRESS "AI:0"
WITH DATA_TYPE "FLOAT"
WITH UNIT "degC"
ADD TAG Humidity
WITH ADDRESS "AI:1"
WITH DATA_TYPE "FLOAT"
WITH UNIT "%RH"
ADD TAG Setpoint
WITH ADDRESS "AV:0"
WITH DATA_TYPE "FLOAT"
WITH UNIT "degC"
```
Publishes to `bacnet/bms/RoomTemp` → `22.5`, `bacnet/bms/Humidity` → `55.0`, `bacnet/bms/Setpoint` → `21.0`.
Subscribe to Change-of-Value for fast-changing points — no polling interval needed:
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
DEFINE ROUTE BMS WITH TYPE BACNET
ADD BACNET_CONFIG
WITH IP "192.168.1.10"
WITH DEVICE_ID 1234
WITH COV_LIFETIME 600
ADD MAPPING CovPoints
WITH SOURCE_TOPIC "bacnet/bms/cov"
ADD TAG RoomTempCov
WITH ADDRESS "AI:0"
WITH DATA_TYPE "FLOAT"
WITH COV true
ADD TAG OccupancyStatus
WITH ADDRESS "BI:0"
WITH DATA_TYPE "BOOL"
WITH COV true
```
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
DEFINE ROUTE BMS WITH TYPE BACNET
ADD BACNET_CONFIG
WITH IP "192.168.1.10"
WITH DEVICE_ID 1234
WITH LOCAL_DEVICE_ID 4194300
WITH ALARMS true
WITH ALARMS_TOPIC "bacnet/bms/alarms"
ADD MAPPING CovPoints
WITH SOURCE_TOPIC "bacnet/bms"
ADD TAG CovCounter
WITH ADDRESS "AV:0"
WITH DATA_TYPE "FLOAT"
WITH COV true
```
Alarm events appear on `bacnet/bms/alarms`. Operator ACK via `bacnet/bms/alarms/ack`.
Connects to the IOTech BACnet Docker simulator when the broker runs in another container:
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
DEFINE ROUTE Sandbox WITH TYPE BACNET
ADD BACNET_CONFIG
WITH IP "coreflux-route-bacnet"
WITH PORT 47808
WITH DEVICE_ID 1234
WITH DIRECT_IP true
ADD MAPPING TestPoints
WITH SOURCE_TOPIC "bacnet/sandbox"
WITH EVERY 2 SECONDS
ADD TAG RoomTemp
WITH ADDRESS "AI:0"
WITH DATA_TYPE "FLOAT"
ADD TAG AnalogOut
WITH ADDRESS "AO:0"
WITH DATA_TYPE "FLOAT"
WITH WRITABLE true
WITH DESTINATION_TOPIC "bacnet/sandbox/AnalogOut/set"
```
***
## Combining with Models and Actions
A BACnet route publishes each point to its own MQTT topic, so the rest of LoT can use that data like any other topic.
First, the route publishes each point under `bacnet/bms`:
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
DEFINE ROUTE BMS WITH TYPE BACNET
ADD BACNET_CONFIG
WITH IP "192.168.1.10"
WITH DEVICE_ID 1234
ADD MAPPING RoomSensors
WITH SOURCE_TOPIC "bacnet/bms"
WITH EVERY 5 SECONDS
ADD TAG RoomTemp
WITH ADDRESS "AI:0"
WITH DATA_TYPE "FLOAT"
ADD TAG Humidity
WITH ADDRESS "AI:1"
WITH DATA_TYPE "FLOAT"
ADD TAG Setpoint
WITH ADDRESS "AV:0"
WITH DATA_TYPE "FLOAT"
```
Then the model aggregates those topics into one object on `bacnet/bms/Info`:
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
DEFINE MODEL BacnetRoomSensors COLLAPSED WITH TOPIC "bacnet/bms/Info"
ADD DOUBLE "RoomTemp" WITH TOPIC "bacnet/bms/RoomTemp" AS TRIGGER
ADD DOUBLE "Humidity" WITH TOPIC "bacnet/bms/Humidity"
ADD DOUBLE "Setpoint" WITH TOPIC "bacnet/bms/Setpoint"
ADD STRING "TimeStamp" WITH TIMESTAMP "ISO"
```
An action can watch per-tag alarm topics and forward events:
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
DEFINE ACTION BmsAlarmMonitor
ON TOPIC "bacnet/bms/+/alarms" DO
PUBLISH TOPIC "site/alarm/log" WITH PAYLOAD
PUBLISH TOPIC "site/alarm/last" WITH PAYLOAD
```
***
## Best Practices
The default `192.168.1.1` is rarely correct. On Docker or routed networks, broadcast Who-Is may never reach the device. Set `WITH IP` to the controller's address or hostname.
If a device does not answer Who-Is/I-Am, set `WITH DIRECT_IP true` to connect to `IP:PORT` and verify with ReadProperty. Otherwise the route waits on discovery timeout every connect.
Polling at one-second intervals wastes bandwidth on BACnet/IP. Use `WITH COV true` for occupancy sensors, fast process variables, and alarm-related points.
The broker must announce itself as a BACnet device via directed I-Am so the field device knows where to send Event-Notifications.
If a BMS holds a point at priority 1, a write at priority 8 will not change the present value. Match priority to your application and verify on the device.
COV-only mappings do not need a polling interval. Adding `WITH EVERY` on a COV-only mapping adds confusion without benefit.
Per-tag alarms (`WITH ALARM true`) give each monitoring point its own ACK topic. Use device-wide (`WITH ALARMS true`) as a catch-all or for commissioning.
Include `event_state_to_ack` and `bacnet_timestamp` from the received alarm message. Wrong state or missing timestamp causes ACK to fail.
***
## Troubleshooting
### Connection check
Publish `-checkRouteConnection BMS` to `$SYS/Coreflux/Command`. The broker returns a JSON status for the named route indicating whether the BACnet device was reached.
| Symptom | Check | Solution |
| ---------------------------------- | -------------------------------- | ------------------------------------------------------------------ |
| Route never connects | `IP` and `DEVICE_ID` correct? | Verify device IP; use Wireshark to confirm Who-Is packet sent |
| Device not discovered | Device answers broadcast Who-Is? | Set `WITH DIRECT_IP true` if the device does not respond to Who-Is |
| Docker: route cannot reach device | `WITH IP "127.0.0.1"` in Docker? | Use the container hostname (e.g. `coreflux-route-bacnet`) |
| Discovery timeout on every connect | `DISCOVERY_TIMEOUT` too low? | Increase to 10+ seconds for slow devices |
| Symptom | Check | Solution |
| ------------------------------ | ------------------------- | ----------------------------------------------------------------- |
| No MQTT values | `DEVICE_ID` match device? | Sandbox default is `1234` — confirm with device profile |
| Wrong object value | Instance number correct? | Verify object index in device BACnet browser or profile |
| Write has no effect | Priority conflict? | Try `WITH WRITE_PRIORITY 1`; check if BMS holds a higher priority |
| COV notifications not arriving | Device supports COV? | Check device profile; simulator may require `--cov` flag |
| Symptom | Check | Solution |
| ---------------------------- | ---------------------------------------------- | --------------------------------------------------------------------------- |
| No MQTT alarms (COV works) | `LOCAL_DEVICE_ID` set? | Add non-zero `LOCAL_DEVICE_ID`; verify device Notification Class recipient |
| Alarms on wrong topic | `SOURCE_TOPIC` or `ALARM_TOPIC` misconfigured? | Default is `{SOURCE_TOPIC}/{tagName}/alarms` |
| ACK returns `success: false` | Wrong `event_state_to_ack`? | Echo the received alarm JSON with `ack_text` field added |
| Events only after reconnect | Directed I-Am not sent? | Ensure `LOCAL_DEVICE_ID` is set and alarms are enabled before first connect |
| Symptom | Solution |
| -------------------------------- | ---------------------------------------------------------------------------------------------- |
| `PORT` vs `LOCAL_PORT` confusion | `PORT` is the device's listen port (47808). `LOCAL_PORT` is the broker's bind port (leave `0`) |
| Event returns `success: false` | Route not connected, or wrong object/property in `QUERY`; check broker logs |
| `LOCAL_DEVICE_ID is 0` warning | Set a non-zero device instance unique on the BACnet network |
### Additional resources
* [ASHRAE 135 Standard](https://www.ashrae.org/technical-resources/bookstore/bacnet) — official BACnet standard
* [BACnet International](https://www.bacnetinternational.org/) — conformance testing and product listings
***
## Next Steps
Learn the patterns shared across all OT routes.
Connect to cross-platform industrial servers with OPC UA.
# EtherNet/IP Route
Source: https://docs.coreflux.org/latest/lot-language/routes/industrial/ethernetip
Connect to CIP-compatible devices over EtherNet/IP with TAG polling, bit extraction, ARRAY/STRUCT decoding, and bidirectional writes.
## Connect to CIP Devices over EtherNet/IP
The `ETHERNETIP` route connects Coreflux to CIP-compatible devices using EtherNet/IP **explicit messaging** (TCP port 44818). Define an `ETHERNETIP_CONFIG` block, add TAGs with CIP addresses, and publish each data point to its own MQTT topic — with optional scaling, bit extraction, joined attribute reads, and bidirectional writes.
**Like reading labeled drawers in a filing cabinet.** Each CIP address (`Class.Instance.Attribute.Index`) points to a specific property on the device. The route opens the drawer, reads the value, and places it on an MQTT topic — on a schedule or when you send a command.
### When to use this
Use the EtherNet/IP route when you need explicit messaging to CIP objects on Allen-Bradley PLCs, remote I/O modules, VFDs, or other ODVA-compatible devices. Addresses use the numeric CIP path format `ClassID.InstanceID.AttributeID.AttributeIndex`.
For **symbolic tag names** (for example `Program:MainProgram.Temperature`), use the dedicated [Allen-Bradley route](./allen-bradley) instead. The EtherNet/IP route reads and writes via CIP class/instance/attribute paths.
### Key capabilities
* **Explicit messaging** — direct read/write to CIP attributes over TCP (port 44818)
* **TAG-based polling** — per-point MQTT topics with configurable intervals
* **Bit extraction** — `BIT_ADDRESS` to read a single bit from an integer word
* **Joined attribute reads** — combine bytes from two CIP attributes (legacy asset parity)
* **ARRAY and STRUCT decoding** — packed assembly buffers published as JSON arrays or objects
* **Bidirectional writes** — `WRITABLE` scalar tags with `DESTINATION_TOPIC`
* **Engineering units** — `SCALING` / `OFFSET` / `DEADBAND` per tag
***
## Quick Start
Read an analog input point every 500 ms and publish to MQTT:
```lot wrap focus={9-12} theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
DEFINE ROUTE AnalogIO WITH TYPE ETHERNETIP
ADD ETHERNETIP_CONFIG
WITH IP "192.168.1.100"
WITH PORT 44818
WITH POLLING_MS 500
ADD MAPPING AnalogInputs
WITH EVERY 500 MILLISECONDS
ADD TAG AnalogIn1
WITH ADDRESS "102.1.3.0"
WITH DATA_TYPE "FLOAT32"
WITH SOURCE_TOPIC "io/analog/in/1"
```
Poll an assembly output word and accept MQTT writes to update it:
```lot wrap focus={9-15} theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
DEFINE ROUTE AssemblyIO WITH TYPE ETHERNETIP
ADD ETHERNETIP_CONFIG
WITH IP "192.168.1.100"
WITH PORT 44818
ADD MAPPING Outputs
WITH EVERY 500 MILLISECONDS
ADD TAG OutputWord0
WITH ADDRESS "4.104.3.0"
WITH DATA_TYPE "UINT16"
WITH SOURCE_TOPIC "eip/output/word0"
WITH WRITABLE true
WITH DESTINATION_TOPIC "eip/output/word0/set"
```
Extract alarm bit 5 from a UINT16 status attribute, joining a second CIP attribute for legacy device parity:
```lot wrap focus={12-22} theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
DEFINE ROUTE Kaeser WITH TYPE ETHERNETIP
ADD ETHERNETIP_CONFIG
WITH IP "10.16.7.99"
WITH PORT 44818
WITH POLLING_MS 2000
ADD MAPPING KaeserData
WITH EVERY 1000 MILLISECONDS
ADD TAG C1_Alarm
WITH ADDRESS "4.100.3.1"
WITH DATA_TYPE "BOOL"
WITH BIT_ADDRESS 5
WITH LENGTH 1
WITH ENABLE_JOIN_ATTRIBUTE "true"
WITH JOIN_CLASS_ID 120
WITH JOIN_INSTANCE_ID 3711
WITH JOIN_ATTRIBUTE_ID 1
WITH JOIN_INDEX 0
WITH JOIN_LENGTH 1
WITH SOURCE_TOPIC "Air/Kaeser/Compressor/C1/Alerts/Alarm/Status"
```
Decode a packed INT32 array from an assembly buffer:
```lot wrap focus={9-14} theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
DEFINE ROUTE AssemblyArray WITH TYPE ETHERNETIP
ADD ETHERNETIP_CONFIG
WITH IP "192.168.1.100"
WITH PORT 44818
ADD MAPPING PackedData
WITH EVERY 1 SECOND
ADD TAG CounterArray
WITH ADDRESS "4.104.1.0"
WITH DATA_TYPE "ARRAY"
WITH ARRAY_ELEMENT_TYPE "INT32"
WITH ARRAY_SIZE 5
WITH SOURCE_TOPIC "eip/assembly/counters"
```
***
## Connection Configuration
### ETHERNETIP\_CONFIG Parameters
| Parameter | Required | Default | Description |
| -------------------- | -------- | ------- | -------------------------------------------- |
| `IP` | Yes | — | Device IP address |
| `PORT` | No | `44818` | EtherNet/IP explicit messaging port (TCP) |
| `POLLING_MS` | No | `750` | Default polling interval in milliseconds |
| `CONNECTION_TIMEOUT` | No | `5000` | Connection timeout in milliseconds |
| `READ_WRITE_TIMEOUT` | No | `3000` | Read/write operation timeout in milliseconds |
| `RETRY_ATTEMPTS` | No | `3` | Retry count on failure (0–10) |
| `RETRY_TIME_SECONDS` | No | `5` | Delay between retries in seconds |
The config block may be declared as `ADD ETHERNETIP_CONFIG` or `ADD EIP_CONFIG` — both are equivalent.
### Connection Example
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
ADD ETHERNETIP_CONFIG
WITH IP "192.168.1.100"
WITH PORT 44818
WITH POLLING_MS 500
WITH CONNECTION_TIMEOUT 5000
WITH READ_WRITE_TIMEOUT 3000
WITH RETRY_ATTEMPTS 3
WITH RETRY_TIME_SECONDS 5
```
### Implicit I/O Mode (Forward Open)
**Not implemented in release 2.2.** The route parser accepts `IO_MODE "IMPLICIT"` and related assembly/RPI settings for forward compatibility, but the broker does **not** establish Forward Open / cyclic UDP connections yet. Use explicit CIP assembly attributes (`4.x.y.z` paths) for I/O adapter devices until implicit mode ships.
Planned parameters (validated when present, not yet wired to the adapter):
| Parameter | Default | Description |
| ----------------- | ---------- | ---------------------------------------------------------------------- |
| `IO_MODE` | `EXPLICIT` | `EXPLICIT` (implemented) or `IMPLICIT` (planned) |
| `INPUT_ASSEMBLY` | `103` | T→O assembly instance (implicit mode) |
| `OUTPUT_ASSEMBLY` | `104` | O→T assembly instance (implicit mode) |
| `CONFIG_ASSEMBLY` | `1` | Configuration assembly for the connection path |
| `INPUT_SIZE` | `20` | Input assembly size in bytes |
| `OUTPUT_SIZE` | `12` | Output assembly size in bytes |
| `RPI` | `100000` | Requested Packet Interval in **microseconds** (e.g. `100000` = 100 ms) |
***
## CIP Addressing
EtherNet/IP uses CIP (Common Industrial Protocol) addressing. The adapter issues CIP `Get_Attribute_Single` / `Set_Attribute_Single` against numeric paths.
**Required format:** `ClassID.InstanceID.AttributeID.AttributeIndex` — four numeric segments, dot-separated.
| Component | Description | Example |
| -------------- | ------------------------------------------------------------------------- | ------------------------------ |
| ClassID | CIP object class | `4` (Assembly), `1` (Identity) |
| InstanceID | Object instance | `100`, `1` |
| AttributeID | Attribute number | `3` (Data on Assembly objects) |
| AttributeIndex | **Byte offset** within the attribute buffer where this tag's slice starts | `0` |
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
# Assembly attribute, full buffer from byte 0
WITH ADDRESS "4.100.3.0"
# Slice starting at byte 2 inside attribute 3 (packed bits)
WITH ADDRESS "4.110.1.2"
# Writable INT32 at instance 100, attribute 4
WITH ADDRESS "4.100.4.0"
```
### Common CIP Classes
| Class ID | Name | Description |
| -------- | --------------------- | -------------------------------------- |
| 1 | Identity | Device identification |
| 2 | Message Router | Message routing |
| 4 | Assembly | I/O data assemblies |
| 6 | Connection Manager | Connection handling |
| 100 | Program Tags | Program-scoped tags (Allen-Bradley) |
| 107 | Controller Tags | Controller-scoped tags (Allen-Bradley) |
| 102 | Analog Input Point | Analog inputs |
| 103 | Analog Output Point | Analog outputs |
| 104 | Discrete Input Point | Digital inputs |
| 105 | Discrete Output Point | Digital outputs |
### EtherNet/IP Communication Types
| Type | Protocol | Port | Use case |
| ------------------ | -------- | ----- | ------------------------------------------------- |
| Explicit messaging | TCP | 44818 | CIP read/write — **used by this route** |
| Implicit messaging | UDP | 2222 | Cyclic real-time I/O — **not implemented in 2.2** |
***
## Data Types
| Data Type | Aliases | Size / format |
| --------- | ----------------- | -------------------------------------------------------------- |
| `BOOLEAN` | `BOOL` | 1 bit |
| `BYTE` | `USINT` | 8 bits |
| `SINT` | — | 8 bits (signed) |
| `INT16` | `INT` | 16 bits |
| `UINT16` | `UINT`, `WORD` | 16 bits |
| `INT32` | `DINT` | 32 bits |
| `UINT32` | `UDINT`, `DWORD` | 32 bits |
| `INT64` | `LINT` | 64 bits |
| `UINT64` | `ULINT`, `LWORD` | 64 bits |
| `FLOAT32` | `REAL`, `FLOAT` | 32 bits |
| `FLOAT64` | `LREAL`, `DOUBLE` | 64 bits — MQTT values round to `DECIMAL_PLACES` (default `2`) |
| `CHAR` | — | Single ASCII character |
| `ASCII` | — | Alias for single-byte ASCII (same as `CHAR` on read) |
| `STRING` | — | CIP string: 2-byte `UINT16` length prefix + ASCII payload |
| `TIME` | — | 32-bit signed duration in milliseconds |
| `TOD` | `TIME_OF_DAY` | 32-bit ms since midnight → `hh:mm:ss.fff` |
| `DATE` | — | 32-bit Unix-style date |
| `WCHAR` | — | 16-bit wide character |
| `ARRAY` | — | Packed elements — requires `ARRAY_ELEMENT_TYPE` + `ARRAY_SIZE` |
| `STRUCT` | — | Packed fields — requires `LENGTH` + `STRUCT_FIELD_DEFINITIONS` |
### String sizing
For `STRING` tags, `WITH LENGTH` sets the maximum string size in bytes. `STRING_SIZE` is accepted only as a parser alias for `STRING` sizing.
***
### Allen-Bradley Type Mapping
| Allen-Bradley | Coreflux `DATA_TYPE` |
| ------------- | -------------------- |
| BOOL | `BOOL` |
| SINT / USINT | `BYTE` |
| INT | `INT16` |
| DINT | `INT32` |
| LINT | `INT64` |
| UINT | `UINT16` |
| UDINT | `UINT32` |
| ULINT | `UINT64` |
| REAL | `FLOAT32` |
| LREAL | `FLOAT64` |
| STRING | `STRING` |
***
## TAG Configuration
Every polled data point is an `ADD TAG` inside an `ADD MAPPING` block. Tags without their own `SOURCE_TOPIC` inherit `{mapping SOURCE_TOPIC}/{tagName}` when the mapping sets a base topic.
### Complete TAG Example
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
ADD TAG ProcessTemperature
WITH ADDRESS "102.1.3.0"
WITH DATA_TYPE "FLOAT32"
WITH SOURCE_TOPIC "eip/process/temperature"
WITH SCALING_FACTOR 0.1
WITH OFFSET 0
WITH DEADBAND 0.5
WITH PUBLISH_MODE "JSON"
WITH LENGTH 1
```
### TAG Parameters
CIP path: `ClassID.InstanceID.AttributeID.AttributeIndex`
CIP data type (see table above).
Bytes to read from the primary CIP attribute starting at `AttributeIndex` (the fourth segment of `ADDRESS`). Default: `1`. For `STRING` tags, this is the maximum string size in bytes; `STRING_SIZE` is accepted only as a parser alias for that `STRING` sizing (not for join, `STRUCT`, or generic attribute-width uses). For numeric tags in join mode, `LENGTH + JOIN_LENGTH` must equal the byte width of `DATA_TYPE` (for example `1 + 1 = 2` for `INT16`).
Extract a single bit (0-based) from the underlying integer word. Alias: `BIT`. Use with integer types (`UINT16`, `INT32`, etc.) or with `BOOL` when the device returns a packed word. For `BOOL` + `BIT_ADDRESS`, read width auto-widens: bits 0–7 read 1 byte, 8–15 read 2 bytes, 16–31 read 4 bytes, 32–63 read 8 bytes.
When `"true"`, perform a second CIP `Get_Attribute_Single` against the join target and concatenate those bytes onto the primary read before parsing, scaling, or bit extraction. Default: `false`.
CIP Class ID of the joined attribute. Required when `ENABLE_JOIN_ATTRIBUTE` is `true` (must be > 0).
CIP Instance ID of the joined attribute. Required when join is enabled (must be >= 0; instance `0` is valid on some devices).
CIP Attribute ID of the joined attribute. Required when join is enabled (must be > 0).
Byte offset within the joined CIP response where the slice starts. Default: `0`.
Bytes to take from the joined attribute starting at `JOIN_INDEX`. Required when join is enabled (must be > 0). **Not** related to `SCALING_FACTOR`.
Multiply the parsed numeric value after byte parsing. Alias: `SCALING_FACTOR`. Default: `1.0`. **Unrelated to byte layout** — does not replace `JOIN_LENGTH`.
Add after scaling. Default: `0.0`.
Suppress MQTT publish when the change is below this threshold. Default: `0.0`.
Decimal places for `FLOAT32` / `FLOAT64` MQTT payloads. Default: `2`.
MQTT topic where polled values are published.
`VALUE_ONLY` (default — publish just the value) or `JSON` (value, quality, and timestamp). Aliases: `VALUE`, `SIMPLE` for value-only; `FULL`, `METADATA` for JSON.
When `true`, subscribe to `DESTINATION_TOPIC` and write incoming MQTT values to the device.
MQTT topic to receive write commands. Required when `WRITABLE` is `true`.
### Publish Modes
**VALUE\_ONLY** (default) — publishes just the scaled scalar value:
```
75.5
```
**JSON** — publishes a metadata wrapper when explicitly set:
```json wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
{
"tag_name": "AnalogIn1",
"value": 75.5,
"quality": "GOOD",
"timestamp": "2025-12-17T10:30:45.123Z"
}
```
### ARRAY and STRUCT Tags
CIP assembly buffers often contain packed arrays or structured records. Use `DATA_TYPE "ARRAY"` or `DATA_TYPE "STRUCT"` to decode them into JSON on MQTT.
#### ARRAY
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
ADD TAG ArrInt32
WITH ADDRESS "4.104.1.0"
WITH DATA_TYPE "ARRAY"
WITH ARRAY_ELEMENT_TYPE "INT32"
WITH ARRAY_SIZE 5
WITH SOURCE_TOPIC "eip/complex/arr_int32"
```
| Parameter | Required | Description |
| --------------------------- | -------- | ---------------------------------------------------------------- |
| `ARRAY_ELEMENT_TYPE` | Yes | Element type (`INT32`, `FLOAT`, `BOOLEAN`, etc.) |
| `ARRAY_SIZE` | Yes | Number of elements |
| `ARRAY_ELEMENT_LENGTH` | No | Byte width per element (for `STRING` / nested `STRUCT` elements) |
| `ARRAY_START_OFFSET` | No | Skip N bytes before the first element |
| `ARRAY_ELEMENT_HEADER_SIZE` | No | Skip N bytes at the start of each element |
**MQTT payload:** JSON array, e.g. `[10, 20, 30, 40, 50]`.
#### STRUCT
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
ADD TAG StructTag
WITH ADDRESS "4.105.1.0"
WITH DATA_TYPE "STRUCT"
WITH LENGTH 12
WITH STRUCT_FIELD_DEFINITIONS "temp:0:FLOAT:4,pressure:4:FLOAT:4,status:8:INT32:4"
WITH SOURCE_TOPIC "eip/complex/struct"
```
Field spec format: `name:byte_offset:type:size` (comma-separated). Types follow the same aliases as scalar `DATA_TYPE`.
**MQTT payload:** JSON object, e.g. `{"temp":25.5,"pressure":101.325,"status":1}`.
ARRAY and STRUCT tags are **read-focused** in release 2.2. Writable scalar tags use normal CIP encoding; complex-type writes from JSON arrays/objects are not yet supported.
### Joined Attribute Reads
Some legacy CIP devices split a logical value across two attributes (for example a status word plus an extension attribute). When `ENABLE_JOIN_ATTRIBUTE` is `"true"`, the broker performs two `Get_Attribute_Single` reads per cycle, concatenates the byte buffers (primary first, joined second), then parses, scales, and applies `BIT_ADDRESS`.
If the join read fails, a warning is logged and the primary buffer is parsed alone — the polling loop continues.
For numeric `DATA_TYPE` values in join mode, `LENGTH + JOIN_LENGTH` must equal the type byte width (`2` for 16-bit, `4` for 32-bit, `8` for 64-bit). `BOOL` tags with `BIT_ADDRESS` are exempt (Kaeser-style alarm bit pattern).
`SCALING` / `SCALING_FACTOR` multiplies the parsed value — it does **not** replace `JOIN_LENGTH`. Using scaling to compensate for a missing join length produces silently wrong values.
***
## Migrating from a Legacy EtherNet/IP Asset
If you are converting an EtherNet/IP **Asset** (Coreflux HUB Configure / Tags panel) into a Route, map each asset field to the LoT parameter below.
| Asset field (HUB) | Route LoT parameter | Notes |
| ------------------------------------------------------- | ----------------------------------------------------------- | ----------------------------------------------- |
| Class Id / Instance Id / Attribute Id / Attribute Index | `WITH ADDRESS "C.I.A.Index"` | Four numeric segments separated by `.` |
| Data Type | `WITH DATA_TYPE ""` | See aliases above |
| Bit | `WITH BIT_ADDRESS ` (alias `WITH BIT `) | For boolean bits packed in an integer word |
| Length | `WITH LENGTH ` | Bytes from the primary attribute |
| Enable Join Attribute | `WITH ENABLE_JOIN_ATTRIBUTE "true"` | Quoted `"true"` / `"false"` |
| Join Class / Instance / Attribute Id | `JOIN_CLASS_ID`, `JOIN_INSTANCE_ID`, `JOIN_ATTRIBUTE_ID` | |
| Join Index | `WITH JOIN_INDEX ` | Byte offset in joined response |
| Join Length | `WITH JOIN_LENGTH ` | Bytes from joined attribute — **not** `SCALING` |
| Topic (Source) | `WITH SOURCE_TOPIC ""` | Where polled values publish |
| Write Direction | `WITH WRITABLE "true"` + `WITH DESTINATION_TOPIC ""` | For MQTT-to-device writes |
***
## Examples
Read identity object attributes on a slow interval:
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
DEFINE ROUTE DeviceIdentity WITH TYPE ETHERNETIP
ADD ETHERNETIP_CONFIG
WITH IP "192.168.1.100"
WITH PORT 44818
ADD MAPPING IdentityData
WITH EVERY 30 SECONDS
ADD TAG VendorID
WITH ADDRESS "1.1.1.0"
WITH DATA_TYPE "UINT16"
WITH SOURCE_TOPIC "device/identity/vendor"
ADD TAG SerialNumber
WITH ADDRESS "1.1.6.0"
WITH DATA_TYPE "UINT32"
WITH SOURCE_TOPIC "device/identity/serial"
ADD TAG ProductName
WITH ADDRESS "1.1.7.0"
WITH DATA_TYPE "STRING"
WITH LENGTH 32
WITH SOURCE_TOPIC "device/identity/name"
```
Read a 4–20 mA value with engineering-unit scaling and deadband:
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
DEFINE ROUTE AnalogInputs WITH TYPE ETHERNETIP
ADD ETHERNETIP_CONFIG
WITH IP "192.168.1.40"
ADD MAPPING AnalogData
WITH EVERY 500 MILLISECONDS
ADD TAG TankLevel
WITH ADDRESS "102.1.3.0"
WITH DATA_TYPE "INT16"
WITH SCALING_FACTOR 0.00625
WITH OFFSET -25.0
WITH DEADBAND 1.0
WITH SOURCE_TOPIC "tank/level"
```
Three boolean status bits from one INT16 word with joined attributes (legacy asset parity):
```lot wrap focus={12-28} theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
DEFINE ROUTE Kaeser WITH TYPE ETHERNETIP
ADD ETHERNETIP_CONFIG
WITH IP "10.16.7.99"
WITH PORT 44818
WITH POLLING_MS 2000
ADD MAPPING KaeserData
WITH EVERY 1000 MILLISECONDS
ADD TAG C1_On
WITH ADDRESS "4.100.3.1"
WITH DATA_TYPE "BOOL"
WITH SOURCE_TOPIC "Air/Kaeser/Compressor/C1/On"
ADD TAG C1_Alarm
WITH ADDRESS "4.100.3.1"
WITH DATA_TYPE "BOOL"
WITH BIT 5
WITH LENGTH 1
WITH ENABLE_JOIN_ATTRIBUTE "true"
WITH JOIN_CLASS_ID 120
WITH JOIN_INSTANCE_ID 3711
WITH JOIN_ATTRIBUTE_ID 1
WITH JOIN_INDEX 0
WITH JOIN_LENGTH 1
WITH SOURCE_TOPIC "Air/Kaeser/Compressor/C1/Alerts/Alarm/Status"
ADD TAG C1_Alert
WITH ADDRESS "4.100.3.1"
WITH DATA_TYPE "BOOL"
WITH BIT 6
WITH LENGTH 1
WITH ENABLE_JOIN_ATTRIBUTE "true"
WITH JOIN_CLASS_ID 120
WITH JOIN_INSTANCE_ID 3711
WITH JOIN_ATTRIBUTE_ID 1
WITH JOIN_INDEX 0
WITH JOIN_LENGTH 1
WITH SOURCE_TOPIC "Air/Kaeser/Compressor/C1/Alerts/Alert/Status"
```
Fast critical data and slow diagnostics in one route:
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
DEFINE ROUTE MultiRateEIP WITH TYPE ETHERNETIP
ADD ETHERNETIP_CONFIG
WITH IP "192.168.1.100"
WITH PORT 44818
ADD MAPPING FastData
WITH EVERY 100 MILLISECONDS
ADD TAG CriticalStatus
WITH ADDRESS "104.1.3.0"
WITH DATA_TYPE "BOOLEAN"
WITH SOURCE_TOPIC "fast/status"
ADD MAPPING SlowData
WITH EVERY 5 SECONDS
ADD TAG DeviceStatus
WITH ADDRESS "1.1.5.0"
WITH DATA_TYPE "UINT16"
WITH SOURCE_TOPIC "slow/device_status"
```
Decode packed assembly data as JSON:
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
DEFINE ROUTE ComplexAssembly WITH TYPE ETHERNETIP
ADD ETHERNETIP_CONFIG
WITH IP "192.168.1.100"
WITH PORT 44818
ADD MAPPING PackedData
WITH EVERY 1 SECOND
ADD TAG TempArray
WITH ADDRESS "4.104.1.0"
WITH DATA_TYPE "ARRAY"
WITH ARRAY_ELEMENT_TYPE "INT32"
WITH ARRAY_SIZE 5
WITH SOURCE_TOPIC "eip/array/counters"
ADD TAG ProcessStruct
WITH ADDRESS "4.105.1.0"
WITH DATA_TYPE "STRUCT"
WITH LENGTH 12
WITH STRUCT_FIELD_DEFINITIONS "temp:0:FLOAT:4,pressure:4:FLOAT:4,status:8:INT32:4"
WITH SOURCE_TOPIC "eip/struct/process"
```
***
## CIP Object Reference
| Attribute | Description | Data Type |
| --------- | ------------- | ---------- |
| 1 | Vendor ID | UINT16 |
| 2 | Device Type | UINT16 |
| 3 | Product Code | UINT16 |
| 4 | Revision | UINT16\[2] |
| 5 | Status | UINT16 |
| 6 | Serial Number | UINT32 |
| 7 | Product Name | STRING |
| Attribute | Description | Data Type |
| --------- | --------------- | --------------- |
| 3 | Value | FLOAT32 / INT16 |
| 4 | Status | UINT16 |
| 5 | Scaling Minimum | FLOAT32 |
| 6 | Scaling Maximum | FLOAT32 |
| Attribute | Description | Data Type |
| --------- | ----------- | --------- |
| 3 | Value | BOOLEAN |
| 4 | Status | UINT16 |
| Attribute | Description | Data Type |
| --------- | ----------- | --------- |
| 3 | Data | Variable |
| 4 | Size | UINT16 |
***
## Best Practices
Match the interval to how fast the value changes: 100 ms for safety-critical data, 1 s for process variables, 10 s or more for slow-changing status.
Use `DEADBAND` on noisy analog values to reduce MQTT traffic. Set `0.0` on digital status tags so every change publishes.
Increase `CONNECTION_TIMEOUT` and `READ_WRITE_TIMEOUT` on congested or remote networks. Raise `RETRY_ATTEMPTS` when connections are intermittent.
Always set `JOIN_LENGTH` explicitly when `ENABLE_JOIN_ATTRIBUTE` is `"true"`. Verify `LENGTH + JOIN_LENGTH` matches the numeric type width before deploying to production.
Use numeric CIP paths on this route. For Rockwell tag-name access (`Program:MainProgram.Counter`), use the [Allen-Bradley route](./allen-bradley).
***
## Troubleshooting
* Verify IP with `ping`
* Confirm port 44818 (TCP) is open through firewalls
* Ensure the device is powered and allows external connections
* Increase `CONNECTION_TIMEOUT` and `RETRY_ATTEMPTS`
* Confirm four-segment format: `ClassID.InstanceID.AttributeID.AttributeIndex`
* Check the device manual for valid class/instance/attribute numbers
* Instance numbering varies by device (often starts at 1, not 0)
* Verify `DATA_TYPE` matches the device attribute
* Check `DEADBAND` is not suppressing small changes
* For join tags, confirm `JOIN_LENGTH` and `LENGTH` byte layout
* For bit tags, confirm `BIT_ADDRESS` is within the read width
* `JOIN_CLASS_ID` and `JOIN_ATTRIBUTE_ID` must be > 0
* `JOIN_INSTANCE_ID` must be present and >= 0
* `JOIN_LENGTH` must be > 0
* For numeric types, `LENGTH + JOIN_LENGTH` must equal the type byte width (unless `BOOL` + `BIT_ADDRESS`)
***
## Next Steps
Symbolic tag-name access for ControlLogix, CompactLogix, and MicroLogix PLCs.
Compare OT routes and choose the right protocol for your devices.
# FINS Route (Omron)
Source: https://docs.coreflux.org/latest/lot-language/routes/industrial/fins
Connect to Omron PLCs using FINS protocol with support for multiple memory areas and TCP/UDP transport
## FINS Overview
The `FINS` route enables communication with Omron PLCs (CJ, CS, CP, and NJ/NX series) using the FINS (Factory Interface Network Service) protocol. It supports both TCP and UDP transport, multiple memory areas, and bit-level access.
FINS is Omron's native protocol providing fast, efficient access to PLC memory. For newer NJ/NX series, consider OPC UA as an alternative.
## Basic Syntax
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
DEFINE ROUTE OmronPLC WITH TYPE FINS
ADD FINS_CONFIG
WITH IP "192.168.1.100"
WITH PORT 9600
WITH PROTOCOL "TCP"
WITH LOCAL_NODE 0
WITH REMOTE_NODE 0
ADD MAPPING ProcessData
WITH EVERY 500 MILLISECONDS
ADD TAG Temperature
WITH ADDRESS "100"
WITH MEMORY_TYPE "DM"
WITH DATA_TYPE "REAL"
WITH SOURCE_TOPIC "plc/temperature"
```
***
## Connection Configuration
### FINS\_CONFIG Parameters
Target Omron device IP address.
FINS protocol port. Default: 9600.
Protocol type: `TCP` or `UDP`. Default: TCP.
Local node address (0-255). Default: 0.
Remote node address - target PLC (0-255). Default: 0.
Connection timeout in seconds. Default: 2.
Number of retries for FINS communication. Default: 5.
Polling interval in milliseconds. Default: 200.
### Connection Examples
Standard TCP connection (recommended):
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
ADD FINS_CONFIG
WITH IP "192.168.1.100"
WITH PORT 9600
WITH PROTOCOL "TCP"
WITH LOCAL_NODE 0
WITH REMOTE_NODE 0
WITH TIMEOUT 2
WITH RETRIES 5
```
TCP transport for FINS is currently in development. Use UDP for production deployments.
UDP for lower latency:
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
ADD FINS_CONFIG
WITH IP "192.168.1.100"
WITH PORT 9600
WITH PROTOCOL "UDP"
WITH LOCAL_NODE 1
WITH REMOTE_NODE 0
WITH TIMEOUT 2
```
UDP requires proper node addressing and may have firewall considerations.
***
## Memory Areas
Omron PLCs organize data in different memory areas:
| Memory Type | Code | Description | Access |
| ----------- | ---- | ------------------------ | ---------- |
| `CIO` | 0 | Core I/O and work area | Read/Write |
| `WR` | 1 | Work area | Read/Write |
| `HR` | 2 | Holding area (retentive) | Read/Write |
| `AR` | 3 | Auxiliary area | Read-only |
| `DM` | 4 | Data Memory | Read/Write |
| `CNT` | 5 | Counter values | Read/Write |
| `TIM` | 6 | Timer values | Read/Write |
| `EM` | 7 | Extended Memory | Read/Write |
DM (Data Memory) is the most commonly used area for user data. CIO is typically used for I/O and internal relays.
***
## Data Types
| Data Type | Description | Size |
| ------------------ | --------------------------------- | -------- |
| `BOOL` | Boolean (single bit) | 1 bit |
| `BYTE` | Unsigned 8-bit | 8 bits |
| `SINT` | Signed 8-bit integer | 8 bits |
| `USINT` | Unsigned 8-bit integer | 8 bits |
| `INT` | Signed 16-bit integer | 16 bits |
| `UINT` / `WORD` | Unsigned 16-bit integer | 16 bits |
| `DINT` | Signed 32-bit integer | 32 bits |
| `UDINT` / `DWORD` | Unsigned 32-bit integer | 32 bits |
| `LINT` | Signed 64-bit integer | 64 bits |
| `ULINT` / `LWORD` | Unsigned 64-bit integer | 64 bits |
| `REAL` / `FLOAT` | 32-bit floating point | 32 bits |
| `LREAL` / `DOUBLE` | 64-bit floating point | 64 bits |
| `CHAR` | Single character | 8 bits |
| `WCHAR` | Wide character | 16 bits |
| `TIME` | Time duration | 32 bits |
| `DATE` | Date value | 16 bits |
| `TOD` | Time of day | 32 bits |
| `ASCII` | ASCII string | 8 bits |
| `STRING` | String value; sized with `LENGTH` | Variable |
### String sizing
For `STRING` tags, `WITH LENGTH` is the string **payload** size in bytes and defaults to **256**. `STRING_SIZE` is accepted as an alias for the same field. The FINS route reserves **two additional header bytes**, so the protocol buffer is `LENGTH + 2` bytes.
Read a product label stored as a STRING in DM memory:
```lot wrap focus={14} theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
DEFINE ROUTE OmronStrings WITH TYPE FINS
ADD FINS_CONFIG
WITH IP "192.168.1.100"
WITH PORT 9600
WITH PROTOCOL "TCP"
WITH LOCAL_NODE 176
WITH REMOTE_NODE 201
ADD MAPPING Labels
WITH EVERY 1 SECOND
ADD TAG ProductLabel
WITH ADDRESS "500"
WITH MEMORY_TYPE "DM"
WITH DATA_TYPE "STRING"
WITH LENGTH 256
WITH SOURCE_TOPIC "production/label"
```
***
## TAG Configuration
### Complete TAG Example
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
ADD TAG ProcessTemperature
WITH ADDRESS "100"
WITH BIT_ADDRESS 0
WITH DATA_TYPE "REAL"
WITH MEMORY_TYPE "DM"
WITH SOURCE_TOPIC "plc/process/temperature"
WITH DESTINATION_TOPIC "plc/process/temperature/write"
WITH WRITABLE "true"
WITH SCALING 0.1
WITH OFFSET 0
WITH UNIT "°C"
WITH DECIMAL_PLACES 2
WITH MIN_VALUE -50
WITH MAX_VALUE 500
WITH DEADBAND 0.5
WITH PUBLISH_MODE "JSON"
WITH DESCRIPTION "Main process temperature sensor"
```
### TAG Parameters
Word address in PLC memory (e.g., 100, 200, 500).
Bit address for bit-specific data types (0-15). Used for BOOL values within a word.
Memory area: `CIO`, `WR`, `HR`, `AR`, `DM`, `CNT`, `TIM`, `EM`. Default: DM.
FINS data type: BOOL, BYTE, INT, UINT, DINT, UDINT, LINT, ULINT, REAL, LREAL, ASCII, STRING.
String payload size in bytes; FINS adds two header bytes (`LENGTH + 2` on the wire). Default: **256**. Alias: `STRING_SIZE`.
Multiplier for value transformation. Default: 1.0.
Offset added after scaling. Default: 0.0.
Decimal places in output. Default: 2.
Minimum allowed value.
Maximum allowed value.
Minimum change to trigger publish. Default: 0.0.
Topic where PLC values are published. Subscribe here to receive sensor data.
Topic to send write commands. Publish a value here to write it to the PLC.
Output format: `VALUE_ONLY` or `JSON`. Default: VALUE\_ONLY.
Engineering unit for documentation.
Human-readable description.
Allow writing to this address. Default: false.
***
## Addressing Examples
### Word Addressing
Access data at word level:
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
ADD TAG DataWord100
WITH ADDRESS "100"
WITH MEMORY_TYPE "DM"
WITH DATA_TYPE "INT"
WITH SOURCE_TOPIC "plc/dm/100"
```
### Bit Addressing
Access individual bits within a word:
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
ADD TAG Bit0
WITH ADDRESS "100"
WITH BIT_ADDRESS 0
WITH MEMORY_TYPE "DM"
WITH DATA_TYPE "BOOL"
WITH SOURCE_TOPIC "plc/dm/100.0"
ADD TAG Bit15
WITH ADDRESS "100"
WITH BIT_ADDRESS 15
WITH MEMORY_TYPE "DM"
WITH DATA_TYPE "BOOL"
WITH SOURCE_TOPIC "plc/dm/100.15"
```
### 32-bit Values
32-bit values span two consecutive words:
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
ADD TAG FloatValue
WITH ADDRESS "200"
WITH MEMORY_TYPE "DM"
WITH DATA_TYPE "REAL"
WITH SOURCE_TOPIC "plc/float"
```
REAL and DINT types use two consecutive words (e.g., DM200 and DM201).
***
## Event-Based Operations
For on-demand FINS operations (not polling), use the EVENT syntax. Publish a message to SOURCE\_TOPIC to trigger the operation; the route executes it and publishes the result to DESTINATION\_TOPIC.
### Supported Operations
| Operation | Description | Query Parameters |
| --------- | --------------------------------- | ---------------------------------------------- |
| `READ` | Read from a memory area on demand | `memory_type`, `address`, `data_type`, `count` |
| `WRITE` | Write a value to a memory area | `memory_type`, `address`, `data_type`, `value` |
### Query Parameters
| Parameter | Description | Example |
| ------------- | -------------------------------------------------------------- | ------------------------------------- |
| `operation` | Operation type: `READ` or `WRITE` | `READ` |
| `memory_type` | Memory area: `DM`, `CIO`, `WR`, `HR`, `AR`, `TIM`, `CNT`, `EM` | `DM` |
| `address` | Word address in the memory area | `100`, `200` |
| `data_type` | FINS data type | `REAL`, `INT`, `UINT`, `DINT`, `BOOL` |
| `count` | Number of consecutive values to read (READ only) | `10` |
| `value` | Value to write (WRITE only) | `25.5`, `100`, `true` |
### Read Example
Read a REAL value from DM area on demand:
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
ADD EVENT ReadDM
WITH SOURCE_TOPIC "fins/commands/read"
WITH DESTINATION_TOPIC "fins/responses/read"
WITH QUERY "{operation: READ, memory_type: DM, address: 100, data_type: REAL}"
```
### Write Example
Write a REAL value to DM area on demand:
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
ADD EVENT WriteDM
WITH SOURCE_TOPIC "fins/commands/write"
WITH DESTINATION_TOPIC "fins/responses/write"
WITH QUERY "{operation: WRITE, memory_type: DM, address: 200, data_type: REAL, value: 25.5}"
```
***
## Complete Examples
Read values from DM area:
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
DEFINE ROUTE OmronBasic WITH TYPE FINS
ADD FINS_CONFIG
WITH IP "192.168.1.100"
WITH PORT 9600
WITH PROTOCOL "TCP"
WITH LOCAL_NODE 0
WITH REMOTE_NODE 0
ADD MAPPING DataMemory
WITH EVERY 500 MILLISECONDS
ADD TAG Temperature
WITH ADDRESS "100"
WITH MEMORY_TYPE "DM"
WITH DATA_TYPE "REAL"
WITH SOURCE_TOPIC "plc/temperature"
WITH SCALING 0.1
WITH UNIT "°C"
ADD TAG Pressure
WITH ADDRESS "102"
WITH MEMORY_TYPE "DM"
WITH DATA_TYPE "REAL"
WITH SOURCE_TOPIC "plc/pressure"
WITH UNIT "bar"
ADD TAG Counter
WITH ADDRESS "104"
WITH MEMORY_TYPE "DM"
WITH DATA_TYPE "DINT"
WITH SOURCE_TOPIC "plc/counter"
```
Read CIO and Work areas:
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
DEFINE ROUTE OmronIO WITH TYPE FINS
ADD FINS_CONFIG
WITH IP "192.168.1.100"
WITH PORT 9600
WITH PROTOCOL "TCP"
ADD MAPPING IOStatus
WITH EVERY 100 MILLISECONDS
ADD TAG DigitalInputs
WITH ADDRESS "0"
WITH MEMORY_TYPE "CIO"
WITH DATA_TYPE "UINT"
WITH SOURCE_TOPIC "plc/io/di"
ADD TAG DigitalOutputs
WITH ADDRESS "100"
WITH MEMORY_TYPE "CIO"
WITH DATA_TYPE "UINT"
WITH SOURCE_TOPIC "plc/io/do"
ADD TAG WorkBits
WITH ADDRESS "0"
WITH MEMORY_TYPE "WR"
WITH DATA_TYPE "UINT"
WITH SOURCE_TOPIC "plc/work"
```
Access individual bits:
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
DEFINE ROUTE OmronBits WITH TYPE FINS
ADD FINS_CONFIG
WITH IP "192.168.1.100"
WITH PORT 9600
WITH PROTOCOL "TCP"
ADD MAPPING BitStatus
WITH EVERY 100 MILLISECONDS
ADD TAG StartButton
WITH ADDRESS "0"
WITH BIT_ADDRESS 0
WITH MEMORY_TYPE "CIO"
WITH DATA_TYPE "BOOL"
WITH SOURCE_TOPIC "plc/buttons/start"
ADD TAG StopButton
WITH ADDRESS "0"
WITH BIT_ADDRESS 1
WITH MEMORY_TYPE "CIO"
WITH DATA_TYPE "BOOL"
WITH SOURCE_TOPIC "plc/buttons/stop"
ADD TAG MotorRunning
WITH ADDRESS "100"
WITH BIT_ADDRESS 0
WITH MEMORY_TYPE "CIO"
WITH DATA_TYPE "BOOL"
WITH SOURCE_TOPIC "plc/motor/running"
WITH WRITABLE "true"
WITH DESTINATION_TOPIC "plc/motor/running/set"
```
Read timer and counter values:
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
DEFINE ROUTE OmronTimerCounter WITH TYPE FINS
ADD FINS_CONFIG
WITH IP "192.168.1.100"
WITH PORT 9600
WITH PROTOCOL "TCP"
ADD MAPPING TimersCounters
WITH EVERY 500 MILLISECONDS
ADD TAG Timer0
WITH ADDRESS "0"
WITH MEMORY_TYPE "TIM"
WITH DATA_TYPE "UINT"
WITH SOURCE_TOPIC "plc/timer/0"
WITH SCALING 0.1
WITH UNIT "s"
ADD TAG Timer1
WITH ADDRESS "1"
WITH MEMORY_TYPE "TIM"
WITH DATA_TYPE "UINT"
WITH SOURCE_TOPIC "plc/timer/1"
ADD TAG Counter0
WITH ADDRESS "0"
WITH MEMORY_TYPE "CNT"
WITH DATA_TYPE "UINT"
WITH SOURCE_TOPIC "plc/counter/0"
ADD TAG Counter1
WITH ADDRESS "1"
WITH MEMORY_TYPE "CNT"
WITH DATA_TYPE "UINT"
WITH SOURCE_TOPIC "plc/counter/1"
```
Read and write process values:
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
DEFINE ROUTE OmronControl WITH TYPE FINS
ADD FINS_CONFIG
WITH IP "192.168.1.100"
WITH PORT 9600
WITH PROTOCOL "TCP"
ADD MAPPING ControlLoop
WITH EVERY 200 MILLISECONDS
ADD TAG ProcessValue
WITH ADDRESS "200"
WITH MEMORY_TYPE "DM"
WITH DATA_TYPE "REAL"
WITH SOURCE_TOPIC "control/pv"
WITH UNIT "°C"
WITH PUBLISH_MODE "JSON"
ADD TAG Setpoint
WITH ADDRESS "202"
WITH MEMORY_TYPE "DM"
WITH DATA_TYPE "REAL"
WITH SOURCE_TOPIC "control/sp"
WITH UNIT "°C"
WITH WRITABLE "true"
WITH DESTINATION_TOPIC "control/sp/write"
WITH MIN_VALUE 0
WITH MAX_VALUE 100
ADD TAG Output
WITH ADDRESS "204"
WITH MEMORY_TYPE "DM"
WITH DATA_TYPE "REAL"
WITH SOURCE_TOPIC "control/output"
WITH UNIT "%"
ADD TAG AutoMode
WITH ADDRESS "206"
WITH BIT_ADDRESS 0
WITH MEMORY_TYPE "DM"
WITH DATA_TYPE "BOOL"
WITH SOURCE_TOPIC "control/auto"
WITH WRITABLE "true"
WITH DESTINATION_TOPIC "control/auto/write"
```
Read retentive data from HR area:
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
DEFINE ROUTE OmronHolding WITH TYPE FINS
ADD FINS_CONFIG
WITH IP "192.168.1.100"
WITH PORT 9600
WITH PROTOCOL "TCP"
ADD MAPPING RetentiveData
WITH EVERY 5 SECONDS
ADD TAG TotalRuntime
WITH ADDRESS "0"
WITH MEMORY_TYPE "HR"
WITH DATA_TYPE "DINT"
WITH SOURCE_TOPIC "plc/stats/runtime"
WITH UNIT "seconds"
ADD TAG ProductionTotal
WITH ADDRESS "2"
WITH MEMORY_TYPE "HR"
WITH DATA_TYPE "DINT"
WITH SOURCE_TOPIC "plc/stats/production"
ADD TAG LastErrorCode
WITH ADDRESS "4"
WITH MEMORY_TYPE "HR"
WITH DATA_TYPE "UINT"
WITH SOURCE_TOPIC "plc/stats/error"
```
Continuous monitoring with on-demand read and write events in the same route:
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
DEFINE ROUTE FullFINSSetup WITH TYPE FINS
ADD FINS_CONFIG
WITH IP "192.168.1.100"
WITH PORT 9600
WITH PROTOCOL "TCP"
WITH LOCAL_NODE 0
WITH REMOTE_NODE 0
ADD MAPPING ProcessData
WITH EVERY 500 MILLISECONDS
ADD TAG Temperature
WITH ADDRESS "100"
WITH MEMORY_TYPE "DM"
WITH DATA_TYPE "REAL"
WITH SOURCE_TOPIC "plc/temperature"
WITH UNIT "°C"
ADD TAG Pressure
WITH ADDRESS "102"
WITH MEMORY_TYPE "DM"
WITH DATA_TYPE "REAL"
WITH SOURCE_TOPIC "plc/pressure"
WITH UNIT "bar"
ADD EVENT ReadDM
WITH SOURCE_TOPIC "fins/commands/read"
WITH DESTINATION_TOPIC "fins/responses/read"
WITH QUERY "{operation: READ, memory_type: DM, address: 200, data_type: REAL}"
ADD EVENT WriteDM
WITH SOURCE_TOPIC "fins/commands/write"
WITH DESTINATION_TOPIC "fins/responses/write"
WITH QUERY "{operation: WRITE, memory_type: DM, address: 300, data_type: REAL, value: 0}"
```
The MAPPING continuously reads temperature and pressure from DM. To trigger an on-demand read, publish any message to `fins/commands/read`. To write a value, publish to `fins/commands/write`.
***
## Memory Area Details
* Used for I/O mapping and internal relays
* Words 0-99: Typically input/output modules
* Words 100-999: Work bits and internal relays
* Addresses are PLC model dependent
* Main user data storage
* Typically DM0-DM32767
* Best for process data, setpoints, configuration
* Retentive (battery-backed)
* Always retentive data
* Survives power cycles
* Use for counters, totals, settings
* Typically HR0-HR511
* General purpose work bits
* Non-retentive (cleared on power cycle)
* Fast access for temporary data
* TIM: Timer present values
* CNT: Counter present values
* Values are 16-bit unsigned
* Timer values often in 0.1s units
***
## Troubleshooting
* Verify IP address is correct
* Check firewall allows port 9600
* Ensure PLC Ethernet module is configured
* Try both TCP and UDP protocols
* Verify node addresses match PLC configuration
* LOCAL\_NODE should be unique on the network
* REMOTE\_NODE is the target PLC's node number
* Check PLC's FINS settings for correct node configuration
* For direct connections, try 0 for both nodes
* Verify address is within valid range for memory type
* Check MEMORY\_TYPE matches where data is stored
* For 32-bit types, ensure two consecutive words are available
* BIT\_ADDRESS must be 0-15
* Increase TIMEOUT value
* Check network connectivity
* Verify PLC is in RUN mode
* Reduce POLLING rate if reading many tags
* Try TCP instead of UDP
* Ensure WRITABLE is "true"
* Verify memory area is writable (not AR)
* Check PLC is not in monitor mode
* Verify value is within data type range
***
## Next Steps
Universal industrial protocol support.
Common patterns for all industrial routes.
# Modbus Serial Client Route
Source: https://docs.coreflux.org/latest/lot-language/routes/industrial/modbus-serial-client
Connect to PLCs and industrial devices using Modbus RTU over RS-232/RS-485 serial connections
## Modbus Serial Client
This page documents the **client** route (`MODBUS_SERIAL`) over serial RTU—the broker connects **to** a remote Modbus device on a COM port. For Modbus TCP client, see [Modbus TCP Client](./modbus-tcp-client).
The `MODBUS_SERIAL` route enables communication with industrial devices using Modbus RTU protocol over serial connections (RS-232 or RS-485). It supports the same TAG-based configuration as Modbus TCP but adds serial-specific parameters for baud rate, parity, and data bits.
Modbus Serial (RTU) is commonly used for legacy equipment, long-distance communication, and multi-drop RS-485 networks where multiple devices share a single bus.
## Basic Syntax
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
DEFINE ROUTE SerialDevice WITH TYPE MODBUS_SERIAL
ADD MODBUS_CONFIG
WITH CONNECTION_TYPE "SERIAL"
WITH COM_PORT "COM1"
WITH BAUD_RATE 9600
WITH PARITY "NONE"
WITH DATA_BITS 8
WITH STOP_BITS "ONE"
WITH SLAVE_ID 1
ADD MAPPING SensorReadings
WITH EVERY 1 SECOND
ADD TAG Temperature
WITH ADDRESS "100"
WITH ADDRESS_TYPE "HOLDING_REGISTER"
WITH DATA_TYPE "FLOAT"
WITH SOURCE_TOPIC "serial/temperature"
```
***
## Serial Configuration
### MODBUS\_CONFIG Parameters
Must be set to `SERIAL` for serial communication.
Serial port name. Windows: `COM1`, `COM2`, etc. Linux: `/dev/ttyUSB0`, `/dev/ttyS0`, etc.
Communication speed in bits per second. Common values: 9600, 19200, 38400, 57600, 115200.
Parity checking: `NONE`, `ODD`, `EVEN`, `MARK`, or `SPACE`. Default: NONE.
Number of data bits per character: 7 or 8. Default: 8.
Number of stop bits: `ONE`, `TWO`, or `ONEPOINTFIVE`. Default: ONE.
Modbus slave/unit ID (1-247). Each device on the bus must have a unique ID.
Read operation timeout in milliseconds. Default: 1000.
Write operation timeout in milliseconds. Default: 1000.
Number of retry attempts on failure. Default: 3.
Delay between retries in milliseconds. Default: 100.
***
## Common Serial Settings
Different devices use different serial settings. Here are common configurations:
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
ADD MODBUS_CONFIG
WITH CONNECTION_TYPE "SERIAL"
WITH COM_PORT "COM1"
WITH BAUD_RATE 9600
WITH PARITY "NONE"
WITH DATA_BITS 8
WITH STOP_BITS "ONE"
WITH SLAVE_ID 1
```
Settings: 9600 8N1
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
ADD MODBUS_CONFIG
WITH CONNECTION_TYPE "SERIAL"
WITH COM_PORT "COM1"
WITH BAUD_RATE 9600
WITH PARITY "EVEN"
WITH DATA_BITS 8
WITH STOP_BITS "ONE"
WITH SLAVE_ID 1
```
Settings: 9600 8E1
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
ADD MODBUS_CONFIG
WITH CONNECTION_TYPE "SERIAL"
WITH COM_PORT "/dev/ttyUSB0"
WITH BAUD_RATE 115200
WITH PARITY "NONE"
WITH DATA_BITS 8
WITH STOP_BITS "ONE"
WITH SLAVE_ID 1
```
Settings: 115200 8N1
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
ADD MODBUS_CONFIG
WITH CONNECTION_TYPE "SERIAL"
WITH COM_PORT "/dev/ttyUSB0"
WITH BAUD_RATE 9600
WITH PARITY "NONE"
WITH DATA_BITS 8
WITH STOP_BITS "ONE"
WITH SLAVE_ID 1
```
On Linux, USB-to-serial adapters typically appear as `/dev/ttyUSB0`, `/dev/ttyUSB1`, etc.
***
## RS-485 Multi-Drop Networks
RS-485 allows multiple devices on a single bus. Each device must have a unique slave ID:
```mermaid theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
graph LR
subgraph bus [RS-485 Bus]
Master[Coreflux]
Device1[Device 1 ID: 1]
Device2[Device 2 ID: 2]
Device3[Device 3 ID: 3]
end
Master --- Device1
Master --- Device2
Master --- Device3
```
### Multi-Device Configuration
Create separate routes or use different slave IDs:
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
DEFINE ROUTE TemperatureController WITH TYPE MODBUS_SERIAL
ADD MODBUS_CONFIG
WITH CONNECTION_TYPE "SERIAL"
WITH COM_PORT "COM1"
WITH BAUD_RATE 9600
WITH SLAVE_ID 1
ADD MAPPING TempController
WITH EVERY 1 SECOND
ADD TAG Temperature
WITH ADDRESS "0"
WITH ADDRESS_TYPE "HOLDING_REGISTER"
WITH DATA_TYPE "FLOAT"
WITH SOURCE_TOPIC "modbus/device1/temperature"
DEFINE ROUTE FlowMeter WITH TYPE MODBUS_SERIAL
ADD MODBUS_CONFIG
WITH CONNECTION_TYPE "SERIAL"
WITH COM_PORT "COM1"
WITH BAUD_RATE 9600
WITH SLAVE_ID 2
ADD MAPPING FlowMeter
WITH EVERY 1 SECOND
ADD TAG FlowRate
WITH ADDRESS "0"
WITH ADDRESS_TYPE "INPUT_REGISTER"
WITH DATA_TYPE "FLOAT"
WITH SOURCE_TOPIC "modbus/device2/flow"
```
When sharing a serial port between multiple slave IDs, ensure polling intervals are set to avoid bus collisions. The route handles sequencing automatically, but very fast polling across many devices may cause timeouts.
***
## TAG Configuration
The TAG configuration for Modbus Serial matches Modbus TCP, including string sizing. See the [Modbus TCP Client documentation](./modbus-tcp-client) for the full TAG parameter reference.
### String sizing
For `STRING` tags, `WITH LENGTH` sets the maximum string size in **bytes** (default **256**). `STRING_SIZE` is accepted as an alias. Each register holds two bytes, so the route reads enough **consecutive registers** from the tag's starting `ADDRESS` to cover the configured size.
```lot wrap focus={16} theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
DEFINE ROUTE ModbusStrings WITH TYPE MODBUS_SERIAL
ADD MODBUS_CONFIG
WITH CONNECTION_TYPE "SERIAL"
WITH COM_PORT "/dev/ttyUSB0"
WITH BAUD_RATE 9600
WITH PARITY "NONE"
WITH DATA_BITS 8
WITH STOP_BITS "ONE"
WITH SLAVE_ID 1
ADD MAPPING ProductData
WITH EVERY 1 SECOND
ADD TAG ProductName
WITH ADDRESS "0"
WITH ADDRESS_TYPE "HOLDING_REGISTER"
WITH DATA_TYPE "STRING"
WITH LENGTH 32
WITH SOURCE_TOPIC "factory/product/name"
```
For cross-protocol defaults and caps, see [String sizing by Route](./overview#string-sizing-by-route) on the industrial overview.
### TAG parameters
**STRING only.** Maximum string size in bytes. The route allocates consecutive registers from the starting `ADDRESS` (2 bytes per register). Alias: `STRING_SIZE`.
### Quick Reference
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
ADD TAG SensorValue
WITH ADDRESS "100"
WITH ADDRESS_TYPE "HOLDING_REGISTER"
WITH DATA_TYPE "FLOAT"
WITH SOURCE_TOPIC "serial/sensor"
WITH SCALING 0.1
WITH OFFSET 0
WITH UNIT "°C"
WITH DEADBAND 0.5
WITH PUBLISH_MODE "JSON"
WITH WRITABLE "true"
WITH DESTINATION_TOPIC "serial/sensor/set"
WITH BYTE_ORDER "BIGENDIAN"
WITH WORD_ORDER "BIGENDIAN"
```
***
## Event-Based Operations
For on-demand Modbus Serial operations (not polling), use the EVENT syntax. Publish a message to SOURCE\_TOPIC to trigger the operation; the route executes it and publishes the result to DESTINATION\_TOPIC.
Modbus Serial supports the same EVENT operations as Modbus TCP. See the [Modbus TCP Client Event-Based Operations](./modbus-tcp-client#event-based-operations) for the full supported operations table and query parameter details.
### Read Example
Read 10 holding registers on demand:
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
ADD EVENT ReadRegisters
WITH SOURCE_TOPIC "serial/commands/read"
WITH DESTINATION_TOPIC "serial/responses/read"
WITH QUERY "{operation: READ_HOLDING_REGISTERS, start_address: 0, count: 10}"
```
### Write Example
Write a single register on demand:
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
ADD EVENT WriteRegister
WITH SOURCE_TOPIC "serial/commands/write"
WITH DESTINATION_TOPIC "serial/responses/write"
WITH QUERY "{operation: WRITE_SINGLE_REGISTER, address: 100, value: 500}"
```
***
## Complete Examples
Basic connection to a single Modbus RTU device:
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
DEFINE ROUTE EnergyMeter WITH TYPE MODBUS_SERIAL
ADD MODBUS_CONFIG
WITH CONNECTION_TYPE "SERIAL"
WITH COM_PORT "COM3"
WITH BAUD_RATE 9600
WITH PARITY "EVEN"
WITH DATA_BITS 8
WITH STOP_BITS "ONE"
WITH SLAVE_ID 1
WITH READ_TIMEOUT 1000
WITH RETRY_COUNT 3
ADD MAPPING Readings
WITH EVERY 1 SECOND
ADD TAG Voltage
WITH ADDRESS "0"
WITH ADDRESS_TYPE "INPUT_REGISTER"
WITH DATA_TYPE "FLOAT"
WITH SOURCE_TOPIC "meter/voltage"
WITH UNIT "V"
ADD TAG Current
WITH ADDRESS "2"
WITH ADDRESS_TYPE "INPUT_REGISTER"
WITH DATA_TYPE "FLOAT"
WITH SOURCE_TOPIC "meter/current"
WITH UNIT "A"
ADD TAG Power
WITH ADDRESS "4"
WITH ADDRESS_TYPE "INPUT_REGISTER"
WITH DATA_TYPE "FLOAT"
WITH SOURCE_TOPIC "meter/power"
WITH UNIT "kW"
```
Read process value and write setpoint:
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
DEFINE ROUTE TempController WITH TYPE MODBUS_SERIAL
ADD MODBUS_CONFIG
WITH CONNECTION_TYPE "SERIAL"
WITH COM_PORT "/dev/ttyUSB0"
WITH BAUD_RATE 9600
WITH PARITY "NONE"
WITH DATA_BITS 8
WITH STOP_BITS "ONE"
WITH SLAVE_ID 1
ADD MAPPING Control
WITH EVERY 500 MILLISECONDS
ADD TAG ProcessValue
WITH ADDRESS "0"
WITH ADDRESS_TYPE "INPUT_REGISTER"
WITH DATA_TYPE "INT16"
WITH SOURCE_TOPIC "tempctrl/pv"
WITH SCALING 0.1
WITH UNIT "°C"
ADD TAG Setpoint
WITH ADDRESS "1"
WITH ADDRESS_TYPE "HOLDING_REGISTER"
WITH DATA_TYPE "INT16"
WITH SOURCE_TOPIC "tempctrl/sp"
WITH SCALING 0.1
WITH UNIT "°C"
WITH WRITABLE "true"
WITH DESTINATION_TOPIC "tempctrl/sp/set"
ADD TAG Output
WITH ADDRESS "2"
WITH ADDRESS_TYPE "INPUT_REGISTER"
WITH DATA_TYPE "INT16"
WITH SOURCE_TOPIC "tempctrl/output"
WITH SCALING 0.1
WITH UNIT "%"
```
Control a motor drive:
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
DEFINE ROUTE VFDControl WITH TYPE MODBUS_SERIAL
ADD MODBUS_CONFIG
WITH CONNECTION_TYPE "SERIAL"
WITH COM_PORT "COM2"
WITH BAUD_RATE 19200
WITH PARITY "EVEN"
WITH DATA_BITS 8
WITH STOP_BITS "ONE"
WITH SLAVE_ID 1
ADD MAPPING DriveStatus
WITH EVERY 200 MILLISECONDS
ADD TAG ActualSpeed
WITH ADDRESS "8451"
WITH ADDRESS_TYPE "HOLDING_REGISTER"
WITH DATA_TYPE "UINT16"
WITH SOURCE_TOPIC "vfd/speed/actual"
WITH SCALING 0.1
WITH UNIT "Hz"
ADD TAG MotorCurrent
WITH ADDRESS "8448"
WITH ADDRESS_TYPE "HOLDING_REGISTER"
WITH DATA_TYPE "UINT16"
WITH SOURCE_TOPIC "vfd/current"
WITH SCALING 0.01
WITH UNIT "A"
ADD TAG SpeedReference
WITH ADDRESS "8502"
WITH ADDRESS_TYPE "HOLDING_REGISTER"
WITH DATA_TYPE "UINT16"
WITH SOURCE_TOPIC "vfd/speed/reference"
WITH SCALING 0.1
WITH UNIT "Hz"
WITH WRITABLE "true"
WITH DESTINATION_TOPIC "vfd/speed/set"
ADD TAG ControlWord
WITH ADDRESS "8501"
WITH ADDRESS_TYPE "HOLDING_REGISTER"
WITH DATA_TYPE "UINT16"
WITH SOURCE_TOPIC "vfd/control"
WITH WRITABLE "true"
WITH DESTINATION_TOPIC "vfd/control/set"
```
Continuous monitoring with on-demand read and write events in the same route:
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
DEFINE ROUTE FullSerialSetup WITH TYPE MODBUS_SERIAL
ADD MODBUS_CONFIG
WITH CONNECTION_TYPE "SERIAL"
WITH COM_PORT "COM3"
WITH BAUD_RATE 9600
WITH PARITY "EVEN"
WITH DATA_BITS 8
WITH STOP_BITS "ONE"
WITH SLAVE_ID 1
ADD MAPPING ProcessReadings
WITH EVERY 1 SECOND
ADD TAG Voltage
WITH ADDRESS "0"
WITH ADDRESS_TYPE "INPUT_REGISTER"
WITH DATA_TYPE "FLOAT"
WITH SOURCE_TOPIC "meter/voltage"
WITH UNIT "V"
ADD TAG Current
WITH ADDRESS "2"
WITH ADDRESS_TYPE "INPUT_REGISTER"
WITH DATA_TYPE "FLOAT"
WITH SOURCE_TOPIC "meter/current"
WITH UNIT "A"
ADD EVENT ReadRegisters
WITH SOURCE_TOPIC "serial/commands/read"
WITH DESTINATION_TOPIC "serial/responses/read"
WITH QUERY "{operation: READ_HOLDING_REGISTERS, start_address: 0, count: 10}"
ADD EVENT WriteRegister
WITH SOURCE_TOPIC "serial/commands/write"
WITH DESTINATION_TOPIC "serial/responses/write"
WITH QUERY "{operation: WRITE_SINGLE_REGISTER, address: 100, value: 0}"
```
The MAPPING continuously reads voltage and current. To trigger an on-demand register read, publish any message to `serial/commands/read`. To write, publish to `serial/commands/write`.
***
## Troubleshooting
* Verify COM port is correct and not in use by another application
* Check physical wiring (TX/RX, A/B for RS-485)
* Ensure baud rate matches device settings
* Verify slave ID is correct
* Check device power and status indicators
* CRC errors indicate data corruption
* Check cable quality and length
* For RS-485, ensure proper termination resistors (120Ω at each end)
* Reduce baud rate for long cable runs
* Check for electrical interference
* Increase `READ_TIMEOUT` value
* Verify serial settings match device exactly
* Check for bus conflicts (multiple masters)
* For RS-485, ensure proper biasing
* Verify register addresses in device documentation
* Check `BYTE_ORDER` and `WORD_ORDER` settings
* Confirm `DATA_TYPE` matches register size
* Some devices use 1-based addressing (subtract 1 from documented address)
* Windows: Check Device Manager for correct COM port number
* Linux: Check `/dev/tty*` with `ls -l /dev/tty*`
* Ensure USB-to-serial driver is installed
* Verify user has permissions to access serial port
***
## RS-485 Wiring Tips
Proper wiring is critical for reliable RS-485 communication.
| Wire | Function | Description |
| ------ | -------- | ----------------------- |
| A (D-) | Data - | Negative data line |
| B (D+) | Data + | Positive data line |
| GND | Ground | Signal ground reference |
**Best Practices:**
* Use twisted pair cable for A and B lines
* Add 120Ω termination resistors at both ends of the bus
* Keep cable length under 1200m (4000ft)
* Use bias resistors if experiencing idle-line issues
* Ensure all grounds are connected
***
## Next Steps
Connect to Modbus devices over Ethernet.
Connect directly to Siemens PLCs.
# Modbus TCP Client Route
Source: https://docs.coreflux.org/latest/lot-language/routes/industrial/modbus-tcp-client
Connect to PLCs and industrial devices using Modbus TCP protocol with TAG-based polling and automatic batch optimization
## Modbus TCP Client
This page documents the **client** route (`MODBUS_TCP`)—the broker connects **to** a remote Modbus device and polls registers over MQTT. To expose MQTT topics **to** Modbus masters instead (SCADA, HMIs connect to the broker), see [Modbus TCP Server](./modbus-tcp-server).
The `MODBUS_TCP` route enables communication with PLCs, RTUs, and industrial devices using the Modbus TCP/IP protocol. It supports reading and writing registers and coils with configurable polling intervals and automatic batch optimization.
Modbus TCP is one of the most widely supported industrial protocols. If your device supports Modbus, this route provides a reliable way to integrate it with MQTT.
## Basic Syntax
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
DEFINE ROUTE ModbusDevice WITH TYPE MODBUS_TCP
ADD MODBUS_CONFIG
WITH HOST "192.168.1.100"
WITH PORT 502
WITH SLAVE_ID 1
ADD MAPPING SensorReadings
WITH EVERY 500 MILLISECONDS
ADD TAG Temperature
WITH ADDRESS "100"
WITH ADDRESS_TYPE "HOLDING_REGISTER"
WITH DATA_TYPE "FLOAT"
WITH SOURCE_TOPIC "modbus/temperature"
```
***
## Connection Configuration
### MODBUS\_CONFIG Parameters
Modbus TCP server hostname or IP address.
Modbus TCP port. Default: 502.
Modbus slave/unit ID (1-247). Default: 1.
Connection timeout in milliseconds. Default: 5000.
Read operation timeout in milliseconds. Default: 1000.
Write operation timeout in milliseconds. Default: 1000.
Number of retry attempts on failure. Default: 3.
Delay between retries in milliseconds. Default: 100.
Treat plain numeric tag addresses as one-based (address `1` = PDU register 0). Modicon-style addresses (`400001+`, `300001+`, `100001+`) are always decoded regardless of this flag. See [Modbus addressing](#modbus-addressing).
### Connection Example
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
ADD MODBUS_CONFIG
WITH HOST "192.168.1.100"
WITH PORT 502
WITH SLAVE_ID 1
WITH CONNECTION_TIMEOUT 5000
WITH READ_TIMEOUT 1000
WITH WRITE_TIMEOUT 1000
WITH RETRY_COUNT 3
WITH RETRY_DELAY 100
```
***
## Address Types
Modbus defines four address spaces:
| Address Type | Description | Function Codes | Typical Use |
| ------------------ | --------------------------- | ---------------- | --------------------------- |
| `HOLDING_REGISTER` | Read/Write 16-bit registers | FC03, FC06, FC16 | Setpoints, configuration |
| `INPUT_REGISTER` | Read-only 16-bit registers | FC04 | Sensor values, measurements |
| `COIL` | Read/Write single bits | FC01, FC05, FC15 | Digital outputs, controls |
| `DISCRETE_INPUT` | Read-only single bits | FC02 | Digital inputs, status |
Most Modbus devices use Holding Registers for configuration and Input Registers for measured values. Check your device documentation for the correct address type.
***
## Modbus Addressing
Modbus addressing varies by vendor. Coreflux supports three styles—choose the one that matches your device documentation.
### Style 1 — Zero-based PDU offsets (default)
The Modbus protocol uses zero-based addresses in the PDU. This is the default (`ONE_BASED_ADDRESSES = false`).
| Device manual label | Tag `ADDRESS` |
| ------------------- | ------------- |
| Register 1 | `"0"` |
| Register 100 | `"99"` |
Use this when documentation labels the first register as `0`, or when targeting integration tests that follow PDU numbering.
### Style 2 — One-based plain addresses
Most device manuals label the first holding register as **1**, not 0. Set `WITH ONE_BASED_ADDRESSES true` on `MODBUS_CONFIG` to use those labels directly in tag addresses.
| Device manual label | Tag `ADDRESS` |
| ------------------- | ------------- |
| Register 1 | `"1"` |
| Register 100 | `"100"` |
```lot wrap focus={5-11} theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
DEFINE ROUTE ModbusDevice WITH TYPE MODBUS_TCP
ADD MODBUS_CONFIG
WITH HOST "192.168.1.100"
WITH PORT 502
WITH SLAVE_ID 1
WITH ONE_BASED_ADDRESSES true
ADD MAPPING Sensors
WITH EVERY 1 SECOND
ADD TAG Temperature
WITH ADDRESS "1"
WITH ADDRESS_TYPE "HOLDING_REGISTER"
WITH DATA_TYPE "FLOAT"
WITH SOURCE_TOPIC "sensors/temperature"
```
### Style 3 — Modicon notation
Traditional Modicon prefix notation is always accepted. The prefix determines the address type automatically:
| Modicon range | Address type | Example |
| ------------- | ---------------- | ---------------- |
| 400001–465536 | Holding register | `400001` → PDU 0 |
| 300001–365536 | Input register | `300001` → PDU 0 |
| 100001–165536 | Discrete input | `100001` → PDU 0 |
```lot wrap focus={4-8} theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
DEFINE ROUTE ModbusDevice WITH TYPE MODBUS_TCP
ADD MODBUS_CONFIG
WITH HOST "192.168.1.100"
WITH SLAVE_ID 1
ADD MAPPING Setpoints
WITH EVERY 1 SECOND
ADD TAG Setpoint
WITH ADDRESS "400100"
WITH ADDRESS_TYPE "HOLDING_REGISTER"
WITH DATA_TYPE "INT16"
WITH SOURCE_TOPIC "plc/setpoint"
```
Modicon-style addresses are decoded the same way regardless of `ONE_BASED_ADDRESSES`. The flag only affects plain numeric addresses (those without a Modicon prefix).
### Conversion reference
| Device manual label | `ONE_BASED_ADDRESSES false` | `ONE_BASED_ADDRESSES true` | Modicon notation |
| ------------------- | --------------------------- | -------------------------- | ---------------- |
| Register 1 | `"0"` | `"1"` | `"400001"` |
| Register 100 | `"99"` | `"100"` | `"400100"` |
| Register 1000 | `"999"` | `"1000"` | `"401000"` |
Multi-word types (`FLOAT`, `INT32`, and so on) occupy consecutive registers from the resolved starting address.
***
## Data Types
The data type determines how raw register values are interpreted:
| Data Type | Size | Description |
| --------- | -------- | ---------------------------------------------------------------------------------- |
| `BOOL` | 1 bit | Boolean value (for coils/discrete inputs) |
| `BIT` | 1 bit | Alias for BOOL |
| `INT8` | 8 bits | Signed 8-bit integer |
| `UINT8` | 8 bits | Unsigned 8-bit integer |
| `INT16` | 16 bits | Signed 16-bit integer (1 register) |
| `UINT16` | 16 bits | Unsigned 16-bit integer (1 register) |
| `INT32` | 32 bits | Signed 32-bit integer (2 registers) |
| `UINT32` | 32 bits | Unsigned 32-bit integer (2 registers) |
| `INT64` | 64 bits | Signed 64-bit integer (4 registers) |
| `UINT64` | 64 bits | Unsigned 64-bit integer (4 registers) |
| `FLOAT` | 32 bits | 32-bit floating point (2 registers) |
| `DOUBLE` | 64 bits | 64-bit floating point (4 registers) |
| `STRING` | Variable | ASCII string across consecutive registers; sized with `LENGTH` (default 256 bytes) |
### String sizing
For `STRING` tags, `WITH LENGTH` sets the maximum string size in **bytes** (default **256**). `STRING_SIZE` is accepted as an alias for the same field. Each Modbus register holds two bytes, so the route reads enough **consecutive registers** starting at the tag's `ADDRESS` to cover the configured size.
```lot wrap focus={12} theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
DEFINE ROUTE ModbusStrings WITH TYPE MODBUS_TCP
ADD MODBUS_CONFIG
WITH HOST "192.168.1.100"
WITH PORT 502
WITH SLAVE_ID 1
ADD MAPPING ProductData
WITH EVERY 1 SECOND
ADD TAG ProductName
WITH ADDRESS "0"
WITH ADDRESS_TYPE "HOLDING_REGISTER"
WITH DATA_TYPE "STRING"
WITH LENGTH 32
WITH SOURCE_TOPIC "factory/product/name"
```
For cross-protocol defaults and caps, see [String sizing by Route](./overview#string-sizing-by-route) on the industrial overview.
***
## TAG Configuration
### Complete TAG Parameters
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
ADD TAG TemperatureSensor
WITH ADDRESS "100"
WITH ADDRESS_TYPE "HOLDING_REGISTER"
WITH DATA_TYPE "FLOAT"
WITH SOURCE_TOPIC "modbus/sensors/temperature"
WITH SCALING 0.1
WITH OFFSET 0
WITH UNIT "°C"
WITH DECIMAL_PLACES 2
WITH MIN_VALUE -40
WITH MAX_VALUE 150
WITH DEADBAND 0.5
WITH PUBLISH_MODE "JSON"
WITH WRITABLE "true"
WITH DESTINATION_TOPIC "modbus/sensors/temperature/set"
WITH BYTE_ORDER "BIGENDIAN"
WITH WORD_ORDER "BIGENDIAN"
WITH DESCRIPTION "Main process temperature sensor"
```
### TAG Parameters Reference
Register or coil address. Plain numeric (0–65535 zero-based by default, or 1–65536 when `ONE_BASED_ADDRESSES true`), or Modicon notation (`400001+`, `300001+`, `100001+`). See [Modbus addressing](#modbus-addressing).
Type of Modbus address: `HOLDING_REGISTER`, `INPUT_REGISTER`, `COIL`, or `DISCRETE_INPUT`.
**STRING only.** Maximum string size in bytes. The route allocates consecutive registers from the starting `ADDRESS` (2 bytes per register). Alias: `STRING_SIZE`.
Multiplier applied to raw value. Default: 1.0. Example: Raw value 245 with SCALING 0.1 becomes 24.5.
Value added after scaling. Default: 0.0. Formula: `result = (raw * scaling) + offset`.
Number of decimal places in published value. Default: 2.
Minimum allowed value. Values below this threshold are not published.
Maximum allowed value. Values above this threshold are not published.
Minimum change required to publish a new value. Default: 0.0.
Topic where PLC values are published. Subscribe here to receive sensor data.
Output format: `VALUE_ONLY` or `JSON`. Default: VALUE\_ONLY.
Engineering unit for documentation (e.g., °C, bar, RPM).
Human-readable description of the TAG.
Allow writing to this register/coil. Default: false.
Topic to send write commands. Publish a value here to write it to the PLC.
Byte order for multi-byte values: `BIGENDIAN` or `LITTLEENDIAN`. Default: BIGENDIAN.
Word order for 32/64-bit values: `BIGENDIAN` or `LITTLEENDIAN`. Default: BIGENDIAN.
Different manufacturers use different byte/word ordering. If values appear incorrect, try changing BYTE\_ORDER or WORD\_ORDER.
***
## Event-Based Operations
For on-demand Modbus operations (not polling), use the EVENT syntax. Publish a message to SOURCE\_TOPIC to trigger the operation; the route executes it and publishes the result to DESTINATION\_TOPIC.
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
ADD EVENT ReadOnDemand
WITH SOURCE_TOPIC "modbus/commands/read"
WITH DESTINATION_TOPIC "modbus/responses/read"
WITH QUERY "{operation: READ_HOLDING_REGISTERS, start_address: 0, count: 10}"
```
### Query Syntax
The `WITH QUERY` value uses LoT object syntax: unquoted keys, unquoted identifiers, and numeric values. The entire query is wrapped in double quotes.
| Element | Format | Example |
| ---------------- | -------------------- | -------------------------------------- |
| Keys | Unquoted | `operation`, `start_address`, `count` |
| Operation values | Unquoted identifiers | `READ_HOLDING_REGISTERS`, `READ_COILS` |
| Numeric values | Unquoted | `0`, `10`, `100` |
| String values | Unquoted (addresses) | `address: 100` |
### Supported Operations
| Operation | Description |
| -------------------------- | --------------------------------------- |
| `READ_HOLDING_REGISTERS` | Read holding registers (FC03) |
| `READ_INPUT_REGISTERS` | Read input registers (FC04) |
| `READ_COILS` | Read coils (FC01) |
| `READ_DISCRETE_INPUTS` | Read discrete inputs (FC02) |
| `WRITE_SINGLE_REGISTER` | Write single holding register (FC06) |
| `WRITE_MULTIPLE_REGISTERS` | Write multiple holding registers (FC16) |
| `WRITE_SINGLE_COIL` | Write single coil (FC05) |
| `WRITE_MULTIPLE_COILS` | Write multiple coils (FC15) |
### Query Parameters by Operation
| Operation | Parameters | Example |
| -------------------------- | ------------------------- | ---------------------------------------------------------------------------------- |
| `READ_HOLDING_REGISTERS` | `start_address`, `count` | `{operation: READ_HOLDING_REGISTERS, start_address: 0, count: 10}` |
| `READ_INPUT_REGISTERS` | `start_address`, `count` | `{operation: READ_INPUT_REGISTERS, start_address: 0, count: 5}` |
| `READ_COILS` | `start_address`, `count` | `{operation: READ_COILS, start_address: 0, count: 8}` |
| `READ_DISCRETE_INPUTS` | `start_address`, `count` | `{operation: READ_DISCRETE_INPUTS, start_address: 0, count: 8}` |
| `WRITE_SINGLE_REGISTER` | `address`, `value` | `{operation: WRITE_SINGLE_REGISTER, address: 100, value: 42}` |
| `WRITE_MULTIPLE_REGISTERS` | `start_address`, `values` | `{operation: WRITE_MULTIPLE_REGISTERS, start_address: 0, values: [100, 200]}` |
| `WRITE_SINGLE_COIL` | `address`, `value` | `{operation: WRITE_SINGLE_COIL, address: 0, value: true}` |
| `WRITE_MULTIPLE_COILS` | `start_address`, `values` | `{operation: WRITE_MULTIPLE_COILS, start_address: 0, values: [true, false, true]}` |
To write a value on demand, publish a message to SOURCE\_TOPIC. The route executes the write and publishes the result to DESTINATION\_TOPIC:
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
ADD EVENT WriteSetpoint
WITH SOURCE_TOPIC "modbus/commands/write"
WITH DESTINATION_TOPIC "modbus/responses/write"
WITH QUERY "{operation: WRITE_SINGLE_REGISTER, address: 100, value: 42}"
```
***
## Complete Examples
Read temperature and pressure from a Modbus device:
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
DEFINE ROUTE ProcessSensors WITH TYPE MODBUS_TCP
ADD MODBUS_CONFIG
WITH HOST "192.168.1.100"
WITH PORT 502
WITH SLAVE_ID 1
ADD MAPPING Sensors
WITH EVERY 1 SECOND
ADD TAG Temperature
WITH ADDRESS "100"
WITH ADDRESS_TYPE "HOLDING_REGISTER"
WITH DATA_TYPE "FLOAT"
WITH SOURCE_TOPIC "process/temperature"
WITH SCALING 0.1
WITH UNIT "°C"
ADD TAG Pressure
WITH ADDRESS "102"
WITH ADDRESS_TYPE "HOLDING_REGISTER"
WITH DATA_TYPE "FLOAT"
WITH SOURCE_TOPIC "process/pressure"
WITH UNIT "bar"
```
Group TAGs by update frequency:
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
DEFINE ROUTE PLCConnection WITH TYPE MODBUS_TCP
ADD MODBUS_CONFIG
WITH HOST "192.168.1.50"
WITH PORT 502
WITH SLAVE_ID 1
ADD MAPPING FastLoop
WITH EVERY 100 MILLISECONDS
ADD TAG MotorSpeed
WITH ADDRESS "200"
WITH ADDRESS_TYPE "HOLDING_REGISTER"
WITH DATA_TYPE "UINT16"
WITH SOURCE_TOPIC "plc/motor/speed"
ADD TAG MotorCurrent
WITH ADDRESS "201"
WITH ADDRESS_TYPE "INPUT_REGISTER"
WITH DATA_TYPE "FLOAT"
WITH SOURCE_TOPIC "plc/motor/current"
ADD MAPPING SlowLoop
WITH EVERY 5 SECONDS
ADD TAG TotalRuntime
WITH ADDRESS "300"
WITH ADDRESS_TYPE "HOLDING_REGISTER"
WITH DATA_TYPE "UINT32"
WITH SOURCE_TOPIC "plc/stats/runtime"
ADD TAG FaultCode
WITH ADDRESS "400"
WITH ADDRESS_TYPE "HOLDING_REGISTER"
WITH DATA_TYPE "UINT16"
WITH SOURCE_TOPIC "plc/status/fault"
```
Read values and write setpoints:
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
DEFINE ROUTE ControlSystem WITH TYPE MODBUS_TCP
ADD MODBUS_CONFIG
WITH HOST "192.168.1.100"
WITH PORT 502
WITH SLAVE_ID 1
ADD MAPPING ControlLoop
WITH EVERY 500 MILLISECONDS
ADD TAG ProcessValue
WITH ADDRESS "100"
WITH ADDRESS_TYPE "INPUT_REGISTER"
WITH DATA_TYPE "FLOAT"
WITH SOURCE_TOPIC "control/pv"
WITH PUBLISH_MODE "JSON"
ADD TAG Setpoint
WITH ADDRESS "200"
WITH ADDRESS_TYPE "HOLDING_REGISTER"
WITH DATA_TYPE "FLOAT"
WITH SOURCE_TOPIC "control/sp"
WITH WRITABLE "true"
WITH DESTINATION_TOPIC "control/sp/set"
WITH MIN_VALUE 0
WITH MAX_VALUE 100
ADD TAG OutputValue
WITH ADDRESS "300"
WITH ADDRESS_TYPE "HOLDING_REGISTER"
WITH DATA_TYPE "FLOAT"
WITH SOURCE_TOPIC "control/output"
```
Read digital inputs and control outputs:
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
DEFINE ROUTE DigitalIO WITH TYPE MODBUS_TCP
ADD MODBUS_CONFIG
WITH HOST "192.168.1.75"
WITH PORT 502
WITH SLAVE_ID 1
ADD MAPPING DigitalInputs
WITH EVERY 100 MILLISECONDS
ADD TAG StartButton
WITH ADDRESS "0"
WITH ADDRESS_TYPE "DISCRETE_INPUT"
WITH DATA_TYPE "BOOL"
WITH SOURCE_TOPIC "io/inputs/start"
ADD TAG StopButton
WITH ADDRESS "1"
WITH ADDRESS_TYPE "DISCRETE_INPUT"
WITH DATA_TYPE "BOOL"
WITH SOURCE_TOPIC "io/inputs/stop"
ADD TAG RunningLight
WITH ADDRESS "0"
WITH ADDRESS_TYPE "COIL"
WITH DATA_TYPE "BOOL"
WITH SOURCE_TOPIC "io/outputs/running"
WITH WRITABLE "true"
WITH DESTINATION_TOPIC "io/outputs/running/set"
```
Continuous monitoring with on-demand read and write events in the same route:
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
DEFINE ROUTE FullModbusSetup WITH TYPE MODBUS_TCP
ADD MODBUS_CONFIG
WITH HOST "192.168.1.100"
WITH PORT 502
WITH SLAVE_ID 1
ADD MAPPING ProcessSensors
WITH EVERY 500 MILLISECONDS
ADD TAG Temperature
WITH ADDRESS "100"
WITH ADDRESS_TYPE "HOLDING_REGISTER"
WITH DATA_TYPE "FLOAT"
WITH SOURCE_TOPIC "process/temperature"
WITH UNIT "°C"
ADD TAG Pressure
WITH ADDRESS "102"
WITH ADDRESS_TYPE "HOLDING_REGISTER"
WITH DATA_TYPE "FLOAT"
WITH SOURCE_TOPIC "process/pressure"
WITH UNIT "bar"
ADD EVENT ReadRegisters
WITH SOURCE_TOPIC "modbus/commands/read"
WITH DESTINATION_TOPIC "modbus/responses/read"
WITH QUERY "{operation: READ_HOLDING_REGISTERS, start_address: 0, count: 10}"
ADD EVENT WriteSetpoint
WITH SOURCE_TOPIC "modbus/commands/write"
WITH DESTINATION_TOPIC "modbus/responses/write"
WITH QUERY "{operation: WRITE_SINGLE_REGISTER, address: 200, value: 0}"
```
To trigger the on-demand read, publish any message to `modbus/commands/read`. The route reads registers 0-9 and publishes the result to `modbus/responses/read`. To write, publish to `modbus/commands/write`.
***
## Troubleshooting
* Verify IP address and port are correct
* Check firewall settings (port 502 must be open)
* Ensure device is powered on and connected to network
* Try increasing `CONNECTION_TIMEOUT`
* Verify `ADDRESS` matches device documentation—try `WITH ONE_BASED_ADDRESSES true` if the manual labels registers starting at 1
* Use Modicon notation (`400100`) when the datasheet shows prefixed addresses
* Check `BYTE_ORDER` and `WORD_ORDER` settings
* Confirm `DATA_TYPE` matches register size
* Verify `SCALING` and `OFFSET` calculations
* Ensure `WRITABLE` is set to "true"
* Verify `ADDRESS_TYPE` is `HOLDING_REGISTER` or `COIL`
* Check device permissions for write operations
* Verify value is within `MIN_VALUE` and `MAX_VALUE`
* Confirm correct `SLAVE_ID` (usually 1 for TCP devices)
* Some gateways use different slave IDs for different devices
* Check device documentation for correct unit ID
***
## Next Steps
Connect to RS-232/RS-485 Modbus RTU devices.
Expose MQTT topics as Modbus registers for SCADA and HMIs.
# Modbus TCP Server Route
Source: https://docs.coreflux.org/latest/lot-language/routes/industrial/modbus-tcp-server
Expose MQTT topics as Modbus TCP registers and coils so SCADA, HMIs, and Modbus masters connect to your Coreflux broker
## Modbus TCP Server
This page documents the **server** route (`MODBUS_SERVER`)—the broker **hosts** a Modbus TCP server and exposes MQTT topics as register and coil addresses. External masters connect to the broker over TCP. To connect the broker **to** a remote Modbus device instead, see [Modbus TCP Client](./modbus-tcp-client).
The `MODBUS_SERVER` route makes your broker act as a **Modbus TCP server (slave)**. External masters—SCADA, HMIs, historians, PLCs—connect **to the broker** and read or write a declared register/coil address space that bridges bidirectionally to MQTT topics.
Use `MODBUS_TCP` when you need to **poll a device**. Use `MODBUS_SERVER` when OT systems need to **read and write** values that live on your MQTT bus—no separate gateway required.
**Like a shop counter with a price list.** MQTT updates what's on the shelf (register values), and customers (Modbus masters) read the tags—or, for writable addresses, change items that publish back to MQTT.
## When to Use This Route
* **SCADA / HMI integration** — Let plant-floor tools poll broker data over standard Modbus without custom drivers.
* **OT/IT bridge** — Expose IoT sensor streams to masters that only speak Modbus.
* **Bidirectional control** — Writable tags let masters adjust setpoints; the broker publishes the new value to MQTT on a cadence you choose.
* **Client-only field devices** — Integrate master-only equipment into MQTT with no middleware between the broker and the bus.
***
The sections below cover data flow, configuration, persistence, and production patterns.
## Quick Start
Map an MQTT topic to a holding register masters can poll:
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
DEFINE ROUTE TempServer WITH TYPE MODBUS_SERVER
ADD MAPPING Sensors
ADD TAG Temperature
WITH ADDRESS "100"
WITH ADDRESS_TYPE "HOLDING_REGISTER"
WITH DATA_TYPE "FLOAT"
WITH SOURCE_TOPIC "plant/line1/temperature"
```
Publish `23.5` to `plant/line1/temperature` → registers 100–101 hold the FLOAT; a master reading them gets `23.5`.
Allow a master to write a register; the broker publishes the value to MQTT within one `WITH EVERY` interval:
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
DEFINE ROUTE SetpointServer WITH TYPE MODBUS_SERVER
ADD MAPPING Control
WITH EVERY 500 MILLISECONDS
WITH SOURCE_TOPIC "plant/line1"
ADD TAG Setpoint
WITH ADDRESS "200"
WITH ADDRESS_TYPE "HOLDING_REGISTER"
WITH DATA_TYPE "FLOAT"
WITH WRITABLE "true"
WITH DESTINATION_TOPIC "plant/line1/setpoint/written"
```
MQTT publishes to `plant/line1/Setpoint` update registers 200–201. A master write surfaces on `plant/line1/setpoint/written` within 500 ms.
Listen on port 502 as unit 1 with multiple tags and connection settings:
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
DEFINE ROUTE PlantModbusServer WITH TYPE MODBUS_SERVER
ADD MODBUS_SERVER_CONFIG
WITH HOST "0.0.0.0"
WITH PORT 502
WITH UNIT_ID 1
WITH MAX_CONNECTIONS 64
ADD MAPPING ServerTags
WITH EVERY 500 MILLISECONDS
WITH SOURCE_TOPIC "plant/modbus"
ADD TAG Temperature
WITH ADDRESS "100"
WITH ADDRESS_TYPE "HOLDING_REGISTER"
WITH DATA_TYPE "FLOAT"
ADD TAG Setpoint
WITH ADDRESS "200"
WITH ADDRESS_TYPE "HOLDING_REGISTER"
WITH DATA_TYPE "FLOAT"
WITH WRITABLE "true"
WITH DESTINATION_TOPIC "plant/modbus/setpoint/set"
ADD TAG Running
WITH ADDRESS "0"
WITH ADDRESS_TYPE "COIL"
WITH DATA_TYPE "BOOL"
WITH WRITABLE "true"
WITH DESTINATION_TOPIC "plant/modbus/running/set"
```
Masters read `Temperature`; `Setpoint` and `Running` are bidirectional—MQTT drives them and master writes publish to their `DESTINATION_TOPIC` within 500 ms.
Persist master-written setpoints across broker restarts:
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
DEFINE ROUTE ResilientServer WITH TYPE MODBUS_SERVER
ADD MODBUS_SERVER_CONFIG
WITH PORT 502
WITH UNIT_ID 1
WITH PERSISTENCE true
ADD MAPPING Control
WITH EVERY 500 MILLISECONDS
WITH SOURCE_TOPIC "plant/line1"
ADD TAG Setpoint
WITH ADDRESS "200"
WITH ADDRESS_TYPE "HOLDING_REGISTER"
WITH DATA_TYPE "FLOAT"
WITH WRITABLE "true"
WITH DESTINATION_TOPIC "plant/line1/setpoint/written"
ADD TAG Temperature
WITH ADDRESS "100"
WITH ADDRESS_TYPE "INPUT_REGISTER"
WITH DATA_TYPE "FLOAT"
```
After a restart, a master reading `Setpoint` (200–201) gets the last value it wrote. `Temperature` (100–101) refreshes from the live MQTT source, not the snapshot.
***
## How Data Flows
```mermaid theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
graph LR
subgraph mqtt [MQTT]
Topics[Topics]
end
subgraph broker [Coreflux Broker]
Route[MODBUS_SERVER Route]
Registers[Register / Coil Space]
end
subgraph ot [OT Masters]
Master[SCADA / HMI / PLC]
end
Topics -->|publish sets value| Route
Route --> Registers
Registers -->|read / write| Master
Master -->|write writable tag| Route
Route -->|publish on EVERY cadence| Topics
```
Sync is **hybrid**—`WITH EVERY` controls outbound latency:
| Direction | Behavior |
| ------------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **MQTT → register (inbound)** | A publish on a tag's `SOURCE_TOPIC` updates the backing register or coil immediately. Works for every address type, including read-only `INPUT_REGISTER` and `DISCRETE_INPUT`. Retained MQTT values are seeded at startup. |
| **register → MQTT (outbound)** | When a master writes a `WRITABLE` `HOLDING_REGISTER` or `COIL`, the broker detects the change on the mapping's `WITH EVERY` cadence and publishes the decoded value to the tag's `DESTINATION_TOPIC` (rounded per `DECIMAL_PLACES`, retained). A master write surfaces on MQTT within one cadence interval. |
A master write never loops back into the register from MQTT, and an MQTT-driven change is never re-published to `DESTINATION_TOPIC` (direction guard).
On server routes, route status **Connected** means the endpoint is **listening**—not that a remote session is established.
***
## Server Configuration
All transport settings live in the `MODBUS_SERVER_CONFIG` block. A valid route needs only `WITH TYPE MODBUS_SERVER` plus a mapping with one tag—it listens on `0.0.0.0:502` as unit 1 by default.
```lot wrap focus={2-6} theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
DEFINE ROUTE PlantModbusServer WITH TYPE MODBUS_SERVER
ADD MODBUS_SERVER_CONFIG
WITH HOST "0.0.0.0"
WITH PORT 502
WITH UNIT_ID 1
WITH MAX_CONNECTIONS 64
```
### Connection parameters
Bind interface to listen on (`0.0.0.0` = all interfaces). `IP` is accepted as an alias.
TCP listen port. Each server route claims its port exclusively—a second route on the same port is refused.
Best-effort soft cap on concurrent master connections. See [Connection-limit behavior](#connection-limit-behavior).
Modbus unit ID the broker answers as (1–247). `SLAVE_ID` is accepted as an alias.
Persist served values across broker restarts. See [Data persistence & recovery](#data-persistence--recovery).
Treat plain numeric tag addresses as one-based (address `1` = PDU register 0). Modicon-style addresses (`400001+`, `300001+`, `100001+`) are always decoded regardless of this flag. See [Address notation](#address-notation).
Modbus has no native authentication—there is no credential configuration. Bind to a specific interface and restrict access at the network layer; MQTT-side access is governed by HUB topic permissions. Use `GET ENV` / `GET SECRET` for environment-derived values (for example `WITH PORT GET ENV "MODBUS_PORT"`).
***
## Tag and Mapping Configuration
Each exposed register or coil is declared as an `ADD TAG`. Only declared addresses are served—reads or writes to undeclared addresses return a Modbus **Illegal Data Address** exception.
```lot wrap focus={7-20} theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
DEFINE ROUTE PlantModbusServer WITH TYPE MODBUS_SERVER
ADD MODBUS_SERVER_CONFIG
WITH HOST "0.0.0.0"
WITH PORT 502
WITH UNIT_ID 1
WITH MAX_CONNECTIONS 64
ADD MAPPING ServerTags
WITH EVERY 500 MILLISECONDS
WITH SOURCE_TOPIC "modbus/server"
ADD TAG Setpoint
WITH ADDRESS "200"
WITH ADDRESS_TYPE "HOLDING_REGISTER"
WITH DATA_TYPE "FLOAT"
WITH SOURCE_TOPIC "modbus/server/setpoint"
WITH WRITABLE "true"
WITH DESTINATION_TOPIC "modbus/server/setpoint/set"
WITH BYTE_ORDER "BIGENDIAN"
WITH WORD_ORDER "BIGENDIAN"
WITH DECIMAL_PLACES 2
WITH DESCRIPTION "Operator setpoint"
ADD TAG ProductCode
WITH ADDRESS "300"
WITH ADDRESS_TYPE "HOLDING_REGISTER"
WITH DATA_TYPE "STRING"
WITH LENGTH 40
WITH WRITABLE "true"
WITH DESTINATION_TOPIC "modbus/server/product/code"
```
### Server vs. client semantics
| Setting | Client (`MODBUS_TCP`) | Server (`MODBUS_SERVER`) |
| ------------------- | ---------------------------- | ------------------------------------------- |
| Role | Master—dials out to a device | Slave—listens for masters |
| `HOST` / `IP` | Device IP to dial | **Bind** interface to listen on |
| `SOURCE_TOPIC` | Register value → MQTT (read) | MQTT → register (write into address space) |
| `DESTINATION_TOPIC` | MQTT → device write | Register → MQTT when a master writes |
| `EVERY` | Poll interval | Outbound sync cadence (master write → MQTT) |
| Transport | TCP / Serial (RTU) | TCP |
### Modbus address banks
| `ADDRESS_TYPE` | Bank | Element | Master access | Function codes |
| ------------------ | ----------------- | ----------- | ------------- | -------------- |
| `HOLDING_REGISTER` | Holding registers | 16-bit word | read / write | 3, 6, 16 |
| `INPUT_REGISTER` | Input registers | 16-bit word | read-only | 4 |
| `COIL` | Coils | 1 bit | read / write | 1, 5, 15 |
| `DISCRETE_INPUT` | Discrete inputs | 1 bit | read-only | 2 |
### Address notation
Tag `ADDRESS` values are converted to the **PDU offset** the broker exposes on the wire. External Modbus masters always use zero-based PDU addresses in their read/write requests—the `ONE_BASED_ADDRESSES` flag only affects how you label tags in LoT, not how masters address the server.
Three styles are supported (same rules as the [Modbus TCP Client](./modbus-tcp-client#modbus-addressing) route):
| Style | Config | Tag address for PDU 0 | Tag address for PDU 99 |
| -------------------------- | --------------------------- | --------------------- | ---------------------- |
| Zero-based plain (default) | `ONE_BASED_ADDRESSES false` | `"0"` | `"99"` |
| One-based plain | `ONE_BASED_ADDRESSES true` | `"1"` | `"100"` |
| Modicon holding register | either | `"400001"` | `"400100"` |
```lot wrap focus={2-12} theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
DEFINE ROUTE PlantModbusServer WITH TYPE MODBUS_SERVER
ADD MODBUS_SERVER_CONFIG
WITH PORT 502
WITH UNIT_ID 1
WITH ONE_BASED_ADDRESSES true
ADD MAPPING ServerTags
WITH SOURCE_TOPIC "plant/modbus"
ADD TAG Temperature
WITH ADDRESS "1"
WITH ADDRESS_TYPE "HOLDING_REGISTER"
WITH DATA_TYPE "FLOAT"
```
Modicon-style addresses are decoded the same way regardless of `ONE_BASED_ADDRESSES`. The flag only affects plain numeric addresses.
### Tag parameters
Register or coil address. Plain numeric (0–65535 zero-based by default, or 1–65536 when `ONE_BASED_ADDRESSES true`), or Modicon notation (`400001+`, `300001+`, `100001+`). Multi-word types occupy consecutive PDU addresses from the resolved offset. See [Address notation](#address-notation).
Which bank the tag lives in: `HOLDING_REGISTER`, `INPUT_REGISTER`, `COIL`, or `DISCRETE_INPUT`.
Wire type: `BOOL`, `INT16`, `UINT16`, `INT32`, `UINT32`, `INT64`, `UINT64`, `FLOAT`, `DOUBLE`, or `STRING`. `INT32`/`UINT32`/`FLOAT` span 2 registers; `INT64`/`UINT64`/`DOUBLE` span 4.
**STRING only.** Maximum string size in **bytes** (2 bytes per register). Register count is `ceil(bytes / 2)` (integer form: `(bytes + 1) / 2`). When omitted, defaults to **40 bytes** (20 registers) and logs a Warning. Alias: `STRING_SIZE`. Must resolve to at most **123 registers** (**246 bytes**), the FC16 write cap; larger values are rejected during route validation.
MQTT topic whose publishes set this register or coil. Defaults to `{mapping SOURCE_TOPIC}/{TagName}`.
Allow masters to write the address. Only `HOLDING_REGISTER` and `COIL` are master-writable.
Topic published to when a master writes this (writable) address.
Byte order within each register for multi-byte types: `BIGENDIAN` or `LITTLEENDIAN`.
Word order across registers for 32-/64-bit types (high word first by default): `BIGENDIAN` or `LITTLEENDIAN`.
Rounding applied to `FLOAT`/`DOUBLE` values published to `DESTINATION_TOPIC`.
Per-tag override of the route persistence default. Precedence: tag → mapping → route. See [Data persistence & recovery](#data-persistence--recovery).
Free-text documentation for the tag.
On the mapping block: register → MQTT sync cadence for master writes.
### All four address types
One server exposing every bank type:
```lot wrap focus={4-22} theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
DEFINE ROUTE MixedServer WITH TYPE MODBUS_SERVER
ADD MAPPING Points
WITH SOURCE_TOPIC "plant/mixed"
ADD TAG Pressure
WITH ADDRESS "100"
WITH ADDRESS_TYPE "HOLDING_REGISTER"
WITH DATA_TYPE "FLOAT"
ADD TAG MotorSpeed
WITH ADDRESS "50"
WITH ADDRESS_TYPE "INPUT_REGISTER"
WITH DATA_TYPE "UINT16"
ADD TAG PumpEnable
WITH ADDRESS "0"
WITH ADDRESS_TYPE "COIL"
WITH DATA_TYPE "BOOL"
WITH WRITABLE "true"
WITH DESTINATION_TOPIC "plant/mixed/pump/cmd"
ADD TAG DoorClosed
WITH ADDRESS "0"
WITH ADDRESS_TYPE "DISCRETE_INPUT"
WITH DATA_TYPE "BOOL"
```
All four points are fed from `plant/mixed/`; only `PumpEnable` accepts master writes (→ `plant/mixed/pump/cmd`).
### STRING tag example
Reserve a writable 40-byte holding-register range for barcode values a master can write with FC16:
```lot wrap focus={7} theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
DEFINE ROUTE ScannerServer WITH TYPE MODBUS_SERVER
ADD MAPPING ScannerTags
ADD TAG Barcode
WITH ADDRESS "0"
WITH ADDRESS_TYPE "HOLDING_REGISTER"
WITH DATA_TYPE "STRING"
WITH LENGTH 40
WITH WRITABLE "true"
WITH DESTINATION_TOPIC "scanner/barcode"
```
`WITH LENGTH 40` reserves holding registers 0–19 (`ceil(40 / 2) = 20` registers). A master FC16 write to those 20 registers is decoded and published to `scanner/barcode`. `STRING_SIZE` is accepted as an alias for `LENGTH`.
***
## Protocol Conformance
This server implements Modbus Application Protocol **Conformance Class 0 and Class 1** over Modbus/TCP—the interoperability bar for standard masters:
| Class | Function codes | Supported |
| ------- | ------------------------------------------------------------------------------------------------------------------------------ | --------- |
| Class 0 | FC 03 Read Holding Registers, FC 16 Write Multiple Registers | ✅ |
| Class 1 | FC 01 Read Coils, FC 02 Read Discrete Inputs, FC 04 Read Input Registers, FC 05 Write Single Coil, FC 06 Write Single Register | ✅ |
| Class 2 | FC 15 Write Multiple Coils | ✅ |
Writes are accepted only on `WRITABLE` `HOLDING_REGISTER`/`COIL` addresses. Undeclared addresses and writes to read-only banks return exception **02 (Illegal Data Address)**.
Modbus/TCP is **plaintext**—the protocol has no native authentication or encryption. Secure deployments by binding to a specific interface and restricting access at the network layer (firewall, VLAN, VPN). Modbus/TCP Security (TLS on port 802) is not yet implemented.
***
## Data Persistence & Recovery
By default a server route holds register and coil values **in memory only**—a broker restart resets every address to zero. Set `WITH PERSISTENCE true` on `MODBUS_SERVER_CONFIG` to snapshot served values and restore them on startup.
### Ownership-aware policy
What is safe to persist depends on who owns the value:
| Bank | Owner | Default | On restart |
| ----------------------------------------------------------- | ----------------------------------------------- | ----------------- | -------------------------------------------------------------- |
| `HOLDING_REGISTER` / `COIL`, `WRITABLE true` | Master-written setpoint lives only in the slave | **Persisted** | Restored **authoritatively**—wins over retained MQTT |
| `INPUT_REGISTER` / `DISCRETE_INPUT`, and non-writable banks | Fed from live MQTT | **Not persisted** | Recovered from **live MQTT** (retained message / next publish) |
Master-written setpoints survive a restart; live telemetry always reflects the current MQTT source.
### Per-tag overrides
`WITH PERSISTENCE true|false` on an individual `ADD TAG` overrides the route default:
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
DEFINE ROUTE OverrideServer WITH TYPE MODBUS_SERVER
ADD MODBUS_SERVER_CONFIG
WITH PORT 502
WITH PERSISTENCE true
ADD MAPPING Registers
WITH SOURCE_TOPIC "plant/line1"
ADD TAG Setpoint
WITH ADDRESS "100"
WITH ADDRESS_TYPE "HOLDING_REGISTER"
WITH DATA_TYPE "FLOAT"
WITH WRITABLE "true"
ADD TAG LastGoodTemp
WITH ADDRESS "200"
WITH ADDRESS_TYPE "INPUT_REGISTER"
WITH DATA_TYPE "FLOAT"
WITH PERSISTENCE "true"
ADD TAG ScratchSetpoint
WITH ADDRESS "300"
WITH ADDRESS_TYPE "HOLDING_REGISTER"
WITH DATA_TYPE "INT16"
WITH WRITABLE "true"
WITH PERSISTENCE "false"
```
* `Setpoint` — restored authoritatively to the last master-written value.
* `LastGoodTemp` — served from snapshot until `plant/line1/LastGoodTemp` next publishes.
* `ScratchSetpoint` — always comes back at zero.
### Caching slow or infrequently-updated sources
Broker-fed tags are not persisted by default. When a source updates infrequently—an hourly energy total, a shift-latched value—force-persist with `WITH PERSISTENCE "true"` so the last received value is re-served on cold start until the next live publish overrides it.
```lot wrap focus={6-12} theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
DEFINE ROUTE MeterServer WITH TYPE MODBUS_SERVER
ADD MODBUS_SERVER_CONFIG
WITH PORT 502
WITH PERSISTENCE true
ADD MAPPING Meters
WITH SOURCE_TOPIC "plant/energy"
ADD TAG TotalEnergy
WITH ADDRESS "100"
WITH ADDRESS_TYPE "INPUT_REGISTER"
WITH DATA_TYPE "FLOAT"
WITH PERSISTENCE "true"
```
If every tag in a mapping is a slow source, set `WITH PERSISTENCE "true"` once on the `ADD MAPPING` block instead of per tag.
### Storage & durability
* Values are written through a non-blocking background worker on every change (rapid updates coalesce to the latest value).
* The snapshot lives in an encrypted-at-rest database under the broker data directory.
* A snapshot is keyed to the route's tag definitions by a fingerprint. If addresses or types change between runs, the stale snapshot is discarded rather than replayed into reshaped addresses.
***
## Verify Your Server
Add the route definition to your broker and confirm route status shows **Connected** (the endpoint is listening).
Use any standards-compliant Modbus TCP master. Connect to `:` and address the route's `UNIT_ID` (default 1).
Publish a test value to a bound `SOURCE_TOPIC`. Read the corresponding address from the master and confirm the decoded value matches.
For a writable tag with a `DESTINATION_TOPIC`, write a value from the master. Subscribe to the destination topic with MQTT Explorer or any MQTT client and confirm the publish arrives within one `WITH EVERY` interval.
***
## Limitations
Each `MODBUS_SERVER` route binds exclusively to its `PORT`. A second route on the same port fails to start. Give each route its own port.
***
## Troubleshooting
Publish `-checkRouteConnection ` to `$SYS/Coreflux/Command`. For a server route this reports whether the hosted endpoint is **listening**, along with the endpoint, port, unit ID, and current connection count.
* **Port in use** — Another route or process owns `PORT`. Choose a free port or stop the conflicting service.
* **Bind error** — Check broker logs. Ensure `HOST` is a valid interface on the machine.
Confirm `HOST`/`PORT`, network path, and firewall rules. The master must target the route's `UNIT_ID` (default 1).
Registers default to zero until first set. Verify `SOURCE_TOPIC` has published; retained MQTT values are seeded at startup.
* Confirm the tag is `WRITABLE` on a `HOLDING_REGISTER` or `COIL` with a `DESTINATION_TOPIC`.
* The value surfaces within one `WITH EVERY` interval—verify the cadence.
Read-only banks (`INPUT_REGISTER`, `DISCRETE_INPUT`) and non-writable tags reject master writes by design. Undeclared addresses return Illegal Data Address.
**Accepted master writes:** FC06 (Write Single Register) and FC16 (Write Multiple Registers) on `WRITABLE` `HOLDING_REGISTER` tags; FC05 (Write Single Coil) and FC15 (Write Multiple Coils) on `WRITABLE` `COIL` tags.
**Rejected master writes:** any write to `INPUT_REGISTER` or `DISCRETE_INPUT`; writes to non-`WRITABLE` tags; writes to addresses outside the declared tag range (including partial FC16 ranges that extend beyond a STRING tag's reserved registers).
**Problem:** A master writes a STRING with FC16, but the server returns Illegal Data Address.
**Check:** Confirm the full holding-register range is declared by the tag. STRING capacity is `ceil(LENGTH / 2)` registers. An omitted `WITH LENGTH` reserves the default **40 bytes** (**20 registers**) and logs a Warning; older broker versions reserved only one register when the setting was omitted. `STRING_SIZE` remains accepted as an alias.
**Solution:** Set `WITH LENGTH` to the complete byte width the master writes, up to **246 bytes** (**123 registers**). Rejected undeclared holding-register ranges log `Master write rejected: … Check WITH LENGTH on STRING tags.`
The broker logs master activity for this route in the broker log:
* **Accepted writes** — FC06/FC16 on writable `HOLDING_REGISTER` tags; FC05/FC15 on writable `COIL` tags, with bank-aware function-code labels.
* **Rejected writes** — read-only banks, non-writable tags, and undeclared address ranges (including partial FC16 spans beyond a STRING tag's reserved registers).
* **Rejected reads** — undeclared or out-of-range read requests.
* **Undeclared holding-register ranges** — rejection messages include `Check WITH LENGTH on STRING tags` when sizing is the likely cause.
* **Connections** — open/close events with the active connection count from the existing watchdog.
These logs **cannot identify the master's source IP** — it is not available to the server route.
Align `BYTE_ORDER` and `WORD_ORDER` with the master's expectation. Default is big-endian, high word first.
Confirm tag `ADDRESS` values match how the master addresses the server. Use `WITH ONE_BASED_ADDRESSES true` when your tag labels follow device-manual numbering (register 1 = address `"1"`). Modicon notation (`400100`) works regardless of the flag.
`MAX_CONNECTIONS` is a **best-effort** soft cap—the underlying library exposes no per-connection control, so exceeding the cap is logged rather than enforced at accept time. Restrict concurrency at the network layer if a hard cap is required.
***
## Best Practices
Prefer `WITH HOST "192.168.10.5"` over `0.0.0.0` when only one network should reach the server.
Use `WITH PORT GET ENV "MODBUS_PORT"` instead of hard-coding environment-specific ports.
Only declared addresses are served. One `ADD TAG` per register or coil a master needs.
Leave `WRITABLE` unset for read-only points. Non-writable holding registers and read-only banks reject writes with a Modbus exception.
Set `WORD_ORDER "LITTLEENDIAN"` when the master expects low word first. Default is big-endian, high word first.
Use a shorter interval for responsive control loops; a longer one to reduce churn for slowly-changing values.
Document which route owns which port to avoid startup conflicts when adding routes.
Modbus TCP has no authentication. Place the server behind a firewall or VLAN and rely on HUB topic permissions for MQTT-side security.
Watch `$SYS/Coreflux/Routes/{RouteName}/status`. `Connected` means the server is listening—investigate `Error` immediately (usually a port conflict).
***
## Next Steps
Connect to remote Modbus devices and poll register data into MQTT.
Connect to remote Modbus RTU devices over serial.
# OPC UA Client Route
Source: https://docs.coreflux.org/latest/lot-language/routes/industrial/opcua-client
Connect to remote OPC UA servers and poll device data into MQTT with security and BrowsePath support
## OPC UA Client
This page documents the **client** route (`OPCUA`)—the broker connects **to** a remote OPC UA server and reads or writes device data over MQTT. To expose MQTT topics **to** OPC UA clients instead (SCADA, UaExpert, PLCs connect to the broker), see [OPC UA Server](./opcua-server).
The `OPCUA` route enables communication with OPC UA servers, providing a standardized, cross-platform protocol for industrial automation. It supports multiple authentication modes, security policies, and both NodeId and BrowsePath addressing.
OPC UA is vendor-neutral and widely supported by modern industrial equipment. Use it when you need secure, standardized communication across different manufacturer's devices.
## Basic Syntax
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
DEFINE ROUTE OPCServer WITH TYPE OPCUA
ADD OPCUA_CONFIG
WITH ENDPOINT_URL "opc.tcp://192.168.1.100:4840"
WITH AUTH_MODE 0
WITH AUTO_ACCEPT_UNTRUSTED_CERTIFICATES "true"
ADD MAPPING ProcessData
WITH EVERY 1 SECOND
ADD TAG Temperature
WITH ADDRESS "ns=2;s=Channel1.Device1.Temperature"
WITH DATA_TYPE "FLOAT"
WITH SOURCE_TOPIC "opcua/temperature"
```
***
## Connection Configuration
### OPCUA\_CONFIG Parameters
OPC UA server endpoint URL (e.g., `opc.tcp://localhost:4840`).
Authentication mode:
* `0` - Anonymous (no authentication)
* `1` - Username/Password
* `2` - Certificate
Default: 0.
Username for authentication (required if AUTH\_MODE is 1).
Password for authentication (required if AUTH\_MODE is 1).
Enable security mode. Default: false.
Security policy level:
* `0` - None
* `1` - Basic128Rsa15
* `2` - Basic256
* `3` - Basic256Sha256
* `4` - Aes128\_Sha256\_RsaOaep
* `5` - Aes256\_Sha256\_RsaPss
Default: 0.
Message security mode:
* `0` - None
* `1` - Sign
* `2` - SignAndEncrypt
Default: 0.
Root directory for the four-folder PKI store (`own`, `issuer`, `trusted`, `rejected`). Defaults to the OPC Foundation layout under local application data. Set an explicit path—and mount it as a volume in Docker—so certificates persist across restarts. See [Certificate Management](#certificate-management).
Path to a PKCS#12 (`.pfx`) or PEM client certificate used when `AUTH_MODE` is `2`. The broker loads it, adopts its application URI, and stores it under `PKI_DIR/own`. When omitted, a self-signed certificate is auto-generated on first connect.
Path to the private key PEM file when `CLIENT_CERT_PATH` points to a certificate-only PEM. Not needed for PKCS#12 bundles.
Password for the PKCS#12 bundle or encrypted private key. Use `GET SECRET`—never hard-code passwords in route definitions.
Automatically accept untrusted server certificates. Default: false. For production, leave false and trust the server certificate explicitly—see [Certificate Management](#certificate-management).
Suppress nonce validation errors. Default: false.
Connection timeout in milliseconds. Default: 90000.
***
## Security Configuration
For development or trusted networks:
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
ADD OPCUA_CONFIG
WITH ENDPOINT_URL "opc.tcp://192.168.1.100:4840"
WITH AUTH_MODE 0
WITH USE_SECURITY "false"
WITH AUTO_ACCEPT_UNTRUSTED_CERTIFICATES "true"
```
Basic authentication:
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
ADD OPCUA_CONFIG
WITH ENDPOINT_URL "opc.tcp://192.168.1.100:4840"
WITH AUTH_MODE 1
WITH USERNAME "opcuser"
WITH PASSWORD "secure_password"
WITH USE_SECURITY "true"
WITH SECURITY_POLICY 3
WITH MESSAGE_SECURITY 2
```
Authenticate with an X.509 certificate identity. The application instance certificate secures the channel and identifies the client to the server:
```lot wrap focus={4-9} theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
ADD OPCUA_CONFIG
WITH ENDPOINT_URL "opc.tcp://192.168.1.100:4840"
WITH AUTH_MODE 2
WITH USE_SECURITY true
WITH SECURITY_POLICY 3
WITH MESSAGE_SECURITY 2
WITH PKI_DIR "/data/opcua/pki"
WITH CLIENT_CERT_PATH "/certs/client.pfx"
WITH CLIENT_CERT_PASSWORD GET SECRET "OPCUA_CLIENT_CERT_PASSWORD"
```
Omit `CLIENT_CERT_PATH` to auto-generate a self-signed certificate. You still need mutual trust between broker and server—see [Certificate Management](#certificate-management).
***
## Certificate Management
A secured OPC UA client connection requires **two certificates that trust each other**: the broker's **application instance certificate** (channel security and, with `AUTH_MODE 2`, user identity) and the **server's certificate** (must be in your `trusted` store). For server-side trust workflows, see [OPC UA Server](./opcua-server#certificate-management).
**Like exchanging ID badges at a secure facility.** Both sides must recognize each other's badge before the door opens—moving a certificate from `rejected` to `trusted` is how you approve a new badge.
### PKI directory layout
Both client and server routes store certificates in four folders under `PKI_DIR`:
| Folder | Holds | File form |
| ---------- | ---------------------------------------------------------------------------- | ----------------- |
| `own` | This application's certificate (`own/certs`) and private key (`own/private`) | `*.der` + `*.pfx` |
| `issuer` | Trusted CA / issuer certificates for chain validation | `*.der` |
| `trusted` | Trusted peer certificates—the server you accept goes here (`trusted/certs`) | `*.der` |
| `rejected` | Certificates seen during a handshake but not yet trusted | `*.der` |
### Where certificates live
When `PKI_DIR` is not set, it defaults to `%LocalApplicationData%/OPC Foundation/pki`:
| OS | Default path |
| ------- | -------------------------------------------------------------------------- |
| macOS | `~/Library/Application Support/OPC Foundation/pki` |
| Linux | `$XDG_DATA_HOME/OPC Foundation/pki` or `~/.local/share/OPC Foundation/pki` |
| Windows | `%LOCALAPPDATA%\OPC Foundation\pki` |
Relocate the store to a path you control:
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
ADD OPCUA_CONFIG
WITH ENDPOINT_URL "opc.tcp://server:4840"
WITH PKI_DIR "/data/opcua/pki"
```
In Docker, the default PKI location is ephemeral—the broker regenerates its certificate on every restart and any server that trusted the previous one will reject the new one. **Always set `PKI_DIR` to a mounted volume.**
### Inspecting and exporting certificates
Certificates are plain files you can inspect with `openssl`:
```bash wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
# The broker's own certificate and private key
ls "$PKI_DIR"/own/certs/*.der
ls "$PKI_DIR"/own/private/*.pfx
# Inspect subject, validity, and application URI SAN
openssl x509 -inform der -in "$PKI_DIR"/own/certs/.der -noout -subject -dates -ext subjectAltName
# Export public cert for the server's trust list
openssl x509 -inform der -in "$PKI_DIR"/own/certs/.der -out broker-client.pem
```
Give peers the **public** certificate (`own/certs/*.der`). The private key (`own/private/*.pfx`) never leaves the broker.
### Auto-generated client certificate
With `AUTH_MODE 2` and no `CLIENT_CERT_PATH`, the broker auto-generates a self-signed certificate (`CN=CorefluxBroker_OpcUaClient, O=Coreflux`) in `PKI_DIR/own` on first connect:
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
DEFINE ROUTE PlantClient WITH TYPE OPCUA
ADD OPCUA_CONFIG
WITH ENDPOINT_URL "opc.tcp://server:4840"
WITH AUTH_MODE 2
WITH USE_SECURITY true
WITH SECURITY_POLICY 3
WITH MESSAGE_SECURITY 2
WITH PKI_DIR "/data/opcua/pki"
```
You still must (1) get this broker certificate trusted by the server and (2) trust the server's certificate—see the workflows below.
An application instance certificate **must** carry an application URI in its Subject Alternative Name. The OPC UA stack matches certificates by that URI and rejects certificates without one.
### Supply your own client certificate
Use a certificate from your PKI via `CLIENT_CERT_PATH`. Generate it with a mandatory URI SAN:
```bash wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
# Self-signed example—use your CA in production. The URI SAN is mandatory.
openssl req -x509 -newkey rsa:2048 -keyout client-key.pem -out client-cert.pem -days 825 -nodes \
-subj "/CN=PlantClient/O=ACME" \
-addext "subjectAltName=URI:urn:acme:opcua:plant-client" \
-addext "keyUsage=digitalSignature,keyEncipherment,dataEncipherment,nonRepudiation" \
-addext "extendedKeyUsage=clientAuth,serverAuth"
# Bundle into PKCS#12 (.pfx)
openssl pkcs12 -export -inkey client-key.pem -in client-cert.pem \
-out client.pfx -passout pass:CHANGEME -name plantclient
```
```lot wrap focus={6-9} theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
DEFINE ROUTE PlantClient WITH TYPE OPCUA
ADD OPCUA_CONFIG
WITH ENDPOINT_URL "opc.tcp://server:4840"
WITH AUTH_MODE 2
WITH USE_SECURITY true
WITH SECURITY_POLICY 3
WITH MESSAGE_SECURITY 2
WITH CLIENT_CERT_PATH "/certs/client.pfx"
WITH CLIENT_CERT_PASSWORD GET SECRET "OPCUA_CLIENT_CERT_PASSWORD"
WITH PKI_DIR "/data/opcua/pki"
```
Point `CLIENT_CERT_PATH` at the certificate PEM. If the key is in a separate file, add `CLIENT_KEY_PATH`:
```lot wrap focus={6-9} theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
DEFINE ROUTE PlantClient WITH TYPE OPCUA
ADD OPCUA_CONFIG
WITH ENDPOINT_URL "opc.tcp://server:4840"
WITH AUTH_MODE 2
WITH USE_SECURITY true
WITH CLIENT_CERT_PATH "/certs/client-cert.pem"
WITH CLIENT_KEY_PATH "/certs/client-key.pem"
WITH CLIENT_CERT_PASSWORD GET SECRET "OPCUA_CLIENT_KEY_PASSWORD"
WITH PKI_DIR "/data/opcua/pki"
```
### Trust the server's certificate
Required when `USE_SECURITY true` and `AUTO_ACCEPT_UNTRUSTED_CERTIFICATES false`:
The first handshake fails and the server's certificate is written to `PKI_DIR/rejected/certs`.
```bash wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
mv "$PKI_DIR"/rejected/certs/.der "$PKI_DIR"/trusted/certs/
```
The route reconnects automatically—the handshake now succeeds.
For testing only, skip this with `WITH AUTO_ACCEPT_UNTRUSTED_CERTIFICATES true`.
### Get the broker trusted by the server
For `AUTH_MODE 2`, the server must trust the broker's client certificate:
```bash wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
openssl x509 -inform der -in "$PKI_DIR"/own/certs/.der -out broker-client.pem
```
Import `broker-client.pem` (or the `.der`) into the server's trusted certificate store—via its PKI `trusted` folder or vendor certificate manager.
The server now accepts the certificate identity.
### Rotation and renewal
* **Auto-generated certificates** — Delete files in `PKI_DIR/own/certs` and `PKI_DIR/own/private`, then reconnect; the broker regenerates a new self-signed certificate.
* **Supplied certificates** — Replace the `.pfx`/PEM file, remove the old copy from `PKI_DIR/own`, and redeploy. Re-distribute the new public certificate to the server's trust list before the old one expires.
* Plan rotation **before expiry**—an expired application instance certificate fails the handshake.
### Verify the connection
After mutual trust is configured, confirm the route connects:
```bash wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
-checkRouteConnection PlantClient
```
Publish that command to `$SYS/Coreflux/Command`. A response with `"success": true` means the certificate-authenticated session is established.
***
## NodeId Addressing
OPC UA uses NodeIds to identify variables. The format is: `ns=;s=` or `ns=;i=`
| Format | Example | Description |
| ------------------ | --------------------------------------------- | ------------------------------------ |
| String identifier | `ns=2;s=Channel1.Device1.Tag1` | Most common, human-readable |
| Numeric identifier | `ns=2;i=1234` | More efficient, used by some servers |
| GUID identifier | `ns=2;g=12345678-1234-1234-1234-123456789012` | Globally unique identifiers |
### Finding NodeIds
Use an OPC UA browser tool (like UaExpert, Prosys OPC UA Browser, or similar) to explore the server's address space and find the correct NodeIds.
### BrowsePath Mode
For servers that support it, you can use symbolic paths instead of NodeIds:
Enable BrowsePath mode - treat all TAG addresses as browse paths instead of NodeIds.
Root path for browsing when USE\_BROWSE\_PATH is enabled. Default: Objects.
#### BrowsePath Example
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
ADD OPCUA_CONFIG
WITH ENDPOINT_URL "opc.tcp://192.168.1.100:4840"
WITH USE_BROWSE_PATH "true"
WITH BROWSE_ROOT_PATH "Objects"
ADD MAPPING Tags
WITH EVERY 1 SECOND
ADD TAG Temperature
WITH ADDRESS "Aliases/MyDevice/Temperature"
WITH DATA_TYPE "FLOAT"
WITH SOURCE_TOPIC "opcua/temperature"
```
***
## Data Types
| Data Type | OPC UA Type | Description |
| ------------------------------ | ----------- | ------------------------------------------------------- |
| `BOOLEAN` / `BOOL` | Boolean | True/False value |
| `SBYTE` / `SINT` | SByte | Signed 8-bit integer |
| `BYTE` / `USINT` | Byte | Unsigned 8-bit integer |
| `INT16` / `INT` / `WORD` | Int16 | 16-bit integer |
| `UINT16` / `UINT` | UInt16 | Unsigned 16-bit integer |
| `INT32` / `DINT` | Int32 | Signed 32-bit integer |
| `UINT32` / `UDINT` / `DWORD` | UInt32 | Unsigned 32-bit integer |
| `INT64` / `LINT` | Int64 | Signed 64-bit integer |
| `UINT64` / `ULINT` / `LWORD` | UInt64 | Unsigned 64-bit integer |
| `FLOAT` / `FLOAT32` / `REAL` | Float | 32-bit floating point |
| `DOUBLE` / `FLOAT64` / `LREAL` | Double | 64-bit floating point |
| `CHAR` | Char | Single character |
| `WCHAR` | WChar | Wide character |
| `TIME` | Time | Time duration |
| `DATE` | Date | Date value |
| `TOD` | TimeOfDay | Time of day |
| `STRING` | String | Unicode string; sized with `LENGTH` (default 256 bytes) |
### String sizing
For `STRING` tags, `WITH LENGTH` sets the maximum string size in **bytes** (default **256**). `STRING_SIZE` is accepted as an alias for the same field.
```lot wrap focus={11} theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
DEFINE ROUTE OpcUaStrings WITH TYPE OPCUA
ADD OPCUA_CONFIG
WITH ENDPOINT_URL "opc.tcp://192.168.1.100:4840"
WITH AUTH_MODE 0
WITH AUTO_ACCEPT_UNTRUSTED_CERTIFICATES "true"
ADD MAPPING ProductData
WITH EVERY 1 SECOND
ADD TAG ProductName
WITH ADDRESS "ns=2;s=Product.Name"
WITH DATA_TYPE "STRING"
WITH LENGTH 128
WITH SOURCE_TOPIC "opcua/product/name"
```
For cross-protocol defaults and caps, see [String sizing by Route](./overview#string-sizing-by-route) on the industrial overview.
***
## TAG Configuration
### Complete TAG Example
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
ADD TAG ProcessTemperature
WITH ADDRESS "ns=2;s=Process.Temperature"
WITH DATA_TYPE "FLOAT"
WITH SOURCE_TOPIC "opcua/process/temperature"
WITH SCALING 1
WITH OFFSET 0
WITH UNIT "°C"
WITH DECIMAL_PLACES 2
WITH MIN_VALUE -50
WITH MAX_VALUE 500
WITH DEADBAND 0.5
WITH PUBLISH_MODE "JSON"
WITH WRITABLE "true"
WITH DESTINATION_TOPIC "opcua/process/temperature/set"
WITH IS_ARRAY "false"
WITH DESCRIPTION "Main process temperature sensor"
```
### TAG Parameters
OPC UA NodeId (e.g., `ns=2;s=MyVariable` or `ns=2;i=1234`) or BrowsePath if USE\_BROWSE\_PATH is enabled.
OPC UA data type: `BOOLEAN`, `BYTE`, `INT16`, `UINT16`, `INT32`, `UINT32`, `INT64`, `UINT64`, `FLOAT`, `DOUBLE`, `STRING`.
Indicates if the value is an array. Default: false.
**STRING only.** Maximum string size in bytes. Alias: `STRING_SIZE`.
Multiplier for value transformation. Default: 1.0.
Offset added after scaling. Default: 0.0.
Decimal places in output. Default: 2.
Minimum allowed value.
Maximum allowed value.
Minimum change to trigger publish. Default: 0.0.
Topic where PLC values are published. Subscribe here to receive sensor data.
Output format: `VALUE_ONLY` or `JSON`. Default: VALUE\_ONLY.
Engineering unit for documentation.
Human-readable description.
Allow writing to this node. Default: false.
Topic to send write commands. Publish a value here to write it to the PLC.
***
## Event-Based Operations
For on-demand OPC UA operations (not polling), use the EVENT syntax. Publish a message to SOURCE\_TOPIC to trigger the operation; the route executes it and publishes the result to DESTINATION\_TOPIC.
### Supported Operations
| Operation | Description | Query Parameters |
| --------- | --------------------------- | ------------------------------- |
| `READ` | Read a node value on demand | `node_id`, `data_type` |
| `WRITE` | Write a value to a node | `node_id`, `data_type`, `value` |
### Query Parameters
| Parameter | Description | Example |
| ----------- | --------------------------------- | ------------------------------------ |
| `operation` | Operation type: `READ` or `WRITE` | `READ` |
| `node_id` | OPC UA NodeId (string or numeric) | `ns=2;i=2`, `ns=2;s=Process.Temp` |
| `data_type` | OPC UA data type | `INT32`, `FLOAT`, `DOUBLE`, `STRING` |
| `value` | Value to write (WRITE only) | `42`, `23.5`, `"hello"` |
### Read Example
Read a node value on demand:
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
ADD EVENT ReadOnDemand
WITH SOURCE_TOPIC "opcua/commands/read"
WITH DESTINATION_TOPIC "opcua/responses/read"
WITH QUERY "{operation: READ, node_id: ns=2;i=2, data_type: INT32}"
```
### Write Example
Write a FLOAT value to a node on demand:
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
ADD EVENT WriteSetpoint
WITH SOURCE_TOPIC "opcua/commands/write"
WITH DESTINATION_TOPIC "opcua/responses/write"
WITH QUERY "{operation: WRITE, node_id: ns=2;s=Process.Setpoint, data_type: FLOAT, value: 75.0}"
```
***
## Complete Examples
Simple OPC UA connection with anonymous access:
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
DEFINE ROUTE OPCServer WITH TYPE OPCUA
ADD OPCUA_CONFIG
WITH ENDPOINT_URL "opc.tcp://192.168.1.100:4840"
WITH AUTH_MODE 0
WITH AUTO_ACCEPT_UNTRUSTED_CERTIFICATES "true"
ADD MAPPING ProcessData
WITH EVERY 1 SECOND
ADD TAG Temperature
WITH ADDRESS "ns=2;s=Channel1.Device1.Temperature"
WITH DATA_TYPE "FLOAT"
WITH SOURCE_TOPIC "opcua/temperature"
WITH UNIT "°C"
ADD TAG Pressure
WITH ADDRESS "ns=2;s=Channel1.Device1.Pressure"
WITH DATA_TYPE "FLOAT"
WITH SOURCE_TOPIC "opcua/pressure"
WITH UNIT "bar"
ADD TAG Status
WITH ADDRESS "ns=2;s=Channel1.Device1.Status"
WITH DATA_TYPE "INT32"
WITH SOURCE_TOPIC "opcua/status"
```
Production setup with encryption:
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
DEFINE ROUTE SecureOPC WITH TYPE OPCUA
ADD OPCUA_CONFIG
WITH ENDPOINT_URL "opc.tcp://192.168.1.100:4840"
WITH AUTH_MODE 1
WITH USERNAME "operator"
WITH PASSWORD "secure_password_here"
WITH USE_SECURITY "true"
WITH SECURITY_POLICY 3
WITH MESSAGE_SECURITY 2
WITH TIMEOUT 30000
ADD MAPPING SecureData
WITH EVERY 500 MILLISECONDS
ADD TAG CriticalValue
WITH ADDRESS "ns=2;s=Safety.CriticalValue"
WITH DATA_TYPE "FLOAT"
WITH SOURCE_TOPIC "secure/critical"
WITH PUBLISH_MODE "JSON"
```
Using symbolic paths:
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
DEFINE ROUTE BrowsePathOPC WITH TYPE OPCUA
ADD OPCUA_CONFIG
WITH ENDPOINT_URL "opc.tcp://192.168.1.100:4840"
WITH AUTH_MODE 0
WITH USE_BROWSE_PATH "true"
WITH BROWSE_ROOT_PATH "Objects"
WITH AUTO_ACCEPT_UNTRUSTED_CERTIFICATES "true"
ADD MAPPING ProcessTags
WITH EVERY 1 SECOND
ADD TAG Temperature
WITH ADDRESS "DataAccess/AnalogItems/Temperature"
WITH DATA_TYPE "DOUBLE"
WITH SOURCE_TOPIC "opcua/temperature"
ADD TAG Setpoint
WITH ADDRESS "DataAccess/AnalogItems/Setpoint"
WITH DATA_TYPE "DOUBLE"
WITH SOURCE_TOPIC "opcua/setpoint"
WITH WRITABLE "true"
WITH DESTINATION_TOPIC "opcua/setpoint/write"
```
Reading array data:
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
DEFINE ROUTE ArrayOPC WITH TYPE OPCUA
ADD OPCUA_CONFIG
WITH ENDPOINT_URL "opc.tcp://192.168.1.100:4840"
WITH AUTH_MODE 0
WITH AUTO_ACCEPT_UNTRUSTED_CERTIFICATES "true"
ADD MAPPING ArrayData
WITH EVERY 1 SECOND
ADD TAG TemperatureArray
WITH ADDRESS "ns=2;s=Process.TemperatureArray"
WITH DATA_TYPE "FLOAT"
WITH IS_ARRAY "true"
WITH SOURCE_TOPIC "opcua/temperatures"
WITH PUBLISH_MODE "JSON"
ADD TAG StatusBits
WITH ADDRESS "ns=2;s=Process.StatusBits"
WITH DATA_TYPE "BOOLEAN"
WITH IS_ARRAY "true"
WITH SOURCE_TOPIC "opcua/status/bits"
```
Connecting to Kepware OPC UA server:
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
DEFINE ROUTE KepwareServer WITH TYPE OPCUA
ADD OPCUA_CONFIG
WITH ENDPOINT_URL "opc.tcp://192.168.1.50:49320"
WITH AUTH_MODE 0
WITH AUTO_ACCEPT_UNTRUSTED_CERTIFICATES "true"
ADD MAPPING KepwareTags
WITH EVERY 500 MILLISECONDS
ADD TAG Motor1Speed
WITH ADDRESS "ns=2;s=Channel1.Device1.Motor1_Speed"
WITH DATA_TYPE "FLOAT"
WITH SOURCE_TOPIC "kepware/motor1/speed"
WITH UNIT "RPM"
ADD TAG Motor1Current
WITH ADDRESS "ns=2;s=Channel1.Device1.Motor1_Current"
WITH DATA_TYPE "FLOAT"
WITH SOURCE_TOPIC "kepware/motor1/current"
WITH UNIT "A"
ADD TAG Motor1Running
WITH ADDRESS "ns=2;s=Channel1.Device1.Motor1_Running"
WITH DATA_TYPE "BOOLEAN"
WITH SOURCE_TOPIC "kepware/motor1/running"
```
Continuous monitoring with on-demand read and write events in the same route:
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
DEFINE ROUTE FullOPCSetup WITH TYPE OPCUA
ADD OPCUA_CONFIG
WITH ENDPOINT_URL "opc.tcp://192.168.1.100:4840"
WITH AUTH_MODE 0
WITH AUTO_ACCEPT_UNTRUSTED_CERTIFICATES "true"
ADD MAPPING ProcessData
WITH EVERY 1 SECOND
ADD TAG Temperature
WITH ADDRESS "ns=2;s=Channel1.Device1.Temperature"
WITH DATA_TYPE "FLOAT"
WITH SOURCE_TOPIC "opcua/temperature"
WITH UNIT "°C"
ADD TAG Pressure
WITH ADDRESS "ns=2;s=Channel1.Device1.Pressure"
WITH DATA_TYPE "FLOAT"
WITH SOURCE_TOPIC "opcua/pressure"
WITH UNIT "bar"
ADD EVENT ReadNode
WITH SOURCE_TOPIC "opcua/commands/read"
WITH DESTINATION_TOPIC "opcua/responses/read"
WITH QUERY "{operation: READ, node_id: ns=2;s=Channel1.Device1.Status, data_type: INT32}"
ADD EVENT WriteSetpoint
WITH SOURCE_TOPIC "opcua/commands/write"
WITH DESTINATION_TOPIC "opcua/responses/write"
WITH QUERY "{operation: WRITE, node_id: ns=2;s=Channel1.Device1.Setpoint, data_type: FLOAT, value: 0}"
```
The MAPPING continuously reads temperature and pressure. To trigger an on-demand status read, publish any message to `opcua/commands/read`. To write a setpoint, publish to `opcua/commands/write`.
***
## Security Best Practices
Always enable security for production deployments:
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
WITH USE_SECURITY "true"
WITH SECURITY_POLICY 3 // Basic256Sha256
WITH MESSAGE_SECURITY 2 // SignAndEncrypt
```
In production, properly configure certificate trust:
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
WITH AUTO_ACCEPT_UNTRUSTED_CERTIFICATES false
```
Move server certificates from `PKI_DIR/rejected/certs` to `PKI_DIR/trusted/certs` during commissioning. See [Certificate Management](#certificate-management).
Prefer username/password or certificate authentication over anonymous:
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
WITH AUTH_MODE 1 // or '2' for certificate
WITH USERNAME "operator"
WITH PASSWORD "strong_password_here"
```
***
## Troubleshooting
* Verify endpoint URL is correct
* Check firewall allows TCP port (usually 4840)
* Ensure server is running and accessible
* Try increasing TIMEOUT value
* Verify security policy matches server configuration
* **Server certificate not trusted** — Move it from `PKI_DIR/rejected/certs` to `PKI_DIR/trusted/certs`, or set `AUTO_ACCEPT_UNTRUSTED_CERTIFICATES true` for testing only
* **Supplied certificate ignored** — Reissue with a `subjectAltName=URI:...` SAN; certificates without an application URI are rejected
* **Server rejects client certificate** — Export the broker's public cert from `PKI_DIR/own/certs` and add it to the server's trust list
* **Certificate regenerates every restart** — Set `PKI_DIR` to a persistent volume (especially in Docker)
* **`BadCertificateUriInvalid`** — Regenerate the certificate or set a stable application URI in the SAN
* Verify the file path is correct and readable by the broker
* Check `CLIENT_CERT_PASSWORD` matches the PKCS#12 bundle or encrypted key
* For a split PEM (certificate and key in separate files), set `CLIENT_KEY_PATH`
* The broker log names the failing file—check it for the exact cause
* Use OPC UA browser to verify correct NodeId
* Check namespace index (ns=) is correct
* Verify node exists in server's address space
* Try BrowsePath mode if NodeId is unclear
* Verify username and password
* Check user has appropriate permissions
* Ensure AUTH\_MODE matches server requirements
* Verify DATA\_TYPE matches server variable type
* Check IS\_ARRAY setting for array values
* Verify SCALING and OFFSET calculations
***
## Next Steps
Expose MQTT topics as OPC UA nodes for SCADA and OT clients.
Connect to TwinCAT systems.
# OPC UA Server Route
Source: https://docs.coreflux.org/latest/lot-language/routes/industrial/opcua-server
Expose MQTT topics as OPC UA nodes so SCADA systems, PLCs, and OPC UA clients connect to your Coreflux broker
## OPC UA Server
This page documents the **server** route (`OPCUA_SERVER`)—the broker **hosts** an OPC UA server and exposes MQTT topics as nodes. External clients connect to the broker. To connect the broker **to** a remote OPC UA device instead, see [OPC UA Client](./opcua-client).
The `OPCUA_SERVER` route makes your broker **host** an OPC UA server. External clients—SCADA, UaExpert, PLCs—connect **to the broker** and see your MQTT topics as OPC UA nodes.
Use `OPCUA` when you need to **read from** a device. Use `OPCUA_SERVER` when OT systems need to **browse and write** values that live on your MQTT bus.
**Like turning your broker into a shop window.** MQTT messages update what's on display (node values), and customers (OPC UA clients) can look—and, for writable tags, change items that publish back to MQTT.
## When to Use This Route
* **SCADA integration** — Let a SCADA or HMI browse broker data without custom drivers.
* **OT/IT bridge** — Expose IoT sensor streams to plant-floor tools that speak OPC UA.
* **Bidirectional control** — Writable tags let clients adjust setpoints; the broker publishes the new value to MQTT.
* **Dynamic discovery** — Wildcard topic mappings create nodes automatically as matching topics first publish.
***
The sections below cover data flow, configuration, persistence, and production patterns.
## Quick Start
Map MQTT topics to fixed OPC UA nodes. Each `ADD TAG` creates one node; publishing to `{SOURCE_TOPIC}/{TagName}` updates it.
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
DEFINE ROUTE PlantServer WITH TYPE OPCUA_SERVER
ADD OPCUA_SERVER_CONFIG
WITH SERVER_PORT 4840
ADD MAPPING Line1
WITH SOURCE_TOPIC "factory/line1"
ADD TAG Temperature
WITH DATA_TYPE "FLOAT"
ADD TAG Counter
WITH DATA_TYPE "INT32"
```
* Publish `23.5` to `factory/line1/Temperature` → node `ns=2;s=PlantServer/Temperature` reads `23.5`.
* Publish `1042` to `factory/line1/Counter` → node `ns=2;s=PlantServer/Counter` reads `1042`.
Allow OPC UA clients to write a node; the broker publishes the value back to MQTT on the same topic, retained.
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
DEFINE ROUTE PlantServer WITH TYPE OPCUA_SERVER
ADD OPCUA_SERVER_CONFIG
WITH SERVER_PORT 4840
ADD MAPPING Line1
WITH SOURCE_TOPIC "factory/line1"
ADD TAG Setpoint
WITH DATA_TYPE "FLOAT"
WITH WRITABLE true
WITH DECIMAL_PLACES 1
WITH MIN_VALUE 0
WITH MAX_VALUE 100
```
MQTT publishes set the node. A client writing `72.4` to the Setpoint node publishes `72.4` to `factory/line1/Setpoint` (clamped and rounded per tag settings).
Omit `ADD TAG` and use an MQTT wildcard in `SOURCE_TOPIC`. Nodes appear automatically when matching topics first publish—see [Dynamic topic handling](#dynamic-topic-handling).
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
DEFINE ROUTE DynamicSensors WITH TYPE OPCUA_SERVER
ADD OPCUA_SERVER_CONFIG
WITH SERVER_PORT 4840
ADD MAPPING Sensors
WITH SOURCE_TOPIC "sensors/+/value"
```
First publish to `sensors/press1/value` creates node `ns=2;s=sensors/press1/value`. First publish to `sensors/tank2/value` creates another node. No route edit required.
Encrypt traffic, require credentials, and combine static tags with dynamic discovery:
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
DEFINE ROUTE SecurePlantServer WITH TYPE OPCUA_SERVER
ADD OPCUA_SERVER_CONFIG
WITH SERVER_PORT 4840
WITH ROOT_FOLDER "Plant"
WITH NAMESPACE_URI "urn:acme:plant"
WITH USE_SECURITY true
WITH SECURITY_POLICY 3
WITH MESSAGE_SECURITY 2
WITH ALLOW_ANONYMOUS false
WITH USERNAME GET ENV "OPCUA_USER"
WITH PASSWORD GET SECRET "OPCUA_PASSWORD"
ADD MAPPING Line1
WITH SOURCE_TOPIC "factory/line1"
ADD TAG Setpoint
WITH DATA_TYPE "FLOAT"
WITH WRITABLE true
WITH DECIMAL_PLACES 1
WITH MIN_VALUE 0
WITH MAX_VALUE 100
ADD MAPPING Sensors
WITH SOURCE_TOPIC "sensors/+/value"
```
Clients connect with `opc.tcp://:4840` using Basic256Sha256 and SignAndEncrypt. Store credentials with `GET ENV` / `GET SECRET`—never hard-code passwords.
Persist client-written setpoints across broker restarts:
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
DEFINE ROUTE ResilientServer WITH TYPE OPCUA_SERVER
ADD OPCUA_SERVER_CONFIG
WITH SERVER_PORT 4840
WITH PERSISTENCE true
ADD MAPPING Control
WITH SOURCE_TOPIC "plant/line1"
ADD TAG Setpoint
WITH DATA_TYPE "FLOAT"
WITH WRITABLE true
WITH DECIMAL_PLACES 1
WITH MIN_VALUE 0
WITH MAX_VALUE 100
ADD TAG Temperature
WITH DATA_TYPE "FLOAT"
```
After a restart, a client reading `Setpoint` gets the last value it wrote instead of `BadWaitingForInitialData`. `Temperature` refreshes from the live MQTT source, not the snapshot.
***
## How Data Flows
```mermaid theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
graph LR
subgraph mqtt [MQTT]
Topics[Topics]
end
subgraph broker [Coreflux Broker]
Route[OPCUA_SERVER Route]
Nodes[OPC UA Nodes]
end
subgraph ot [OT Clients]
Client[SCADA / UaExpert / PLC]
end
Topics -->|publish sets value| Route
Route --> Nodes
Nodes -->|browse / read| Client
Client -->|write writable tag| Route
Route -->|publish retained| Topics
```
| Direction | Behavior |
| --------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **MQTT → node** | Each tag binds to an MQTT topic via `SOURCE_TOPIC`. A publish on that topic sets the node value. The MQTT payload is authoritative—no scaling or rounding is applied inbound. Retained values present before the route starts are seeded at startup. |
| **node → MQTT** | When a tag has `WITH WRITABLE true`, an external client writing the node publishes the new value back to the **same** `SOURCE_TOPIC`, retained. `FLOAT` and `DOUBLE` values are rounded to `DECIMAL_PLACES` (default 2) and clamped by `MIN_VALUE` / `MAX_VALUE`. Client writes do not loop back into duplicate node updates. |
| **Wildcard mappings** | Wildcard `SOURCE_TOPIC` patterns create nodes on first publish. See [Dynamic topic handling](#dynamic-topic-handling). |
***
## Server Configuration
All settings live in the `OPCUA_SERVER_CONFIG` block. Store credentials with `GET ENV` or `GET SECRET`.
The example below shows a production-oriented config: custom identity, encrypted endpoint, username/password auth, and a persistent certificate store. Clients connect at `opc.tcp://:4840/coreflux`.
```lot wrap focus={2-16} theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
DEFINE ROUTE PlantOpcServer WITH TYPE OPCUA_SERVER
ADD OPCUA_SERVER_CONFIG
WITH SERVER_PORT 4840
WITH ENDPOINT_PATH "/coreflux"
WITH APPLICATION_NAME "Coreflux Plant Server"
WITH APPLICATION_URI "urn:plant.example.com:Coreflux:PlantOpcServer"
WITH NAMESPACE_URI "urn:plant.example.com:opcua"
WITH ROOT_FOLDER "Plant"
WITH PKI_DIR "/data/opcua-pki"
WITH USE_SECURITY true
WITH SECURITY_POLICY 3
WITH MESSAGE_SECURITY 2
WITH ALLOW_ANONYMOUS false
WITH USERNAME GET ENV "OPCUA_USER"
WITH PASSWORD GET SECRET "OPCUA_PASSWORD"
WITH AUTO_ACCEPT_UNTRUSTED_CERTIFICATES false
ADD MAPPING Tags
WITH SOURCE_TOPIC "plant/tags"
ADD TAG Status
WITH DATA_TYPE "INT32"
```
### Connection and identity
TCP port the server listens on (`opc.tcp`). Each server route claims its port process-wide—two routes cannot share the same port.
Optional path appended to the base address, for example `/coreflux`. Default: empty.
Application display name and certificate common name (CN).
Application URI. Must match the certificate SAN URI. Changing this regenerates the certificate.
Namespace URI for exposed nodes.
Root folder under `Objects` where tags are organized.
Directory for own, issuer, trusted, and rejected certificate stores. Defaults to the OPC Foundation PKI layout. In Docker, mount this path as a volume so certificates persist across restarts. See [Certificate Management](#certificate-management).
### Security
Offer a signed or encrypted endpoint.
This defaults to `true` to comply with OPC Foundation specifications, which require secure endpoints. For testing purposes, you might need to set it to `false`.
Security policy offered when `USE_SECURITY` is true:
| Value | Policy |
| ----- | ----------------------- |
| `0` | None |
| `3` | Basic256Sha256 |
| `4` | Aes128\_Sha256\_RsaOaep |
| `5` | Aes256\_Sha256\_RsaPss |
Message security mode. Requires `SECURITY_POLICY` ≥ 1 when using Sign or SignAndEncrypt.
| Value | Mode |
| ----- | -------------- |
| `0` | None |
| `1` | Sign |
| `2` | SignAndEncrypt |
When `USE_SECURITY` is true, also offer a None/None endpoint. Use for testing only.
### Authentication
Offer anonymous login. Set to `false` for production and configure `USERNAME`/`PASSWORD` or `ALLOW_CERTIFICATE_AUTH`.
Username for password-based login. Use `GET ENV` or `GET SECRET`.
Password for password-based login. Use `GET ENV` or `GET SECRET`.
Offer X.509 certificate user authentication.
Automatically trust connecting client certificates. When false, untrusted client certs are placed in the `rejected` store.
Persist served node values across broker restarts. See [Data persistence & recovery](#data-persistence--recovery).
An application instance certificate is auto-generated (self-signed) on first start under `PKI_DIR/own`. If you change `APPLICATION_URI`, the certificate is regenerated to match.
***
## Tag and Mapping Configuration
Tags reuse the same vocabulary as the [OPC UA client route](./opcua-client), but several settings carry different meaning on the server side.
### Server vs. client semantics
| Setting | Client (`OPCUA`) | Server (`OPCUA_SERVER`) |
| -------------------------------------------- | ----------------------------------------- | ------------------------------------------------------------------------------- |
| `SOURCE_TOPIC` | Topic where read values are **published** | **Bidirectional binding** — MQTT sets the node; client writes publish back here |
| `WRITABLE` | Broker may write the remote device | External clients may write the node |
| `DESTINATION_TOPIC` | Write-command topic | Not used on server routes |
| `ADDRESS` | Required remote NodeId | Optional explicit NodeId; default `s={ROOT_FOLDER}/{TagName}` |
| `EVERY` | Poll interval | Ignored — updates are event-driven from MQTT |
| `QUERY` events | On-demand read/write | Not supported |
| `SCALING` / `OFFSET` / `DEADBAND` | Applied to reads from the device | Not applied inbound — MQTT payload is authoritative |
| `DECIMAL_PLACES` / `MIN_VALUE` / `MAX_VALUE` | Applied to reads | Applied when a client write is published to MQTT |
| `STORE TO` / triggers | Supported on client routes | Not supported |
On server routes, route status **Connected** means the endpoint is **listening**—not that a remote session is established.
### Explicit NodeId
By default, a tag named `Temperature` under route `PlantServer` with `ROOT_FOLDER` defaulting to the route name maps to `ns=2;s=PlantServer/Temperature`. Override with `ADDRESS`:
```lot wrap focus={6,7} theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
DEFINE ROUTE PlantServer WITH TYPE OPCUA_SERVER
ADD OPCUA_SERVER_CONFIG
WITH SERVER_PORT 4840
ADD MAPPING Line1
WITH SOURCE_TOPIC "factory/line1"
ADD TAG Temperature
WITH ADDRESS "ns=2;s=Custom/Line1/Temp"
WITH DATA_TYPE "FLOAT"
```
### Writable tag parameters
For writable tags, configure outbound formatting the same way as on the client route:
Allow OPC UA clients to write this node. Writes publish to the tag's `SOURCE_TOPIC`.
Decimal places applied to `FLOAT`/`DOUBLE` values when a client write is published to MQTT.
Minimum value after a client write. Values below this are clamped.
Maximum value after a client write. Values above this are clamped.
Per-tag override of the route persistence default. Precedence: tag → mapping → route. See [Data persistence & recovery](#data-persistence--recovery).
### Supported data types
Use the same `DATA_TYPE` values as the [OPC UA client route](./opcua-client)—for example `BOOLEAN`, `INT32`, `FLOAT`, `DOUBLE`, and `STRING`. Tags can also expose one-dimensional **arrays** or **struct objects**—see [Array and struct nodes](#array--struct-nodes). For statically defined scalar tags, set `DATA_TYPE` explicitly. Dynamic nodes infer type from the first MQTT payload (see below).
**String sizing:** `STRING` nodes are natively variable-length in OPC UA. The `OPCUA_SERVER` route does not accept `LENGTH` or `STRING_SIZE`—there is no string-size parameter for scalar string tags. For array tags, `ARRAY_SIZE` sets the **node dimension** only (how many elements the array node exposes), not a string buffer width.
***
## Dynamic Topic Handling
Wildcard mappings let you expose many MQTT topics without listing each one as an `ADD TAG`. When a topic matches the pattern for the first time, the route creates a new OPC UA node automatically.
### How it works
1. Define a mapping with a wildcard `SOURCE_TOPIC`—for example `sensors/+/value` or `plant/#`.
2. Do **not** add `ADD TAG` entries under that mapping. The wildcard pattern alone drives node creation.
3. On the **first publish** to a matching topic, the route creates an OPC UA node and sets its value from the payload.
4. Later publishes to the same topic update that node. High-frequency updates use an equality check so unchanged values do not spam change notifications.
Dynamic nodes are **read-only**. OPC UA clients cannot write them back to MQTT. Use static `ADD TAG` definitions with `WRITABLE true` when you need bidirectional control.
### Topic → node mapping
The NodeId string identifier mirrors the full MQTT topic path:
| MQTT topic (first publish) | OPC UA NodeId |
| -------------------------- | ------------------------------ |
| `sensors/press1/value` | `ns=2;s=sensors/press1/value` |
| `sensors/tank2/value` | `ns=2;s=sensors/tank2/value` |
| `plant/line3/motor/rpm` | `ns=2;s=plant/line3/motor/rpm` |
Under `Objects`, folder nodes are created from topic segments so clients can browse a hierarchy that matches your MQTT layout—for example `sensors` → `press1` → `value`.
### Supported wildcards
| Pattern | Matches | Example |
| ------------------ | -------------------------------- | ----------------------------------------------------------------------------------- |
| `+` (single level) | Exactly one topic segment | `sensors/+/value` matches `sensors/press1/value` but not `sensors/line1/temp/value` |
| `#` (multi-level) | Zero or more segments at the end | `plant/#` matches `plant/line1/temp` and `plant/line1/motor/rpm` |
Combine static and dynamic mappings in the same route. Static `ADD TAG` entries under a non-wildcard `SOURCE_TOPIC` behave as fixed nodes; wildcard mappings handle everything else that matches.
### Type inference
Dynamic nodes have no `DATA_TYPE` in the route definition. The broker infers the OPC UA type from the **first** payload and locks it for the lifetime of that node:
| First MQTT payload | Inferred OPC UA type |
| ------------------------------------ | -------------------- |
| Numeric (`42`, `23.5`) | `Double` |
| `true` or `false` (case-insensitive) | `Boolean` |
| Anything else | `String` |
If a later publish cannot be converted to the pinned type, the node keeps its previous value and the broker logs an error. Publish a representative value first—do not send a placeholder string if you expect numeric updates later.
### Example
This mapping exposes every `sensors//value` topic as it appears on the bus:
```lot wrap focus={4-5} theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
DEFINE ROUTE SensorGateway WITH TYPE OPCUA_SERVER
ADD OPCUA_SERVER_CONFIG
WITH SERVER_PORT 4840
ADD MAPPING Sensors
WITH SOURCE_TOPIC "sensors/+/value"
```
**Sequence:**
1. Route starts; no dynamic nodes exist yet.
2. Device publishes `45.2` to `sensors/press1/value` → node `ns=2;s=sensors/press1/value` appears with value `45.2` (`Double`).
3. Device publishes `false` to `sensors/alarm1/value` → node `ns=2;s=sensors/alarm1/value` appears with value `false` (`Boolean`).
4. UaExpert refresh shows both nodes under the `sensors` folder. Further publishes update values in place.
Retained messages already on the broker when the route starts are seeded the same way as for static tags—matching topics create nodes at startup.
### Dynamic node limits
Dynamic nodes are not removed while the route is running. They persist until the route is removed or the broker restarts. Plan wildcard patterns carefully in high-churn environments where topic names change frequently.
***
## Array & Struct Nodes
Scalar tags expose one value per node. Use an **array** tag when MQTT carries a list of values on one topic, or a **struct** tag when the payload is a JSON object with named fields. OPC UA clients then browse and write the same shape—no extra config block beyond the tag settings below.
### Array tags
An array tag turns a JSON list on MQTT into a single OPC UA array node. Publish a list; the client sees one node with multiple elements. With `WRITABLE true`, a client edit publishes the full list back to the same topic, retained.
Set to `"ARRAY"` for list values.
Type of each element: `BOOL`, `INT16`, `UINT16`, `INT32`, `UINT32`, `INT64`, `UINT64`, `FLOAT`, `DOUBLE`, or `STRING`.
Number of elements in the array. Set this to match your payload length. If omitted, the dimension is unspecified.
Define the tag, then publish a JSON array to `{SOURCE_TOPIC}/{TagName}`:
```lot wrap focus={4-10} theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
DEFINE ROUTE PlantServer WITH TYPE OPCUA_SERVER
ADD OPCUA_SERVER_CONFIG
WITH SERVER_PORT 4840
ADD MAPPING Line1
WITH SOURCE_TOPIC "factory/line1"
ADD TAG Levels
WITH DATA_TYPE "ARRAY"
WITH ARRAY_ELEMENT_TYPE "DOUBLE"
WITH ARRAY_SIZE 3
WITH WRITABLE true
```
**What happens:**
* Publish `[1.5, 2.5, 3.5]` to `factory/line1/Levels` → node `ns=2;s=PlantServer/Levels` shows three values.
* Payloads wrapped as `{"value": [1.5, 2.5, 3.5]}` are accepted—the broker unwraps the list automatically.
* A client write to the array node publishes the updated list back to `factory/line1/Levels`.
### Struct tags
A struct tag maps a JSON object to a folder-like node with one child per field. Field names in MQTT must match the names you declare in `STRUCT_FIELD_DEFINITIONS`.
Set to `"STRUCT"` for object payloads.
Comma-separated field list in `name:type` form—for example `bRunning:BOOL,iRpm:INT,sState:STRING`. If you reuse a definition from an ADS or EtherNet/IP route (`name:offset:type:size`), the server uses the name and type only; offset and size are ignored.
```lot wrap focus={4-9} theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
DEFINE ROUTE PlantServer WITH TYPE OPCUA_SERVER
ADD OPCUA_SERVER_CONFIG
WITH SERVER_PORT 4840
ADD MAPPING Line1
WITH SOURCE_TOPIC "factory/line1"
ADD TAG Motor
WITH DATA_TYPE "STRUCT"
WITH WRITABLE true
WITH STRUCT_FIELD_DEFINITIONS "bRunning:BOOL,iRpm:INT,sState:STRING"
```
**What happens:**
* Publish `{"bRunning": true, "iRpm": 1450, "sState": "RUN"}` to `factory/line1/Motor` → clients see `Motor.bRunning`, `Motor.iRpm`, and `Motor.sState` under the struct node.
* With `WRITABLE true`, a client writing one field to OPC-UA (for example `1600` to `Motor.iRpm`) merges into the current struct and publishes the **full** object back to MQTT `factory/line1/Motor`—for example `{"bRunning": true, "iRpm": 1600, "sState": "RUN"}`.
* Fields that were never set by MQTT and were not written by the client are left out of that publish.
`DECIMAL_PLACES`, `MIN_VALUE`, and `MAX_VALUE` apply to scalar writable tags only—not to individual struct fields.
***
## Security Examples
For lab or trusted networks where encryption is not required:
```lot wrap focus={3-4} theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
DEFINE ROUTE DevServer WITH TYPE OPCUA_SERVER
ADD OPCUA_SERVER_CONFIG
WITH SERVER_PORT 4840
WITH ALLOW_ANONYMOUS true
ADD MAPPING Demo
WITH SOURCE_TOPIC "demo/tags"
ADD TAG Value
WITH DATA_TYPE "FLOAT"
```
Require credentials and encrypt traffic:
```lot wrap focus={4-9} theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
DEFINE ROUTE ProdServer WITH TYPE OPCUA_SERVER
ADD OPCUA_SERVER_CONFIG
WITH SERVER_PORT 4840
WITH USE_SECURITY true
WITH SECURITY_POLICY 3
WITH MESSAGE_SECURITY 2
WITH ALLOW_ANONYMOUS false
WITH USERNAME GET ENV "OPCUA_USER"
WITH PASSWORD GET SECRET "OPCUA_PASSWORD"
ADD MAPPING Tags
WITH SOURCE_TOPIC "plant/tags"
ADD TAG Setpoint
WITH DATA_TYPE "FLOAT"
WITH WRITABLE true
```
Accept X.509 certificates for client login:
```lot wrap focus={4-9} theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
DEFINE ROUTE CertServer WITH TYPE OPCUA_SERVER
ADD OPCUA_SERVER_CONFIG
WITH SERVER_PORT 4840
WITH USE_SECURITY true
WITH SECURITY_POLICY 3
WITH MESSAGE_SECURITY 2
WITH ALLOW_ANONYMOUS false
WITH ALLOW_CERTIFICATE_AUTH true
WITH AUTO_ACCEPT_UNTRUSTED_CERTIFICATES false
ADD MAPPING Tags
WITH SOURCE_TOPIC "plant/tags"
ADD TAG Status
WITH DATA_TYPE "INT32"
```
***
## Certificate Management
A secured OPC UA server connection requires **two certificates that trust each other**: the broker's **application instance certificate** (identifies and secures the server) and each **client's certificate** (must be in the server's `trusted` store when using certificate authentication). For client-side trust workflows when the broker dials out to remote servers, see [OPC UA Client](./opcua-client#certificate-management).
### PKI directory layout
Both client and server routes store certificates in four folders under `PKI_DIR`:
| Folder | Holds | File form |
| ---------- | ----------------------------------------------------------------------- | ----------------- |
| `own` | This server's certificate (`own/certs`) and private key (`own/private`) | `*.der` + `*.pfx` |
| `issuer` | Trusted CA / issuer certificates for chain validation | `*.der` |
| `trusted` | Trusted client certificates (`trusted/certs`) | `*.der` |
| `rejected` | Client certificates seen during a handshake but not yet trusted | `*.der` |
### Where certificates live
When `PKI_DIR` is not set, it defaults to `%LocalApplicationData%/OPC Foundation/pki`:
| OS | Default path |
| ------- | -------------------------------------------------------------------------- |
| macOS | `~/Library/Application Support/OPC Foundation/pki` |
| Linux | `$XDG_DATA_HOME/OPC Foundation/pki` or `~/.local/share/OPC Foundation/pki` |
| Windows | `%LOCALAPPDATA%\OPC Foundation\pki` |
Relocate the store to a path you control and mount it as a volume in Docker:
```lot wrap focus={4} theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
ADD OPCUA_SERVER_CONFIG
WITH SERVER_PORT 4840
WITH PKI_DIR "/data/opcua/pki"
```
In Docker, the default PKI location is ephemeral—the broker regenerates its certificate on every restart and clients that trusted the previous one will reject the new one. **Always set `PKI_DIR` to a mounted volume.**
### Inspecting and exporting certificates
```bash wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
# The server's own certificate and private key
ls "$PKI_DIR"/own/certs/*.der
ls "$PKI_DIR"/own/private/*.pfx
# Inspect subject, validity, and application URI SAN
openssl x509 -inform der -in "$PKI_DIR"/own/certs/.der -noout -subject -dates -ext subjectAltName
# Export public cert for clients to trust
openssl x509 -inform der -in "$PKI_DIR"/own/certs/.der -out server-public.pem
```
Give clients the **public** certificate (`own/certs/*.der`). The private key never leaves the broker.
### The server's own certificate
On first start the broker auto-generates a self-signed server certificate (`CN={APPLICATION_NAME}, O=Coreflux`; default `APPLICATION_NAME` is `CorefluxBroker_OpcUaServer`) in `PKI_DIR/own`. Its SAN URI matches `APPLICATION_URI` (default `urn:{host}:Coreflux:{route}`):
```lot wrap focus={4-8} theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
DEFINE ROUTE PlantServer WITH TYPE OPCUA_SERVER
ADD OPCUA_SERVER_CONFIG
WITH SERVER_PORT 4840
WITH APPLICATION_NAME "AcmePlantServer"
WITH APPLICATION_URI "urn:acme:opcua:plant-server"
WITH SECURITY_POLICY 3
WITH MESSAGE_SECURITY 2
WITH PKI_DIR "/data/opcua/pki"
```
* **Keep `APPLICATION_URI` and the certificate SAN in sync.** Strict clients refuse the handshake on a mismatch; changing `APPLICATION_URI` regenerates the certificate to match.
* Clients trust this server by adding `PKI_DIR/own/certs/.der` to their trusted list—the same workflow as [trusting a server certificate on the client route](./opcua-client#trust-the-servers-certificate).
### Enable certificate user authentication
Offer the X.509 user token so clients authenticate with a certificate instead of—or alongside—username and password:
```lot wrap focus={6-7} theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
DEFINE ROUTE CertServer WITH TYPE OPCUA_SERVER
ADD OPCUA_SERVER_CONFIG
WITH SERVER_PORT 4840
WITH USE_SECURITY true
WITH ALLOW_ANONYMOUS false
WITH ALLOW_CERTIFICATE_AUTH true
```
When `ALLOW_CERTIFICATE_AUTH true`, the application instance certificate doubles as the user identity—the client's certificate must be trusted and must carry a valid application URI SAN.
### Trust connecting client certificates
When a client connects with certificate authentication, its certificate must be in the server's trusted store:
The client's certificate lands in `PKI_DIR/rejected/certs`.
```bash wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
mv "$PKI_DIR"/rejected/certs/.der "$PKI_DIR"/trusted/certs/
```
The client connects successfully.
For testing only, set `WITH AUTO_ACCEPT_UNTRUSTED_CERTIFICATES true` to auto-trust every connecting client certificate.
### Supply your own server certificate
The server route has no `CLIENT_CERT_PATH`-style parameter. To run with a CA-issued certificate instead of the auto-generated one, place it in the `own` store **before first start**, with a subject matching `CN={APPLICATION_NAME}, O=Coreflux` and a SAN URI matching `APPLICATION_URI`:
```bash wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
mkdir -p "$PKI_DIR"/own/certs "$PKI_DIR"/own/private
openssl x509 -in server-cert.pem -outform der -out "$PKI_DIR"/own/certs/server.der
openssl pkcs12 -export -inkey server-key.pem -in server-cert.pem \
-out "$PKI_DIR"/own/private/server.pfx -passout pass:
```
If a valid matching certificate is already present, the broker uses it instead of generating one.
### Rotation and renewal
* **Auto-generated certificates** — Delete files in `PKI_DIR/own/certs` and `PKI_DIR/own/private`, then restart the route; the broker regenerates.
* **Changing `APPLICATION_URI`** — Regenerates the certificate; re-trust it on every client.
* Plan rotation **before expiry**—an expired application instance certificate fails the handshake on both ends.
***
## Data Persistence & Recovery
By default a server route holds node values **in memory only**—a broker restart resets them (nodes return to `BadWaitingForInitialData` until the next publish). Set `WITH PERSISTENCE true` on `OPCUA_SERVER_CONFIG` to snapshot served values and restore them on startup.
```lot wrap focus={3-12} theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
DEFINE ROUTE PlantServer WITH TYPE OPCUA_SERVER
ADD OPCUA_SERVER_CONFIG
WITH SERVER_PORT 4840
WITH PERSISTENCE true
ADD MAPPING Nodes
WITH SOURCE_TOPIC "plant/line1"
ADD TAG Setpoint
WITH DATA_TYPE "FLOAT"
WITH WRITABLE true
ADD TAG Temperature
WITH DATA_TYPE "FLOAT"
```
The same **ownership-aware policy** as the [Modbus TCP Server](./modbus-tcp-server#data-persistence--recovery) route applies:
| Node | Owner | Default | On restart |
| -------------------- | ------------------------------------------------ | ----------------- | ---------------------------------------------------------------------------------------------------------------- |
| `WRITABLE true` | Client-written setpoint lives only in the server | **Persisted** | Restored **authoritatively**—wins over retained MQTT; returns a good value instead of `BadWaitingForInitialData` |
| Read-only / MQTT-fed | Fed from live MQTT | **Not persisted** | Recovered from **live MQTT** (retained message / next publish) |
Client-written setpoints survive a restart; live telemetry always reflects the current MQTT source.
### Per-tag and mapping overrides
`WITH PERSISTENCE true|false` on an individual `ADD TAG`—or a whole `ADD MAPPING`—overrides the route default:
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
DEFINE ROUTE OverrideServer WITH TYPE OPCUA_SERVER
ADD OPCUA_SERVER_CONFIG
WITH SERVER_PORT 4840
WITH PERSISTENCE true
ADD MAPPING Control
WITH SOURCE_TOPIC "plant/line1"
ADD TAG Setpoint
WITH DATA_TYPE "FLOAT"
WITH WRITABLE true
ADD TAG LastGoodTemp
WITH DATA_TYPE "FLOAT"
WITH PERSISTENCE "true"
ADD MAPPING Scratch
WITH SOURCE_TOPIC "plant/scratch"
WITH PERSISTENCE "false"
ADD TAG TestpointA
WITH DATA_TYPE "FLOAT"
WITH WRITABLE true
ADD TAG TestpointB
WITH DATA_TYPE "INT32"
WITH WRITABLE true
```
* `Setpoint` — restored authoritatively to the last client-written value.
* `LastGoodTemp` — served from snapshot until `plant/line1/LastGoodTemp` next publishes.
* `Scratch` mapping tags — never snapshotted; start cold on every restart.
### Caching slow or infrequently-updated sources
MQTT-fed nodes are not persisted by default. When a source updates infrequently—an hourly energy total, a shift-latched value—force-persist with `WITH PERSISTENCE "true"` so the last received value is re-served on cold start until the next live publish overrides it.
```lot wrap focus={6-12} theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
DEFINE ROUTE MeterServer WITH TYPE OPCUA_SERVER
ADD OPCUA_SERVER_CONFIG
WITH SERVER_PORT 4840
WITH PERSISTENCE true
ADD MAPPING Meters
WITH SOURCE_TOPIC "plant/energy"
ADD TAG TotalEnergy
WITH DATA_TYPE "DOUBLE"
WITH PERSISTENCE "true"
ADD TAG DailyPeak
WITH DATA_TYPE "DOUBLE"
WITH PERSISTENCE "true"
```
After a restart, a client browsing `TotalEnergy` or `DailyPeak` reads the last published value—a good value rather than `BadWaitingForInitialData`—until the next publish refreshes it.
Persistence applies only to **explicitly-declared** `ADD TAG` nodes. Wildcard/dynamic nodes created from MQTT topic patterns are not snapshotted—a slow source behind a wildcard mapping still starts cold.
### Storage & durability
* Values are written through a non-blocking background worker on every change (rapid updates coalesce to the latest value).
* The snapshot lives in an encrypted-at-rest database under the broker data directory.
* A snapshot is keyed to the route's tag definitions by a fingerprint. If addresses or types change between runs, the stale snapshot is discarded rather than replayed into reshaped nodes.
***
## Verify Your Server
Add the route definition to your broker and confirm route status shows **Connected** (the endpoint is listening).
Use UaExpert or any OPC UA browser. Connect to `opc.tcp://:` (append `ENDPOINT_PATH` if configured). When using security, trust the server certificate—export it from `PKI_DIR/own/certs` or follow the trust workflow in [Certificate Management](#certificate-management).
Publish a test value to a bound topic—for example `factory/line1/Temperature`. Refresh the client and confirm the node value updates.
For a writable tag, write a value from the OPC UA client. Subscribe to the tag's `SOURCE_TOPIC` with MQTT Explorer or any MQTT client and confirm the retained publish arrives.
***
## Limitations
Each `OPCUA_SERVER` route binds exclusively to its `SERVER_PORT`. A second route on the same port fails to start and reports a red route status.
***
## Troubleshooting
* **Port in use** — Another process or server route already uses `SERVER_PORT`. Choose a different port or stop the conflicting service.
* **PKI directory not writable** — Ensure the broker can write to `PKI_DIR` (or the default PKI location).
The application instance certificate regenerates when `PKI_DIR` is ephemeral—for example, an unmounted Docker volume. Mount `PKI_DIR` as a persistent volume so client trust stores remain valid across restarts.
* **Client cert not trusted** — Move it from `PKI_DIR/rejected/certs` to `PKI_DIR/trusted/certs`, or set `AUTO_ACCEPT_UNTRUSTED_CERTIFICATES true` for testing only
* **Client cert has no application URI SAN** — The connecting client must present a certificate with a valid URI in Subject Alternative Name
`APPLICATION_URI` must match the certificate SAN URI. Leave both at their defaults, or change them together so they stay in sync.
* Confirm the tag has `WITH WRITABLE true`.
* Subscribe to the tag's `SOURCE_TOPIC` and check for the retained publish after the write.
* Verify `MIN_VALUE` / `MAX_VALUE` are not clamping the written value away from what you expect.
The broker logs client activity for this route in the broker log:
* **Sessions** — client session create and close events.
* **Accepted external writes** — writes accepted on tags and on individual STRUCT fields.
Use these log lines to confirm which clients connected and which node writes reached MQTT.
`ALLOW_ANONYMOUS` is `false`. Connect with the configured username and password, or use certificate authentication if enabled.
The `SOURCE_TOPIC` has not published yet (or no retained value). Publish the topic, enable route persistence for client-written setpoints, or force-persist a slow MQTT-fed source with `WITH PERSISTENCE "true"`. See [Data persistence & recovery](#data-persistence--recovery).
***
## Best Practices
Mount `PKI_DIR` as a persistent volume so server certificates survive broker restarts. Ephemeral storage regenerates the application instance certificate on every boot and breaks client trust stores. See [Certificate Management](#certificate-management).
Set `ALLOW_ANONYMOUS false` on plant networks and require username/password or certificate authentication. Anonymous endpoints are appropriate for lab or isolated development only.
Each server route binds exclusively to its `SERVER_PORT`. Document which route owns which port to avoid startup conflicts when adding routes.
Prefer `GET ENV` and `GET SECRET` over literals in route definitions. Never commit passwords or client credentials to version control.
Align `SECURITY_POLICY` and `MESSAGE_SECURITY` with what your SCADA or HMI supports. If the client cannot negotiate encryption, enable `ALLOW_NO_SECURITY` for testing only—not in production.
Set `WITH PERSISTENCE true` on `OPCUA_SERVER_CONFIG` so client-written values survive a broker restart instead of returning `BadWaitingForInitialData`. See [Data persistence & recovery](#data-persistence--recovery).
***
## Next Steps
Connect to remote OPC UA servers and poll device data into MQTT.
Compare protocols and choose the right industrial route for your architecture.
# Industrial Routes Overview
Source: https://docs.coreflux.org/latest/lot-language/routes/industrial/overview
Connect your Coreflux broker to PLCs, industrial controllers, and automation systems using industrial protocol routes
## Why Industrial Routes?
Bridge the gap between your PLCs and MQTT without custom middleware. Read sensor data, write setpoints, and integrate industrial equipment into your IoT pipeline—using a single, consistent configuration pattern across all supported protocols.
## Industrial Protocol Integration
Industrial routes enable direct communication between your Coreflux MQTT broker and industrial automation equipment. Read data from PLCs, write setpoints, and bridge the gap between IT and OT systems - all through standardized LoT (Language of Things) syntax.
**Like a translator at an international meeting.** Industrial routes speak both PLC (Modbus, S7, OPC UA) and MQTT—so your broker can read sensor data and send commands without custom glue code.
## Supported Protocols
Poll PLCs and industrial devices over Modbus TCP.
RS-232/RS-485 serial communication with Modbus RTU devices.
Expose MQTT topics as Modbus registers and coils for SCADA and HMIs.
Native S7 protocol for Siemens S7-200/300/400/1200/1500 PLCs.
Connect to remote OPC UA servers and poll device data into MQTT.
Expose MQTT topics as OPC UA nodes for SCADA and OT clients.
TwinCAT ADS protocol for Beckhoff PLCs with AutoDiscovery support.
ControlLogix, CompactLogix, and other Rockwell Automation PLCs.
CIP-based communication for Allen-Bradley and compatible devices.
Omron FINS protocol for CJ/CS/CP series PLCs.
BACnet/IP for building automation controllers (ASHRAE 135) — HVAC, lighting, and metering.
***
## Protocol Comparison
| Protocol | Transport | Use Case | Vendor |
| -------------------- | --------------- | ---------------------------------------------- | ------------ |
| Modbus TCP Client | TCP/IP | Poll remote Modbus devices | Multi-vendor |
| Modbus Serial Client | RS-232/485 | Legacy devices, long distances | Multi-vendor |
| Modbus TCP Server | TCP/IP | Expose MQTT to Modbus masters | Multi-vendor |
| Siemens S7 | TCP/IP | Siemens automation systems | Siemens |
| OPC UA Client | TCP/IP | Read from remote OPC UA servers | Multi-vendor |
| OPC UA Server | TCP/IP | Expose MQTT to SCADA/HMI clients | Multi-vendor |
| ADS | TCP/IP | Beckhoff TwinCAT systems | Beckhoff |
| Allen-Bradley | TCP/IP | Rockwell Automation | Rockwell |
| EtherNet/IP | TCP/IP | CIP devices, explicit messaging | Multi-vendor |
| FINS | TCP/UDP | Omron PLCs | Omron |
| BACnet | UDP (BACnet/IP) | Building automation (HVAC, lighting, metering) | Multi-vendor |
***
## Common Architecture
All industrial routes share a common architecture with three key components:
```mermaid theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
graph LR
subgraph broker [Coreflux Broker]
Route[Industrial Route]
Tags[TAG Definitions]
Mapping[MAPPING Config]
end
subgraph plc [PLC / Controller]
Registers[Registers/Variables]
end
subgraph mqtt [MQTT Topics]
Pub[Published Data]
Sub[Write Commands]
end
Route --> Tags
Tags --> Mapping
Mapping -->|Poll| Registers
Registers -->|Read| Pub
Sub -->|Write| Registers
```
### 1. Connection Configuration
Each protocol has specific connection parameters:
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
ADD _CONFIG
WITH HOST "192.168.1.100"
WITH PORT 502
// Protocol-specific options...
```
### 2. MAPPING with Polling
**MAPPING Definition**
A **MAPPING** is a scheduled, continuous polling operation that automatically reads data from industrial devices (PLCs, Modbus devices, etc.) at regular intervals and publishes the values to MQTT topics.
* **Polls:** industrial device at regular intervals (`EVERY`)
* **Reads:** multiple data points (TAGs) in a single operation
* **Publishes:** each TAG value to its own MQTT topic
In LoT, you define a MAPPING like this:
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
ADD MAPPING SensorGroup
WITH EVERY 500 MILLISECONDS
ADD TAG Temperature
// TAG config...
ADD TAG Pressure
// TAG config...
```
#### Common MAPPING-level fields
These fields are available at the MAPPING level across all industrial protocols:
Base MQTT topic for publishing tag data. Each TAG publishes to `{SOURCE_TOPIC}/{TAG_NAME}`.
Polling interval (e.g. `500 MILLISECONDS`, `1 SECOND`).
***
### 3. TAG Definitions
**TAG Definition**
A **TAG** is an individual data point within a MAPPING that represents a specific variable or register in an industrial device.
* **Identifies:** a specific memory address in the device
* **Transforms:** raw value using scaling, offset, and engineering units
* **Publishes:** to a dedicated MQTT topic
* **Monitors:** changes using deadband filtering
- **Data transformation:** Scaling, offset, min/max validation
- **Change detection:** Deadband to filter noise
- **Formatting:** Publish as JSON or raw value
In LoT, you define a TAG like this:
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
ADD TAG Temperature
WITH ADDRESS "100"
WITH DATA_TYPE "FLOAT"
WITH SOURCE_TOPIC "plc/temperature"
WITH SCALING 0.1
WITH UNIT "°C"
```
#### Common TAG Parameters
These parameters are available across all industrial protocols:
Protocol-specific address (register number, variable path, NodeId, etc.).
Address type varies by protocol (e.g., HOLDING\_REGISTER, DATABLOCK, SYMBOL).
Data type for the value. Common types: `BOOL`, `INT16`, `UINT16`, `INT32`, `UINT32`, `FLOAT`, `DOUBLE`, `STRING`.
Maximum string size when `DATA_TYPE` is `STRING` on routes that support configurable sizing. Units and defaults vary—see [String sizing by Route](#string-sizing-by-route). Preferred keyword; `STRING_SIZE` is accepted as an alias where sizing is supported.
Topic where PLC values are published. Subscribe here to receive sensor data.
Topic to send write commands. Publish a value here to write it to the PLC (requires WRITABLE true).
Multiplier applied to raw value. Formula: `published_value = (raw_value * SCALING) + OFFSET`. Default: 1.0.
Value added after scaling. Default: 0.0.
Number of decimal places in published value. Default: 2.
Minimum allowed value. Values below this are filtered out.
Maximum allowed value. Values above this are filtered out.
Minimum change required to publish a new value. Reduces network traffic for slowly changing values. Default: 0.0.
Output format: `VALUE_ONLY` (just the value) or `JSON` (structured object with metadata). Default: VALUE\_ONLY.
Engineering unit for documentation and JSON output (e.g., °C, bar, RPM).
Human-readable description of the TAG.
Allow writing to this TAG via DESTINATION\_TOPIC. Default: false.
Byte order for multi-byte values: `BIGENDIAN` or `LITTLEENDIAN`. Default: BIGENDIAN.
Word order for 32-bit values: `BIGENDIAN` or `LITTLEENDIAN`. Default: BIGENDIAN.
### String sizing by Route
When `DATA_TYPE` is `STRING`, routes that support configurable sizing accept `WITH LENGTH` to declare how many bytes the tag occupies on the wire. Use `LENGTH` as the preferred keyword; `STRING_SIZE` is accepted as an alias where sizing is supported. OPC UA Server strings are natively variable-length (no sizing parameter); Allen-Bradley uses a fixed 88-byte CIP layout—`LENGTH`/`STRING_SIZE` do not resize it.
| Route | Unit | Default | Constraint |
| -------------- | -------------------------------- | --------------------------- | ------------------------------- |
| Modbus Server | bytes, 2 bytes/register | 40 bytes | at most 246 bytes/123 registers |
| Modbus clients | bytes | 256 | consecutive registers |
| Siemens S7 | bytes including 2-byte header | 256 | — |
| EtherNet/IP | bytes | type-driven; 1 in join mode | separate join/array semantics |
| OPC UA client | bytes | 256 | — |
| OPC UA Server | natively variable | unbounded | no sizing parameter |
| ADS | bytes including length byte | read 256; write 80 | — |
| Allen-Bradley | fixed layout | 88-byte CIP STRING | sizing keywords do not resize |
| FINS | payload bytes plus 2-byte header | 256 | — |
| BACnet | bytes | 256 | `MAX_APDU` is unrelated |
See the protocol-specific route guide for register math, validation limits, and complete examples.
***
### 4. SOURCE\_TOPIC Configuration
There are two ways to configure where TAG values are published. Use group-level when you want consistent topic structure across all TAGs in a MAPPING; use individual TAG when you need custom paths per sensor (e.g., different namespaces for temperature vs pressure).
Define `SOURCE_TOPIC` at the MAPPING level. Each TAG automatically publishes to `{SOURCE_TOPIC}/{TAG_NAME}`.
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
ADD MAPPING ProductionLine
WITH SOURCE_TOPIC "factory/line1"
WITH EVERY 1 SECOND
ADD TAG Temperature
WITH ADDRESS "100"
WITH DATA_TYPE "FLOAT"
ADD TAG Pressure
WITH ADDRESS "102"
WITH DATA_TYPE "FLOAT"
```
**Result:** Temperature publishes to `factory/line1/Temperature`; Pressure publishes to `factory/line1/Pressure`.
Define `SOURCE_TOPIC` at each TAG level for custom topic structure.
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
ADD MAPPING ProductionLine
WITH EVERY 1 SECOND
ADD TAG Temperature
WITH SOURCE_TOPIC "sensors/temp/line1"
WITH ADDRESS "100"
WITH DATA_TYPE "FLOAT"
ADD TAG Pressure
WITH SOURCE_TOPIC "sensors/pressure/line1"
WITH ADDRESS "102"
WITH DATA_TYPE "FLOAT"
```
**Result:** Temperature publishes to `sensors/temp/line1`; Pressure publishes to `sensors/pressure/line1`.
***
### 5. EVENT (On-Demand Operations)
Understanding on-demand operations in routes.
**EVENT Definition**
An **EVENT** is an on-demand operation that waits for messages on an MQTT topic, executes a specific action when triggered, and publishes the results back to another MQTT topic.
Use EVENT when you need on-demand reads or writes (triggered by a message); use MAPPING for continuous polling. Syntax varies by protocol—see the protocol-specific docs (e.g., [Modbus TCP Client](./modbus-tcp-client#event-based-operations)) for details.
In LoT, you define an EVENT like this:
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
ADD EVENT ReadOnDemand
WITH SOURCE_TOPIC "plc/commands/read"
WITH DESTINATION_TOPIC "plc/responses/read"
WITH QUERY ""
```
#### Common EVENT Parameters
These parameters are available across all industrial protocols for EVENT definitions:
MQTT topic where the EVENT listens for trigger messages. Publish a message here to execute the operation.
MQTT topic where the EVENT publishes the result of the operation.
Protocol-specific specification of the operation (read or write) and its parameters. Uses LoT object syntax. See each protocol's documentation for supported operations.
### When to Use MAPPING vs EVENT
Use the right acquisition pattern for the job:
| | MAPPING (Cyclic) | EVENT (On-Demand) |
| --------------- | ---------------------------------------- | ------------------------------------------------ |
| **Trigger** | Automatic, on a timer (`WITH EVERY`) | Manual, via an MQTT message to SOURCE\_TOPIC |
| **Direction** | Reads from device, publishes to MQTT | Reads or writes on command, publishes result |
| **Best for** | Dashboards, alarms, continuous telemetry | Operator commands, diagnostics, recipe downloads |
| **Data volume** | Constant stream at defined interval | Only when requested |
Most real-world routes combine both patterns: MAPPING for continuous monitoring and EVENT for on-demand operations within the same `DEFINE ROUTE`.
***
## Basic Example Pattern
### Cyclic Only
This minimal pattern works across all industrial protocols for continuous data acquisition:
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
DEFINE ROUTE MyPLCConnection WITH TYPE
ADD _CONFIG
WITH HOST "192.168.1.100"
// Connection parameters...
ADD MAPPING CriticalSensors
WITH EVERY 100 MILLISECONDS
ADD TAG Temperature
WITH ADDRESS ""
WITH DATA_TYPE "FLOAT"
WITH SOURCE_TOPIC "plc/sensors/temperature"
ADD TAG Pressure
WITH ADDRESS ""
WITH DATA_TYPE "FLOAT"
WITH SOURCE_TOPIC "plc/sensors/pressure"
ADD MAPPING SlowChangingData
WITH EVERY 5 SECONDS
ADD TAG ProductCount
WITH ADDRESS ""
WITH DATA_TYPE "INT32"
WITH SOURCE_TOPIC "plc/production/count"
```
### Combined (Cyclic + On-Demand)
A production route typically combines MAPPING for continuous monitoring with EVENT for on-demand operations. Publish a message to the EVENT's SOURCE\_TOPIC to trigger a one-shot read or write, and receive the result on the DESTINATION\_TOPIC:
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
DEFINE ROUTE MyPLCConnection WITH TYPE
ADD _CONFIG
WITH HOST "192.168.1.100"
// Connection parameters...
// Continuous monitoring — polls every 500ms
ADD MAPPING ProcessSensors
WITH EVERY 500 MILLISECONDS
ADD TAG Temperature
WITH ADDRESS ""
WITH DATA_TYPE "FLOAT"
WITH SOURCE_TOPIC "plc/sensors/temperature"
ADD TAG Pressure
WITH ADDRESS ""
WITH DATA_TYPE "FLOAT"
WITH SOURCE_TOPIC "plc/sensors/pressure"
// On-demand read — triggered by publishing to SOURCE_TOPIC
ADD EVENT ReadRegister
WITH SOURCE_TOPIC "plc/commands/read"
WITH DESTINATION_TOPIC "plc/responses/read"
WITH QUERY "{operation: READ, address: , data_type: FLOAT}"
// On-demand write — triggered by publishing to SOURCE_TOPIC
ADD EVENT WriteSetpoint
WITH SOURCE_TOPIC "plc/commands/write"
WITH DESTINATION_TOPIC "plc/responses/write"
WITH QUERY "{operation: WRITE, address: , value: 0}"
```
***
## Advanced Example Pattern
When you need engineering-unit conversion, noise filtering, range validation, or bidirectional control, add transformation and publishing parameters to your TAGs. This pattern builds on the basic structure above to unlock the full power of industrial routes.
### Scaled and Filtered Monitoring
Apply scaling and offset to convert raw PLC register values into meaningful engineering units, and use deadband to suppress noise:
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
DEFINE ROUTE AdvancedPLCConnection WITH TYPE
ADD _CONFIG
WITH HOST "192.168.1.100"
// Connection parameters...
ADD MAPPING ProcessMonitoring
WITH EVERY 500 MILLISECONDS
ADD TAG Temperature
WITH ADDRESS ""
WITH DATA_TYPE "FLOAT"
WITH SOURCE_TOPIC "plc/sensors/temperature"
// Value transformation — raw 245 becomes (245 * 0.1) + (-40) = -15.5
WITH SCALING 0.1
WITH OFFSET -40
WITH UNIT "°C"
// Noise filtering — only publish when change exceeds 0.5
WITH DEADBAND 0.5
// Range validation — discard values outside physical limits
WITH MIN_VALUE -20
WITH MAX_VALUE 150
// Precision — publish with 1 decimal place
WITH DECIMAL_PLACES 1
// Structured output — includes unit, description, and timestamp
WITH PUBLISH_MODE "JSON"
ADD TAG Pressure
WITH ADDRESS ""
WITH DATA_TYPE "FLOAT"
WITH SOURCE_TOPIC "plc/sensors/pressure"
WITH SCALING 0.01
WITH UNIT "bar"
WITH DEADBAND 0.1
```
### Writable Setpoints with Validation
Enable bidirectional control by marking TAGs as writable. Published values on the DESTINATION\_TOPIC are written to the PLC, with MIN/MAX enforcing safe limits:
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
ADD MAPPING Setpoints
WITH EVERY 1 SECOND
ADD TAG TemperatureSetpoint
WITH ADDRESS ""
WITH DATA_TYPE "FLOAT"
WITH SOURCE_TOPIC "plc/setpoints/temperature"
// Bidirectional — publish to DESTINATION_TOPIC to write a value
WITH WRITABLE "true"
WITH DESTINATION_TOPIC "plc/setpoints/temperature/write"
WITH UNIT "°C"
// Safety limits — reject values outside the safe operating range
WITH MIN_VALUE 0
WITH MAX_VALUE 100
```
***
## Best Practices
Group TAGs by update frequency:
* **Fast (50-100ms)**: Safety signals, real-time control
* **Medium (500ms-1s)**: Process values, sensor readings
* **Slow (5-30s)**: Configuration, counters, status
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
ADD MAPPING FastLoop
WITH EVERY 100 MILLISECONDS
// Critical TAGs only
ADD MAPPING SlowLoop
WITH EVERY 5 SECONDS
// Non-critical TAGs
```
Reduce network traffic for slowly changing values:
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
ADD TAG Temperature
WITH DEADBAND 0.5 // Only publish if change > 0.5
```
Transform raw PLC values to engineering units in the route:
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
ADD TAG Temperature
WITH SCALING 0.1 // Raw 245 becomes 24.5
WITH OFFSET -40 // Apply offset if needed
WITH UNIT "°C"
```
Include metadata in published messages:
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
ADD TAG Temperature
WITH PUBLISH_MODE "JSON"
WITH UNIT "°C"
WITH DESCRIPTION "Main reactor temperature"
```
Output:
```json wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
{
"value": 24.5,
"unit": "°C",
"description": "Main reactor temperature",
"timestamp": "2024-01-15T10:30:00Z"
}
```
Only enable WRITABLE for TAGs that need it, and use specific topics:
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
ADD TAG Setpoint
WITH WRITABLE "true"
WITH DESTINATION_TOPIC "plc/setpoints/temperature/write"
WITH MIN_VALUE 0
WITH MAX_VALUE 100
```
***
## Next Steps
Choose the protocol that matches your equipment:
Start with the most common industrial protocol.
Connect to Siemens PLCs directly.
# Siemens S7 Route
Source: https://docs.coreflux.org/latest/lot-language/routes/industrial/siemens-s7
Connect to Siemens S7-200, S7-300, S7-400, S7-1200, and S7-1500 PLCs using native S7 protocol
## Siemens S7 Overview
The `SIEMENS_S7` route enables direct communication with Siemens S7 PLCs using the native S7 protocol (ISO-on-TCP). It supports all major S7 PLC families and provides access to data blocks, memory areas, inputs, outputs, timers, and counters.
The S7 route uses native protocol communication, which is typically faster and more efficient than OPC UA for Siemens PLCs.
## Basic Syntax
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
DEFINE ROUTE SiemensPLC WITH TYPE SIEMENS_S7
ADD S7_CONFIG
WITH IP "192.168.1.100"
WITH CPU_TYPE "S71500"
WITH RACK 0
WITH SLOT 1
ADD MAPPING ProcessData
WITH EVERY 500 MILLISECONDS
ADD TAG Temperature
WITH ADDRESS "DB1.DBD100"
WITH DATA_TYPE "REAL"
WITH SOURCE_TOPIC "plc/temperature"
```
***
## Supported CPU Types
| CPU Type | Parameter Value | Typical Slot | Description |
| -------- | --------------- | ------------ | --------------------- |
| S7-200 | `S7200` | 0 | Compact micro PLCs |
| S7-300 | `S7300` | 2 | Modular PLCs |
| S7-400 | `S7400` | 2/3 | High-performance PLCs |
| S7-1200 | `S71200` | 1 | Compact controllers |
| S7-1500 | `S71500` | 1 | Advanced controllers |
For S7-1200/1500 PLCs, you may need to enable "Permit access with PUT/GET" in the PLC's protection settings (Hardware Configuration → Protection & Security).
***
## Connection Configuration
### S7\_CONFIG Parameters
S7 PLC IP address.
CPU type: `S7200`, `S7300`, `S7400`, `S71200`, or `S71500`.
PLC rack number. Default: 0.
PLC slot number. Default: 1 for S7-1200/1500, 2 for S7-300/400.
Connection type: `PG` (Programming device), `OP` (Operator Panel), or `S7BASIC`. Default: PG.
Number of retry attempts on failure. Default: 3.
Seconds to wait between retries. Default: 5.
Refresh time in milliseconds. Default: 1000.
Read timeout in milliseconds. Default: 5000.
Write timeout in milliseconds. Default: 5000.
### Connection Examples
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
ADD S7_CONFIG
WITH IP "192.168.1.100"
WITH CPU_TYPE "S71500"
WITH RACK 0
WITH SLOT 1
WITH CONNECTION_TYPE "PG"
```
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
ADD S7_CONFIG
WITH IP "192.168.1.50"
WITH CPU_TYPE "S71200"
WITH RACK 0
WITH SLOT 1
```
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
ADD S7_CONFIG
WITH IP "192.168.1.30"
WITH CPU_TYPE "S7300"
WITH RACK 0
WITH SLOT 2
```
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
ADD S7_CONFIG
WITH IP "192.168.1.20"
WITH CPU_TYPE "S7400"
WITH RACK 0
WITH SLOT 3
```
***
## S7 Addressing
### Address Format
S7 addresses follow the pattern: `.` or `.`
| Address | Description | Example |
| ------------ | ------------------------------- | ------------------------------------- |
| `DBn.DBXx.y` | Data Block bit | `DB1.DBX0.0` - DB1, byte 0, bit 0 |
| `DBn.DBBx` | Data Block byte | `DB1.DBB0` - DB1, byte 0 |
| `DBn.DBWx` | Data Block word (16-bit) | `DB1.DBW100` - DB1, word at byte 100 |
| `DBn.DBDx` | Data Block double word (32-bit) | `DB1.DBD100` - DB1, dword at byte 100 |
| `Mx.y` | Memory bit | `M0.0` - Marker byte 0, bit 0 |
| `MBx` | Memory byte | `MB0` - Marker byte 0 |
| `MWx` | Memory word | `MW100` - Marker word at byte 100 |
| `MDx` | Memory double word | `MD100` - Marker dword at byte 100 |
| `Ix.y` | Input bit | `I0.0` - Input byte 0, bit 0 |
| `IBx` | Input byte | `IB0` - Input byte 0 |
| `IWx` | Input word | `IW0` - Input word at byte 0 |
| `Qx.y` | Output bit | `Q0.0` - Output byte 0, bit 0 |
| `QBx` | Output byte | `QB0` - Output byte 0 |
| `QWx` | Output word | `QW0` - Output word at byte 0 |
| `Tx` | Timer | `T0` - Timer 0 |
| `Cx` | Counter | `C0` - Counter 0 |
### Address Types
Memory area type: `DATABLOCK`, `MEMORY`, `INPUT`, `OUTPUT`, `TIMER`, `COUNTER`.
For most configurations, the ADDRESS\_TYPE is inferred from the address format. Use explicit ADDRESS\_TYPE when needed for clarity.
***
## Data Types
| Data Type | S7 Type | Size | Description |
| ------------------------------------ | ------- | -------- | --------------------------------------------------------------------------------- |
| `BOOL` / `BIT` | BOOL | 1 bit | Boolean value |
| `BYTE` | BYTE | 8 bits | Unsigned 8-bit |
| `SINT` | SINT | 8 bits | Signed 8-bit integer |
| `USINT` | USINT | 8 bits | Unsigned 8-bit integer |
| `WORD` | WORD | 16 bits | Unsigned 16-bit |
| `INT` / `INT16` | INT | 16 bits | Signed 16-bit integer |
| `UINT` / `UINT16` | UINT | 16 bits | Unsigned 16-bit integer |
| `DWORD` | DWORD | 32 bits | Unsigned 32-bit |
| `DINT` / `INT32` | DINT | 32 bits | Signed 32-bit integer |
| `UDINT` / `UINT32` | UDINT | 32 bits | Unsigned 32-bit integer |
| `LWORD` | LWORD | 64 bits | Unsigned 64-bit |
| `LINT` / `INT64` | LINT | 64 bits | Signed 64-bit integer |
| `ULINT` / `UINT64` | ULINT | 64 bits | Unsigned 64-bit integer |
| `REAL` / `FLOAT` | REAL | 32 bits | 32-bit floating point |
| `LREAL` / `DOUBLE` | LREAL | 64 bits | 64-bit floating point |
| `STRING` | STRING | Variable | S7 string type; sized with `LENGTH` (default 256 bytes, includes two-byte header) |
| `CHAR` | CHAR | 8 bits | Single character |
| `WCHAR` | WCHAR | 16 bits | Wide character |
| `TIME` | TIME | 32 bits | Time duration |
| `DATE` | DATE | 16 bits | Date value |
| `TOD` | TOD | 32 bits | Time of day |
| `DTL` / `DATETIME` / `DATE_AND_TIME` | DTL | 12 bytes | Date and time combined |
### String sizing
For `STRING` tags, `WITH LENGTH` is the **total allocated buffer in bytes** on the wire—the S7 two-byte maximum-length/current-length header **plus** the maximum character payload. It is **not** the character count alone. Default: **256** bytes. `STRING_SIZE` is accepted as an alias for the same field.
In TIA Portal, a `STRING[n]` variable occupies **n + 2** bytes (two header bytes plus **n** characters of capacity). Set `WITH LENGTH` to that combined total so the route reads the full allocation from the starting byte address—for example, `STRING[254]` occupies 256 bytes, so use `WITH LENGTH 256`.
```lot wrap focus={13} theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
DEFINE ROUTE S7Strings WITH TYPE SIEMENS_S7
ADD S7_CONFIG
WITH IP "192.168.1.100"
WITH CPU_TYPE "S71500"
WITH RACK 0
WITH SLOT 1
ADD MAPPING ProductData
WITH EVERY 1 SECOND
ADD TAG ProductName
WITH ADDRESS "DB1.DBB300"
WITH ADDRESS_TYPE "DATABLOCK"
WITH DATA_TYPE "STRING"
WITH LENGTH 64
WITH SOURCE_TOPIC "plc/product/name"
```
For cross-protocol defaults and caps, see [String sizing by Route](./overview#string-sizing-by-route) on the industrial overview.
***
## TAG Configuration
### Complete TAG Example
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
ADD TAG ProcessTemperature
WITH ADDRESS "DB1.DBD100"
WITH ADDRESS_TYPE "DATABLOCK"
WITH DATA_TYPE "REAL"
WITH SOURCE_TOPIC "plc/process/temperature"
WITH SCALING 1
WITH OFFSET 0
WITH UNIT "°C"
WITH DECIMAL_PLACES 2
WITH MIN_VALUE -50
WITH MAX_VALUE 200
WITH DEADBAND 0.5
WITH PUBLISH_MODE "JSON"
WITH WRITABLE "true"
WITH DESTINATION_TOPIC "plc/process/temperature/set"
WITH BYTE_ORDER "BIGENDIAN"
WITH DESCRIPTION "Main process temperature"
```
### TAG Parameters
S7 address in standard notation (e.g., `DB1.DBW100`, `M0.0`, `I0.0`, `Q0.0`).
Address type: `DATABLOCK`, `MEMORY`, `INPUT`, `OUTPUT`, `TIMER`, `COUNTER`.
S7 data type: `BOOL`, `BYTE`, `WORD`, `DWORD`, `INT`, `DINT`, `REAL`, `STRING`.
**STRING only.** Total string buffer size in bytes, including the two-byte S7 header. Alias: `STRING_SIZE`.
Multiplier applied to raw value. Default: 1.0.
Value added after scaling. Default: 0.0.
Number of decimal places in output. Default: 2.
Minimum allowed value.
Maximum allowed value.
Minimum change to trigger publish. Default: 0.0.
Topic where PLC values are published. Subscribe here to receive sensor data.
Output format: `VALUE_ONLY` or `JSON`. Default: VALUE\_ONLY.
Engineering unit (e.g., °C, bar, RPM).
Allow writing to this address. Default: false.
Topic to send write commands. Publish a value here to write it to the PLC.
***
## Event-Based Operations
For on-demand S7 operations (not polling), use the EVENT syntax. Publish a message to SOURCE\_TOPIC to trigger the operation; the route executes it and publishes the result to DESTINATION\_TOPIC.
### Supported Operations
| Operation | Description | Query Parameters |
| --------- | --------------------------------- | ------------------------------- |
| `READ` | Read data from a memory address | `address`, `data_type`, `count` |
| `WRITE` | Write a value to a memory address | `address`, `data_type`, `value` |
### Query Parameters
| Parameter | Description | Example |
| ----------- | ------------------------------------------------ | ------------------------------ |
| `operation` | Operation type: `READ` or `WRITE` | `READ` |
| `address` | S7 address in standard notation | `DB1.DBW0`, `MW100`, `I0.0` |
| `data_type` | S7 data type | `WORD`, `REAL`, `BOOL`, `DINT` |
| `count` | Number of consecutive values to read (READ only) | `10` |
| `value` | Value to write (WRITE only) | `42`, `23.5`, `true` |
### Read Example
Read 10 words from Data Block 1 on demand:
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
ADD EVENT ReadOnDemand
WITH SOURCE_TOPIC "s7/commands/read"
WITH DESTINATION_TOPIC "s7/responses/read"
WITH QUERY "{operation: READ, address: DB1.DBW0, data_type: WORD, count: 10}"
```
### Write Example
Write a REAL value to Data Block 10 on demand:
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
ADD EVENT WriteSetpoint
WITH SOURCE_TOPIC "s7/commands/write"
WITH DESTINATION_TOPIC "s7/responses/write"
WITH QUERY "{operation: WRITE, address: DB10.DBD4, data_type: REAL, value: 75.0}"
```
***
## Complete Examples
Read values from a data block:
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
DEFINE ROUTE ProductionPLC WITH TYPE SIEMENS_S7
ADD S7_CONFIG
WITH IP "192.168.1.100"
WITH CPU_TYPE "S71500"
WITH RACK 0
WITH SLOT 1
ADD MAPPING ProductionData
WITH EVERY 500 MILLISECONDS
ADD TAG Temperature
WITH ADDRESS "DB1.DBD0"
WITH DATA_TYPE "REAL"
WITH SOURCE_TOPIC "plc/temperature"
WITH UNIT "°C"
ADD TAG Pressure
WITH ADDRESS "DB1.DBD4"
WITH DATA_TYPE "REAL"
WITH SOURCE_TOPIC "plc/pressure"
WITH UNIT "bar"
ADD TAG Speed
WITH ADDRESS "DB1.DBW8"
WITH DATA_TYPE "INT"
WITH SOURCE_TOPIC "plc/speed"
WITH UNIT "RPM"
ADD TAG Running
WITH ADDRESS "DB1.DBX10.0"
WITH DATA_TYPE "BOOL"
WITH SOURCE_TOPIC "plc/running"
```
Read markers, inputs, and outputs:
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
DEFINE ROUTE IOMonitor WITH TYPE SIEMENS_S7
ADD S7_CONFIG
WITH IP "192.168.1.100"
WITH CPU_TYPE "S71200"
WITH RACK 0
WITH SLOT 1
ADD MAPPING DigitalIO
WITH EVERY 100 MILLISECONDS
ADD TAG StartButton
WITH ADDRESS "I0.0"
WITH ADDRESS_TYPE "INPUT"
WITH DATA_TYPE "BOOL"
WITH SOURCE_TOPIC "plc/io/start"
ADD TAG StopButton
WITH ADDRESS "I0.1"
WITH ADDRESS_TYPE "INPUT"
WITH DATA_TYPE "BOOL"
WITH SOURCE_TOPIC "plc/io/stop"
ADD TAG MotorRunning
WITH ADDRESS "Q0.0"
WITH ADDRESS_TYPE "OUTPUT"
WITH DATA_TYPE "BOOL"
WITH SOURCE_TOPIC "plc/io/motor"
ADD TAG SystemReady
WITH ADDRESS "M0.0"
WITH ADDRESS_TYPE "MEMORY"
WITH DATA_TYPE "BOOL"
WITH SOURCE_TOPIC "plc/status/ready"
```
Read process values and write setpoints:
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
DEFINE ROUTE ControlLoop WITH TYPE SIEMENS_S7
ADD S7_CONFIG
WITH IP "192.168.1.100"
WITH CPU_TYPE "S71500"
WITH RACK 0
WITH SLOT 1
ADD MAPPING ControlData
WITH EVERY 200 MILLISECONDS
ADD TAG ProcessValue
WITH ADDRESS "DB10.DBD0"
WITH DATA_TYPE "REAL"
WITH SOURCE_TOPIC "control/pv"
WITH UNIT "°C"
WITH PUBLISH_MODE "JSON"
ADD TAG Setpoint
WITH ADDRESS "DB10.DBD4"
WITH DATA_TYPE "REAL"
WITH SOURCE_TOPIC "control/sp"
WITH UNIT "°C"
WITH WRITABLE "true"
WITH DESTINATION_TOPIC "control/sp/set"
WITH MIN_VALUE 0
WITH MAX_VALUE 100
ADD TAG Output
WITH ADDRESS "DB10.DBD8"
WITH DATA_TYPE "REAL"
WITH SOURCE_TOPIC "control/output"
WITH UNIT "%"
ADD TAG ManualMode
WITH ADDRESS "DB10.DBX12.0"
WITH DATA_TYPE "BOOL"
WITH SOURCE_TOPIC "control/manual"
WITH WRITABLE "true"
WITH DESTINATION_TOPIC "control/manual/set"
```
Optimize with different update frequencies:
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
DEFINE ROUTE OptimizedPLC WITH TYPE SIEMENS_S7
ADD S7_CONFIG
WITH IP "192.168.1.100"
WITH CPU_TYPE "S71500"
WITH RACK 0
WITH SLOT 1
ADD MAPPING FastLoop
WITH EVERY 100 MILLISECONDS
ADD TAG CriticalAlarm
WITH ADDRESS "DB1.DBX0.0"
WITH DATA_TYPE "BOOL"
WITH SOURCE_TOPIC "plc/alarms/critical"
ADD TAG MotorSpeed
WITH ADDRESS "DB1.DBD10"
WITH DATA_TYPE "REAL"
WITH SOURCE_TOPIC "plc/motor/speed"
ADD MAPPING MediumLoop
WITH EVERY 1 SECOND
ADD TAG Temperature
WITH ADDRESS "DB2.DBD0"
WITH DATA_TYPE "REAL"
WITH SOURCE_TOPIC "plc/process/temp"
ADD TAG Pressure
WITH ADDRESS "DB2.DBD4"
WITH DATA_TYPE "REAL"
WITH SOURCE_TOPIC "plc/process/pressure"
ADD MAPPING SlowLoop
WITH EVERY 10 SECONDS
ADD TAG ProductionCount
WITH ADDRESS "DB3.DBD0"
WITH DATA_TYPE "DINT"
WITH SOURCE_TOPIC "plc/production/count"
ADD TAG RuntimeHours
WITH ADDRESS "DB3.DBD4"
WITH DATA_TYPE "REAL"
WITH SOURCE_TOPIC "plc/stats/runtime"
```
Continuous monitoring with on-demand read and write events in the same route:
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
DEFINE ROUTE FullS7Setup WITH TYPE SIEMENS_S7
ADD S7_CONFIG
WITH IP "192.168.1.100"
WITH CPU_TYPE "S71500"
WITH RACK 0
WITH SLOT 1
ADD MAPPING ProcessData
WITH EVERY 500 MILLISECONDS
ADD TAG Temperature
WITH ADDRESS "DB1.DBD0"
WITH DATA_TYPE "REAL"
WITH SOURCE_TOPIC "plc/temperature"
WITH UNIT "°C"
ADD TAG Pressure
WITH ADDRESS "DB1.DBD4"
WITH DATA_TYPE "REAL"
WITH SOURCE_TOPIC "plc/pressure"
WITH UNIT "bar"
ADD EVENT ReadBlock
WITH SOURCE_TOPIC "s7/commands/read"
WITH DESTINATION_TOPIC "s7/responses/read"
WITH QUERY "{operation: READ, address: DB2.DBW0, data_type: WORD, count: 20}"
ADD EVENT WriteSetpoint
WITH SOURCE_TOPIC "s7/commands/write"
WITH DESTINATION_TOPIC "s7/responses/write"
WITH QUERY "{operation: WRITE, address: DB1.DBD8, data_type: REAL, value: 0}"
```
The MAPPING continuously reads temperature and pressure from DB1. To trigger an on-demand read of 20 words from DB2, publish any message to `s7/commands/read`. To write a setpoint, publish to `s7/commands/write`.
***
## S7-1200/1500 Setup
S7-1200 and S7-1500 PLCs require additional configuration to allow external access.
### Enable PUT/GET Access
1. Open TIA Portal project
2. Navigate to **Device Configuration** → **Properties**
3. Go to **Protection & Security** → **Connection mechanisms**
4. Enable **Permit access with PUT/GET communication from remote partner**
### Optimized Block Access
For S7-1500, ensure data blocks are set to allow standard access:
1. Open the Data Block in TIA Portal
2. In Properties, uncheck **Optimized block access**
3. Compile and download to PLC
***
## Troubleshooting
* Verify IP address is correct
* Check PLC is in RUN mode
* For S7-1200/1500: Enable PUT/GET access
* Verify correct RACK and SLOT settings
* Check firewall allows TCP port 102
Use correct slot for your CPU:
* S7-1200/1500: Slot 1
* S7-300: Slot 2
* S7-400: Slot 2 or 3 (check hardware config)
For S7-1500: Disable optimized block access in TIA Portal for data blocks you want to access.
* Verify address matches TIA Portal
* Check DATA\_TYPE corresponds to PLC variable type
* For multi-word types, verify byte offset is correct
* Check BYTE\_ORDER if values seem swapped
* Ensure WRITABLE is set to "true"
* Verify PLC is in RUN mode
* Check access permissions in PLC
* Confirm value is within MIN\_VALUE/MAX\_VALUE
***
## Next Steps
Cross-platform industrial communication.
Connect to TwinCAT systems.
# MCP Routes
Source: https://docs.coreflux.org/latest/lot-language/routes/mcp-routes
Connect your Coreflux broker to external tools and services through the Model Context Protocol (MCP)
## Plug Your Broker Into Any MCP Tool
MCP (Model Context Protocol) is an open standard that defines how applications communicate with external tool servers. MCP Routes let your Coreflux broker connect to any MCP-compatible server — file systems, messaging platforms, cloud storage, spreadsheets, and more — and call their tools directly from LoT Actions.
**Like adding power tools to a workbench.** Each MCP Route plugs a new tool into your broker — a Slack server for sending messages, a filesystem server for reading logs, a Google Drive server for cloud backups. Once connected, any LoT Action can pick up and use that tool.
***
## Integration Architecture
MCP Routes allow the Coreflux Broker to connect to external MCP servers as a **client**. This enables LoT Actions to call tools provided by any MCP server—file systems, cloud services, communication platforms, and more.
```mermaid actions={false} theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
graph LR
subgraph mcpServers [External MCP Servers]
Filesystem[Filesystem Server]
Slack[Slack Server]
GDrive[Google Drive]
Custom[Custom MCP Server]
end
subgraph broker [Coreflux Broker]
MCPRoutes[MCP Routes]
Actions[LoT Actions]
Topics[MQTT Topics]
end
subgraph devices [IoT Layer]
Sensors[Sensors]
Actuators[Actuators]
end
MCPRoutes -->|connects to| Filesystem
MCPRoutes -->|connects to| Slack
MCPRoutes -->|connects to| GDrive
MCPRoutes -->|connects to| Custom
Actions -->|CALL MCP| MCPRoutes
Actions -->|PUBLISH/KEEP| Topics
Topics <--> Sensors
Topics <--> Actuators
```
### How It Works
1. **Define MCP Routes** - Each route connects to an external MCP server
2. **Discover Tools** - The broker discovers available tools from each server
3. **Call from Actions** - Use `CALL MCP` in LoT Actions to invoke any tool
4. **Reusable Connections** - Multiple actions can share the same MCP route
**MCP as a Connection Type.** Just like database routes or REST API routes, MCP routes provide a reusable connection that any action can leverage. Define the route once, use it everywhere.
***
## Basic Syntax
The `MCP` route type connects your Coreflux broker to an external MCP server, making that server's tools callable from LoT Actions. Each route manages one MCP server connection, and you can define multiple routes to connect to different servers simultaneously.
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
DEFINE ROUTE MCPFilesystem WITH TYPE MCP
ADD MCP_CONFIG
WITH SERVER_COMMAND "npx"
WITH SERVER_ARGUMENTS "-y, @modelcontextprotocol/server-filesystem, /data"
WITH CLIENT_NAME "CorefluxFS"
```
Once connected, any LoT Action can use `CALL MCP` to invoke the server's tools — send Slack messages, read and write files, generate spreadsheet reports, or interact with any other MCP-compatible service.
***
## MCP Configuration
### MCP\_CONFIG Parameters
MCP server command to execute (e.g., `npx`, `python`, `node`).
Comma-separated server arguments (e.g., `-y, @modelcontextprotocol/server-filesystem`).
MCP client identifier. Default: CorefluxBroker.
***
## Configuration Examples
Enable LoT Actions to perform file operations:
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
DEFINE ROUTE MCPFilesystem WITH TYPE MCP
ADD MCP_CONFIG
WITH SERVER_COMMAND "npx"
WITH SERVER_ARGUMENTS "-y, @modelcontextprotocol/server-filesystem, /data"
WITH CLIENT_NAME "CorefluxFS"
```
Connect to a custom MCP server:
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
DEFINE ROUTE MCPCustom WITH TYPE MCP
ADD MCP_CONFIG
WITH SERVER_COMMAND "python"
WITH SERVER_ARGUMENTS "-m, my_mcp_server"
WITH CLIENT_NAME "CorefluxCustom"
```
Run a Node.js MCP server:
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
DEFINE ROUTE MCPNode WITH TYPE MCP
ADD MCP_CONFIG
WITH SERVER_COMMAND "node"
WITH SERVER_ARGUMENTS "/opt/mcp/server.js, --config, /etc/mcp/config.json"
WITH CLIENT_NAME "CorefluxNode"
```
***
## MCP Server Examples
The MCP ecosystem includes servers for various platforms and services. These examples show how to connect popular MCP servers to your Coreflux broker for IoT integration.
Node.js-based MCP servers can be run directly using `npx`. These are the most common and include official Anthropic servers.
| Server | LoT Configuration | IoT Use Case |
| ------------------- | --------------------------------------------------------------------------------------------------- | ------------------------------------- |
| **Filesystem** | `SERVER_COMMAND "npx"` with `SERVER_ARGUMENTS "-y, @modelcontextprotocol/server-filesystem, /data"` | Read sensor logs, manage config files |
| **Google Drive** | `SERVER_COMMAND "npx"` with `SERVER_ARGUMENTS "-y, @modelcontextprotocol/server-gdrive"` | Cloud backup of IoT data |
| **Slack** | `SERVER_COMMAND "npx"` with `SERVER_ARGUMENTS "-y, @modelcontextprotocol/server-slack"` | Send alerts to team channels |
| **Memory** | `SERVER_COMMAND "npx"` with `SERVER_ARGUMENTS "-y, @modelcontextprotocol/server-memory"` | Persist context across AI sessions |
| **Fetch** | `SERVER_COMMAND "npx"` with `SERVER_ARGUMENTS "-y, @modelcontextprotocol/server-fetch"` | Access external APIs and web data |
| **Windows Control** | `SERVER_COMMAND "npx"` with `SERVER_ARGUMENTS "-y, mcp-control"` | Automate HMI/SCADA systems |
**Example: Slack Alerts for IoT Events**
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
DEFINE ROUTE MCPSlack WITH TYPE MCP
ADD MCP_CONFIG
WITH SERVER_COMMAND "npx"
WITH SERVER_ARGUMENTS "-y, @modelcontextprotocol/server-slack"
WITH CLIENT_NAME "CorefluxSlack"
```
Python-based MCP servers use `uvx` (the uv package runner). These are common for third-party integrations.
| Server | LoT Configuration | IoT Use Case |
| ----------- | ----------------------------------------------------------------- | --------------------------------- |
| **Spotify** | `SERVER_COMMAND "uvx"` with `SERVER_ARGUMENTS "spotify-mcp"` | Smart home audio control |
| **Excel** | `SERVER_COMMAND "uvx"` with `SERVER_ARGUMENTS "excel-mcp-server"` | Generate reports from sensor data |
**Example: Excel Reports from Sensor Data**
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
DEFINE ROUTE MCPExcel WITH TYPE MCP
ADD MCP_CONFIG
WITH SERVER_COMMAND "uvx"
WITH SERVER_ARGUMENTS "excel-mcp-server"
WITH CLIENT_NAME "CorefluxExcel"
```
***
## Calling MCP Tools
Once an MCP route is defined, you can call its tools from within LoT Actions using the `CALL MCP` syntax. This enables AI-powered automation triggered by MQTT events.
### Basic Syntax
The standard syntax to invoke an MCP tool from an Action:
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
CALL MCP "RouteName.ToolName"
WITH (arg1 = value1, arg2 = value2)
RETURN AS {result_variable}
```
| Component | Format | Description |
| -------------- | -------------------------- | ------------------------------------------- |
| **Route.Tool** | `"RouteName.ToolName"` | Route name and tool name separated by a dot |
| **WITH** | `WITH (name = value, ...)` | Named arguments in parentheses |
| **RETURN AS** | `RETURN AS {varname}` | Capture the result in a variable |
### Complete Example
This Action monitors a temperature sensor and uses an MCP tool to send a Slack message when the temperature exceeds a threshold:
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
DEFINE ACTION TemperatureAlert
ON TOPIC "sensors/+/temperature" DO
SET "temp" WITH (GET JSON "value" IN PAYLOAD AS DOUBLE)
SET "sensor_id" WITH TOPIC POSITION 2
IF {temp} > 80 THEN
CALL MCP "MCPSlack.send-message"
WITH (channel = "alerts", message = "🔥 Sensor " + {sensor_id} + " at " + {temp} + "°C")
RETURN AS {result}
PUBLISH TOPIC "alerts/temperature/sent" WITH "Sensor: " + {sensor_id} + " Temp: " + {temp} + " Result: " + {result}
END IF
```
### Syntax Variations
Some tools don't require arguments:
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
CALL MCP "MCPSystem.get-status"
RETURN AS {status}
```
Pass multiple named parameters:
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
CALL MCP "MCPFilesystem.write-file"
WITH (path = "/data/log.txt", content = {payload}, append = true)
RETURN AS {write_result}
```
Build dynamic values using expressions:
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
CALL MCP "MCPSlack.send-message"
WITH (message = "Device " + {device_id} + " reported: " + {value} + " at " + TIMESTAMP "UTC")
RETURN AS {chat_result}
```
***
## Tool Discovery
When an MCP route connects to a server, Coreflux automatically discovers all available tools and publishes them to MQTT system topics. This allows you to dynamically query what tools are available.
### System Topics
| Topic | Description |
| --------------------------------------------------- | --------------------------------- |
| `$SYS/Coreflux/Routes/{RouteName}/Tools` | Summary of all tools for a route |
| `$SYS/Coreflux/Routes/{RouteName}/Tools/{ToolName}` | Detailed info for a specific tool |
### Tools Summary Payload
Subscribe to the summary topic to see all available tools:
```json wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
{
"route_name": "MCPSlack",
"route_type": "MCP",
"total_tools": 8,
"tools": ["send-message", "list-channels", "get-channel-history", "add-reaction", ...],
"timestamp": "2025-12-31T12:00:00.000Z"
}
```
### Individual Tool Payload
Each tool publishes detailed metadata including parameters and usage examples:
```json wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
{
"name": "send-message",
"route_name": "MCPSlack",
"tool_type": "MCP",
"description": "Send a message to a Slack channel",
"parameters": {
"channel": { "type": "string", "required": true },
"message": { "type": "string", "required": true }
},
"usage_example": "CALL MCP \"MCPSlack.send-message\" WITH (channel = {channel}, message = {message}) RETURN AS {result}",
"capability": {
"execution_count": 42,
"average_latency_ms": 150.25,
"reliability_percent": 98.5
},
"timestamp": "2025-12-31T12:00:00.000Z"
}
```
### Subscribing to Tool Discovery
Use MQTT Explorer or any MQTT client to discover available tools:
| What to Find | Topic Pattern |
| ------------------------------- | -------------------------------------------------- |
| All tools for a specific route | `$SYS/Coreflux/Routes/MCPSlack/Tools` |
| Details for a specific tool | `$SYS/Coreflux/Routes/MCPSlack/Tools/send-message` |
| All tools across all MCP routes | `$SYS/Coreflux/Routes/+/Tools/#` |
Tool discovery topics are **retained messages**, so the information is immediately available when you subscribe—you don't need to wait for the next update.
***
## Use Cases
Connect to Slack, email, or other messaging MCP servers to send alerts directly from LoT Actions. When a sensor crosses a threshold, the Action calls the messaging tool — no external middleware needed.
Use filesystem MCP servers to read configuration files, write sensor logs, or manage data exports. LoT Actions can read a config file at startup or append readings to a log file on every event.
Connect to Google Drive, Excel, or other cloud MCP servers to back up IoT data, generate reports from sensor readings, or sync configurations with cloud storage.
Use Windows Control or custom MCP servers to automate HMI/SCADA interfaces, trigger external scripts, or interact with desktop applications as part of your IoT workflow.
***
## Best Practices
* Limit MCP access to specific topics and actions
* Use authentication for MCP server connections
* Monitor MCP tool calls through logging
* Implement rate limiting for MCP requests
* Configure appropriate timeouts for MCP operations
* Limit concurrent MCP connections if needed
* Monitor memory usage of MCP server processes
* Implement graceful fallbacks when the MCP server is unavailable
* Log MCP communication errors for debugging
* Set up alerts for repeated failures
***
## Troubleshooting
* Verify SERVER\_COMMAND path is correct
* Check SERVER\_ARGUMENTS syntax (comma-separated)
* Ensure required packages are installed (npx, node, python)
* Check system PATH includes required executables
* Verify MCP server is running and responsive
* Check network/firewall settings
* Increase timeout values if server startup is slow
* Check server logs for errors
* Verify the requested tool is available
* Check permissions for the operation
* Review server logs for detailed error messages
* Ensure broker has required access rights
***
## Next Steps
Connect to external REST APIs.
Add custom AI/ML logic with Python.
# Media Routes
Source: https://docs.coreflux.org/latest/lot-language/routes/media-routes
Process barcodes and video streams with encoding, decoding, and streaming capabilities
## Media Routes Overview
Media routes enable processing of visual data including barcode scanning and video streaming. These routes bridge the gap between image/video sources and your MQTT infrastructure.
**Like adding eyes to your broker.** Media routes let your MQTT broker see and interpret visual data — scanning barcodes off a camera feed or streaming video to a dashboard — without any external processing code.
***
## Barcode Route
The `BARCODE` route enables encoding and decoding of various barcode formats including QR codes, Code 128, EAN-13, and more.
### Basic Syntax
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
DEFINE ROUTE BarcodeProcessor WITH TYPE BARCODE
ADD BARCODE_CONFIG
WITH SUPPORTED_FORMATS "QR_CODE, CODE_128, EAN_13"
ADD EVENT DecodeBarcode
WITH SOURCE_TOPIC "barcode/scan"
WITH DESTINATION_TOPIC "barcode/result"
WITH MODE "DECODE"
```
### Configuration Parameters
Comma-separated barcode formats to support (e.g., `QR_CODE, CODE_128, EAN_13`).
### Event Parameters
MQTT topic with barcode image (base64) or data to encode.
MQTT topic to publish barcode results.
Operation mode: `ENCODE` (create barcode) or `DECODE` (read barcode).
Annotate decoded barcode location on image. Default: false.
Topic to publish annotated image (for DECODE mode).
### Supported Formats
| Format | Description |
| ------------- | ------------------- |
| `QR_CODE` | QR Code (2D) |
| `CODE_128` | Code 128 (1D) |
| `CODE_39` | Code 39 (1D) |
| `EAN_13` | EAN-13 (1D retail) |
| `EAN_8` | EAN-8 (1D retail) |
| `UPC_A` | UPC-A (1D retail) |
| `UPC_E` | UPC-E (1D retail) |
| `DATA_MATRIX` | Data Matrix (2D) |
| `PDF_417` | PDF417 (2D stacked) |
### Barcode Examples
Scan barcodes from images:
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
DEFINE ROUTE BarcodeScanner WITH TYPE BARCODE
ADD BARCODE_CONFIG
WITH SUPPORTED_FORMATS "QR_CODE, CODE_128, EAN_13, UPC_A"
ADD EVENT ScanBarcode
WITH SOURCE_TOPIC "camera/frame"
WITH DESTINATION_TOPIC "barcode/decoded"
WITH MODE "DECODE"
WITH ENABLE_ANNOTATION "true"
WITH ANNOTATED_IMAGE_TOPIC "barcode/annotated"
```
**Input:** Base64-encoded image on `camera/frame`
**Output:** JSON with decoded data on `barcode/decoded`:
```json wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
{
"format": "QR_CODE",
"data": "https://example.com",
"timestamp": "2024-01-15T10:30:00Z"
}
```
Generate barcode images:
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
DEFINE ROUTE BarcodeGenerator WITH TYPE BARCODE
ADD BARCODE_CONFIG
WITH SUPPORTED_FORMATS "QR_CODE, CODE_128"
ADD EVENT GenerateBarcode
WITH SOURCE_TOPIC "barcode/generate"
WITH DESTINATION_TOPIC "barcode/image"
WITH MODE "ENCODE"
```
**Input:** JSON with data and format on `barcode/generate`:
```json wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
{
"format": "QR_CODE",
"data": "Product-12345",
"width": 300,
"height": 300
}
```
**Output:** Base64-encoded barcode image on `barcode/image`
Industrial barcode scanning:
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
DEFINE ROUTE ProductionScanner WITH TYPE BARCODE
ADD BARCODE_CONFIG
WITH SUPPORTED_FORMATS "CODE_128, DATA_MATRIX"
ADD EVENT ScanProduct
WITH SOURCE_TOPIC "line/camera/frame"
WITH DESTINATION_TOPIC "line/product/scanned"
WITH MODE "DECODE"
ADD EVENT ScanSerial
WITH SOURCE_TOPIC "line/serial/frame"
WITH DESTINATION_TOPIC "line/serial/decoded"
WITH MODE "DECODE"
WITH ENABLE_ANNOTATION "true"
WITH ANNOTATED_IMAGE_TOPIC "line/serial/annotated"
```
***
## Video Input Route
The `VIDEO_INPUT` route captures video frames from RTSP streams, webcams, or video files and publishes them to MQTT topics.
### Basic Syntax
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
DEFINE ROUTE VideoCapture WITH TYPE VIDEO_INPUT
ADD VIDEO_INPUT_CONFIG
WITH SOURCE "rtsp://192.168.1.100:554/stream"
WITH DESTINATION_TOPIC "video/frames"
WITH FORMAT "JPEG"
WITH FRAME_RATE '10'
```
### Configuration Parameters
Video source: RTSP URL, webcam identifier, or file path.
MQTT topic to publish video frames.
Output format: `JPEG`, `MJPEG`, `H264`, `H265`, `MP4`, `MPEGTS`.
Frame rate for capture (1-60 fps).
Frame width in pixels.
Frame height in pixels.
JPEG quality (1-100).
### Event Parameters
MQTT topic to control video capture.
Control command: `START`, `STOP`, or `CAPTURE `.
### Video Input Examples
Capture from IP camera:
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
DEFINE ROUTE IPCamera WITH TYPE VIDEO_INPUT
ADD VIDEO_INPUT_CONFIG
WITH SOURCE "rtsp://admin:password@192.168.1.100:554/stream1"
WITH DESTINATION_TOPIC "camera/entrance/frames"
WITH FORMAT "JPEG"
WITH FRAME_RATE '5'
WITH WIDTH '1280'
WITH HEIGHT '720'
WITH QUALITY '80'
ADD EVENT CameraControl
WITH SOURCE_TOPIC "camera/entrance/control"
WITH QUERY "START"
```
Capture from local webcam:
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
DEFINE ROUTE Webcam WITH TYPE VIDEO_INPUT
ADD VIDEO_INPUT_CONFIG
WITH SOURCE "webcam:0"
WITH DESTINATION_TOPIC "webcam/frames"
WITH FORMAT "JPEG"
WITH FRAME_RATE '15'
WITH WIDTH '640'
WITH HEIGHT '480'
ADD EVENT WebcamControl
WITH SOURCE_TOPIC "webcam/control"
WITH QUERY "START"
```
Process a video file:
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
DEFINE ROUTE VideoFile WITH TYPE VIDEO_INPUT
ADD VIDEO_INPUT_CONFIG
WITH SOURCE "/videos/sample.mp4"
WITH DESTINATION_TOPIC "video/file/frames"
WITH FORMAT "JPEG"
WITH FRAME_RATE '30'
ADD EVENT FileControl
WITH SOURCE_TOPIC "video/file/control"
WITH QUERY "START"
```
Low frame rate for motion detection:
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
DEFINE ROUTE MotionDetection WITH TYPE VIDEO_INPUT
ADD VIDEO_INPUT_CONFIG
WITH SOURCE "rtsp://192.168.1.50:554/stream"
WITH DESTINATION_TOPIC "security/motion/frames"
WITH FORMAT "JPEG"
WITH FRAME_RATE '2'
WITH WIDTH '640'
WITH HEIGHT '480'
WITH QUALITY '60'
ADD EVENT MotionControl
WITH SOURCE_TOPIC "security/motion/control"
WITH QUERY "START"
```
***
## Video Output Route
The `VIDEO_OUTPUT` route streams video frames from MQTT to various output protocols.
### Basic Syntax
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
DEFINE ROUTE VideoStreamer WITH TYPE VIDEO_OUTPUT
ADD VIDEO_OUTPUT_CONFIG
WITH SOURCE_TOPIC "video/frames"
WITH OUTPUT_PROTOCOL "HTTP"
WITH OUTPUT_PATH "/stream"
```
### Configuration Parameters
MQTT topic with video frames (base64).
Output protocol: `HTTP`, `RTSP`, `UDP`, `TCP`.
Output path or port for streaming.
### Event Parameters
Control command: `START` or `STOP`.
### Video Output Examples
Stream via HTTP (MJPEG):
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
DEFINE ROUTE HTTPStream WITH TYPE VIDEO_OUTPUT
ADD VIDEO_OUTPUT_CONFIG
WITH SOURCE_TOPIC "camera/frames"
WITH OUTPUT_PROTOCOL "HTTP"
WITH OUTPUT_PATH ":8081/video"
ADD EVENT StreamControl
WITH QUERY "START"
```
Access stream at: `http://broker-ip:8081/video`
Re-stream as RTSP:
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
DEFINE ROUTE RTSPOutput WITH TYPE VIDEO_OUTPUT
ADD VIDEO_OUTPUT_CONFIG
WITH SOURCE_TOPIC "processed/frames"
WITH OUTPUT_PROTOCOL "RTSP"
WITH OUTPUT_PATH ":8554/stream"
ADD EVENT RTSPControl
WITH QUERY "START"
```
Access stream at: `rtsp://broker-ip:8554/stream`
Multicast video stream:
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
DEFINE ROUTE UDPStream WITH TYPE VIDEO_OUTPUT
ADD VIDEO_OUTPUT_CONFIG
WITH SOURCE_TOPIC "broadcast/frames"
WITH OUTPUT_PROTOCOL "UDP"
WITH OUTPUT_PATH "239.0.0.1:5000"
ADD EVENT UDPControl
WITH QUERY "START"
```
***
## Complete Example: Vision System
A complete vision system with camera input, barcode scanning, and video streaming:
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
// Camera input
DEFINE ROUTE InspectionCamera WITH TYPE VIDEO_INPUT
ADD VIDEO_INPUT_CONFIG
WITH SOURCE "rtsp://192.168.1.100:554/stream"
WITH DESTINATION_TOPIC "inspection/camera/raw"
WITH FORMAT "JPEG"
WITH FRAME_RATE '10'
WITH WIDTH '1920'
WITH HEIGHT '1080'
WITH QUALITY '90'
ADD EVENT CameraControl
WITH SOURCE_TOPIC "inspection/camera/control"
WITH QUERY "START"
// Barcode scanning
DEFINE ROUTE ProductScanner WITH TYPE BARCODE
ADD BARCODE_CONFIG
WITH SUPPORTED_FORMATS "CODE_128, DATA_MATRIX, QR_CODE"
ADD EVENT ScanProduct
WITH SOURCE_TOPIC "inspection/camera/raw"
WITH DESTINATION_TOPIC "inspection/barcode/result"
WITH MODE "DECODE"
WITH ENABLE_ANNOTATION "true"
WITH ANNOTATED_IMAGE_TOPIC "inspection/camera/annotated"
// Live stream for operators
DEFINE ROUTE OperatorStream WITH TYPE VIDEO_OUTPUT
ADD VIDEO_OUTPUT_CONFIG
WITH SOURCE_TOPIC "inspection/camera/annotated"
WITH OUTPUT_PROTOCOL "HTTP"
WITH OUTPUT_PATH ":8080/inspection"
ADD EVENT StreamControl
WITH QUERY "START"
```
***
## Best Practices
Use lower frame rates for bandwidth efficiency:
* **Monitoring**: 1-5 fps
* **Motion detection**: 2-5 fps
* **Barcode scanning**: 5-10 fps
* **Live viewing**: 15-30 fps
Balance quality and performance:
* **Barcode scanning**: 640x480 to 1280x720
* **Monitoring**: 1280x720
* **High detail**: 1920x1080
JPEG format is ideal for MQTT transport:
* Reasonable file sizes
* Wide compatibility
* Adjustable quality (60-90 recommended)
Configure reconnection for RTSP streams and monitor connection status.
***
## Troubleshooting
* Verify RTSP URL format and credentials
* Check camera is accessible on network
* Ensure port is not blocked by firewall
* Try alternative RTSP paths (check camera documentation)
* Ensure image quality is sufficient
* Check barcode format is in SUPPORTED\_FORMATS
* Verify barcode is properly visible in image
* Try higher resolution capture
* Reduce frame rate
* Lower resolution
* Decrease JPEG quality
* Process fewer simultaneous streams
* Use UDP for lowest latency
* Reduce frame size and quality
* Check network bandwidth
* Use local network when possible
***
## Next Steps
Connect to PLCs and industrial equipment.
See complete route configuration examples.
# Email Route
Source: https://docs.coreflux.org/latest/lot-language/routes/notifications/email
Send templated email notifications and alerts from MQTT messages over SMTP
## Email Overview
The `EMAIL` route sends templated emails in response to MQTT messages. It's the simplest way to turn a broker event — a critical alarm, a daily report, a user notification — into a message that reaches a person's inbox.
**Like an auto-reply rule in your mailbox.** When a message lands on a topic you're watching, the route fills in a template and sends the email for you — no manual step required.
### When to use it
* Alert an operations team the moment a critical alarm fires.
* Email a stakeholder a daily or shift report.
* Notify an individual user using an address pulled from the message payload.
***
Let's look at how to define an email route, starting with the most common scenarios.
## Quick Start
Send an immediate alert to a fixed team address when a critical event arrives:
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
DEFINE ROUTE CriticalAlerts WITH TYPE EMAIL
ADD SMTP_CONFIG
WITH HOST "smtp.gmail.com"
WITH PORT '587'
WITH USERNAME "alerts@company.com"
WITH PASSWORD "app-password-here"
WITH USE_TLS "true"
ADD EVENT criticalNotify
WITH SOURCE_TOPIC "alerts/critical/+"
WITH TEMPLATE_PATH "/templates/critical_alert.html"
WITH SUBJECT "CRITICAL: {value.json.alert_type}"
WITH RECIPIENT "ops-team@company.com"
```
Send to an address taken from the message payload:
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
DEFINE ROUTE UserNotifications WITH TYPE EMAIL
ADD SMTP_CONFIG
WITH HOST "smtp.office365.com"
WITH PORT '587'
WITH USERNAME "noreply@company.com"
WITH PASSWORD "secure-password"
WITH USE_TLS "true"
ADD EVENT userNotify
WITH SOURCE_TOPIC "users/+/notifications"
WITH TEMPLATE_PATH "/templates/user_notification.html"
WITH SUBJECT "Hello {value.json.username}"
WITH RECIPIENT "{value.json.email}"
```
Embed images (a logo, a chart) directly in the email:
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
DEFINE ROUTE ReportEmail WITH TYPE EMAIL
ADD SMTP_CONFIG
WITH HOST "smtp.company.com"
WITH PORT '587'
WITH USERNAME "reports@company.com"
WITH PASSWORD "password"
WITH USE_TLS "true"
ADD EVENT sendReport
WITH SOURCE_TOPIC "reports/daily"
WITH TEMPLATE_PATH "/templates/daily_report.html"
WITH SUBJECT "Daily Report - {value.json.date}"
WITH RECIPIENT "management@company.com"
WITH EMBED_RESOURCES
ADD "/images/logo.png"
ADD "/images/chart.png"
```
For Gmail, use an [App Password](https://support.google.com/accounts/answer/185833) instead of your regular password. Enable 2FA on your Google account first.
***
## SMTP Configuration
The `SMTP_CONFIG` block tells the route which mail server to send through and how to authenticate.
SMTP server address (e.g., `smtp.gmail.com`, `smtp.office365.com`).
SMTP port (typically 587 for TLS, 465 for SSL).
Enable TLS encryption. Recommended: true.
Email account to send from.
Email password or app-specific password.
***
## Event Configuration
Each `ADD EVENT` block defines a trigger topic and the content of the email it sends.
MQTT topic to trigger email sending.
MQTT topic to publish send status.
Path to HTML email template file.
Email subject line (supports placeholders).
Email recipient address (supports placeholders).
### Template Placeholders
Subjects, recipients, and templates support placeholders that are filled in from the triggering message:
| Placeholder | Description |
| --------------------------- | --------------------------- |
| `{value}` | Full JSON payload as string |
| `{value.json.field}` | Single field from JSON |
| `{value.json.nested.field}` | Nested field access |
***
## Troubleshooting
* Verify SMTP credentials are correct
* For Gmail, ensure you're using an App Password
* Check firewall allows outbound SMTP connections
* Verify recipient email address is valid
* Double-check USERNAME and PASSWORD
* Ensure 2FA is enabled if using app passwords
* Some providers require enabling "less secure apps" access
* Check for account lockouts due to failed attempts
***
## Next Steps
Publish the MQTT events that trigger notifications.
Store MQTT data in databases.
# Routes Overview
Source: https://docs.coreflux.org/latest/lot-language/routes/overview
Connect your Coreflux broker to external systems, databases, industrial equipment, and other brokers using LoT routes
## What are Routes?
Routes define connections between your Coreflux MQTT broker and external systems. They enable broker-to-broker communication, database storage, industrial protocol integration, REST API connectivity, email notifications, AI integration, and media processing - all without writing custom middleware.
Routes extend your broker's capabilities by creating managed connections to external systems - all configured through LoT syntax.
## Route Categories
Configure MQTT bridges and Sparkplug roles.
Connect MQTT topics to external event-streaming platforms.
Send alerts and reports to people from MQTT events.
Store MQTT data in PostgreSQL, MySQL, MongoDB, OpenSearch, Snowflake, and more.
HTTP client/server for external API integration and webhook endpoints.
Connect to PLCs via Modbus, Siemens S7, OPC UA, ADS, Allen-Bradley, and FINS.
Connect to external tools and services through MCP.
Add an autonomous AI assistant that can read topics and call tools.
Barcode scanning and video streaming capabilities.
Choose wait or drop when a route cannot keep up with incoming MQTT.
This page covers the fundamentals of route configuration. Choose a specific route category above for detailed syntax and examples.
***
## Basic Syntax
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
DEFINE ROUTE WITH TYPE
ADD
WITH ""
ADD MAPPING
WITH SOURCE_TOPIC ""
WITH DESTINATION_TOPIC ""
```
### Components
Unique identifier for the route. Use descriptive names like `CloudBridge`, `SensorDataIndexer`, `AlertEmailer`.
The type of route (see Available Route Types below).
Configuration blocks specific to the route type (e.g., `MQTT_CONFIG`, `MONGODB_CONFIG`, `REST_API_CONFIG`).
Optional. Controls wait-or-drop behavior when this route cannot keep up with incoming MQTT. Works on every route type. See [Route Overload Configuration](./route-config).
Defines topic mappings for data flow between source and destination.
***
## Available Route Types
| Route Type | Description |
| ---------------- | ------------------------------------------- |
| `MQTT_BRIDGE` | Broker-to-broker topic synchronization |
| `SPARKPLUG_B` | Sparkplug decoding and edge-node publishing |
| `SPARKPLUG_HOST` | Sparkplug Primary Host Application |
| Route Type | Description |
| ---------- | --------------------------------------- |
| `KAFKA` | Stream data both ways with Apache Kafka |
| Route Type | Description |
| ---------- | ---------------------------------------- |
| `EMAIL` | Send templated emails from MQTT messages |
| Route Type | Description |
| -------------- | ------------------------------ |
| `POSTGRESQL` | PostgreSQL database storage |
| `MYSQL` | MySQL database storage |
| `MARIADB` | MariaDB database storage |
| `SQLSERVER` | SQL Server database storage |
| `MONGODB` | MongoDB document database |
| `OPENSEARCH` | OpenSearch indexing and search |
| `CRATEDB` | CrateDB time-series database |
| `SNOWFLAKE` | Snowflake cloud data warehouse |
| `FILE_STORAGE` | CSV/JSON file storage |
| Route Type | Description |
| --------------- | --------------------------- |
| `MODBUS_TCP` | Modbus TCP protocol |
| `MODBUS_SERIAL` | Modbus RTU over RS-232/485 |
| `SIEMENS_S7` | Siemens S7 PLCs |
| `OPCUA` | OPC UA client |
| `ADS` | Beckhoff TwinCAT ADS |
| `ALLEN_BRADLEY` | Allen-Bradley/Rockwell PLCs |
| `ETHERNETIP` | EtherNet/IP CIP protocol |
| `FINS` | Omron FINS protocol |
| Route Type | Description |
| -------------- | ----------------------------------------- |
| `REST_API` | REST API client/server |
| `MQTT_TO_REST` | Forward MQTT messages to REST endpoints |
| `MCP` | Model Context Protocol for external tools |
| `AGENT` | Autonomous AI assistant with tool access |
| `BARCODE` | Barcode encoding/decoding |
| `VIDEO_INPUT` | Video capture |
| `VIDEO_OUTPUT` | Video streaming |
***
## Quick Examples
Send email notifications from MQTT events:
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
DEFINE ROUTE AlertEmail WITH TYPE EMAIL
ADD SMTP_CONFIG
WITH HOST "smtp.gmail.com"
WITH PORT '587'
WITH USERNAME "alerts@company.com"
WITH PASSWORD "app-password"
WITH USE_TLS "true"
ADD EVENT criticalAlert
WITH SOURCE_TOPIC "alerts/critical/+"
WITH SUBJECT "Critical Alert: {value.json.type}"
WITH RECIPIENT "team@company.com"
```
Read data from PLCs via Modbus:
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
DEFINE ROUTE PlcReader WITH TYPE MODBUS_TCP
ADD MODBUS_CONFIG
WITH HOST "192.168.1.100"
WITH PORT '502'
WITH SLAVE_ID '1'
ADD MAPPING Sensors
WITH EVERY 500 MILLISECONDS
ADD TAG Temperature
WITH ADDRESS "100"
WITH ADDRESS_TYPE "HOLDING_REGISTER"
WITH DATA_TYPE "FLOAT"
WITH SOURCE_TOPIC "plc/temperature"
```
Connect to external REST APIs:
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
DEFINE ROUTE WeatherService WITH TYPE REST_API
ADD REST_API_CONFIG
WITH ENABLE_CLIENT "true"
WITH BASE_ADDRESS "https://api.weather.com"
WITH USE_SSL "true"
ADD EVENT GetWeather
WITH SOURCE_TOPIC "weather/request"
WITH DESTINATION_TOPIC "weather/response"
WITH METHOD "GET"
WITH ENDPOINT "/v1/current?city={value.json.city}"
```
Store MQTT data in PostgreSQL:
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
DEFINE ROUTE SensorDB WITH TYPE POSTGRESQL
ADD SQL_CONFIG
WITH SERVER "postgres.example.com"
WITH PORT '5432'
WITH DATABASE "iot_data"
WITH USERNAME "iot_user"
WITH PASSWORD "secure_password"
ADD EVENT StoreSensorReading
WITH SOURCE_TOPIC "sensors/+/reading"
WITH QUERY "INSERT INTO readings (ts, sensor, value) VALUES (NOW(), '{sensor_id}', '{value.json}')"
```
Connect two brokers and sync topics:
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
DEFINE ROUTE CloudBridge WITH TYPE MQTT_BRIDGE
ADD SOURCE_CONFIG
WITH BROKER SELF
ADD DESTINATION_CONFIG
WITH BROKER_ADDRESS "cloud.example.com"
WITH BROKER_PORT '8883'
WITH CLIENT_ID "EdgeBridge"
WITH USE_TLS "true"
ADD MAPPING sensorSync
WITH SOURCE_TOPIC "sensors/+/data"
WITH DESTINATION_TOPIC "edge/sensors/+/data"
WITH DIRECTION "out"
```
***
## Understanding Route Configuration
Routes share two common building blocks: **MQTT configuration** and **topic mappings**. You'll see these across most route types, so understanding them here saves time when setting up any specific route.
### Connecting to a Broker
Routes that communicate with MQTT brokers — like bridges — need to know which broker to talk to. The simplest option is `BROKER SELF`, which tells the route to use your local Coreflux broker:
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
ADD MQTT_CONFIG
WITH BROKER SELF
```
When you need to reach a **remote** or **authenticated** broker (for example, a cloud broker or a second Coreflux instance on your network), provide the connection details explicitly:
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
ADD MQTT_CONFIG
WITH BROKER_ADDRESS "192.168.1.50"
WITH BROKER_PORT '1883'
WITH CLIENT_ID "MyRouteClient"
WITH USERNAME "user"
WITH PASSWORD "password"
```
Not every route type requires an `MQTT_CONFIG` block. Database routes, email routes, and REST routes have their own configuration sections. Check the specific route page for details.
### Controlling Data Flow with Mappings
Mappings define **which MQTT topics** flow through a route and **in which direction**. For example, to push local sensor readings to a cloud broker:
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
ADD MAPPING sensorSync
WITH SOURCE_TOPIC "sensors/+/temperature"
WITH DESTINATION_TOPIC "cloud/sensors/+/temperature"
WITH DIRECTION "out"
```
The `DIRECTION` parameter controls where data goes:
| Direction | What it does | Example scenario |
| --------- | --------------------------- | ---------------------------- |
| `out` | Local broker to destination | Push edge data to the cloud |
| `in` | Destination to local broker | Pull remote commands locally |
| `both` | Bidirectional sync | Keep two brokers in sync |
### When a route cannot keep up
Incoming MQTT can outpace the destination. Add `ADD ROUTE_CONFIG` on any route to choose whether that route waits or drops the newest message. Routes without the block use the broker default (`balanced`). See [Route Overload Configuration](./route-config).
***
## Route Status Monitoring
Every route automatically publishes real-time health and status information to a dedicated MQTT topic. No additional configuration is required — the status topic is created as soon as the route is defined.
Status updates are published to a `$SYS` topic and are available to any client subscribed to the broker.
### Status Topic
The topic follows this pattern:
```
$SYS/Coreflux/Routes//status
```
For example, a route named `MyS7Route` publishes its status to `$SYS/Coreflux/Routes/MyS7Route/status`.
### Status Payload
The status topic publishes a JSON object with connection health, message statistics, and timing information:
```json wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
{
"name": "MyS7Route",
"type": "SIEMENS_S7",
"connection": "Connected",
"health": "Green",
"errors": {
"total": 0,
"consecutive": 0,
"rate": 0
},
"messages": {
"total": 4749,
"succeeded": 4749,
"failed": 0
},
"lastActivity": "2026-03-03T15:03:14.563Z",
"lastSuccess": "2026-03-03T15:03:14.563Z",
"lastError": null,
"uptime": "00:08:01",
"connectedSince": "2026-03-03T14:55:13.299Z",
"timestamp": "2026-03-03T15:03:14.590Z"
}
```
### Field Reference
| Field | Type | Description |
| -------------------- | -------------- | ----------------------------------------------------------------------- |
| `name` | string | The route name as defined in LoT |
| `type` | string | The route type (e.g., `SIEMENS_S7`, `MQTT_BRIDGE`, `POSTGRESQL`) |
| `connection` | string | Current connection state (`Connected`, `Disconnected`, etc.) |
| `health` | string | Overall health indicator (`Green`, `Yellow`, `Red`) |
| `errors.total` | integer | Total number of errors since the route started |
| `errors.consecutive` | integer | Number of consecutive errors without a successful message |
| `errors.rate` | number | Current error rate |
| `messages.total` | integer | Total messages processed |
| `messages.succeeded` | integer | Messages processed successfully |
| `messages.failed` | integer | Messages that failed processing |
| `lastActivity` | string | ISO 8601 timestamp of the last message (success or failure) |
| `lastSuccess` | string | ISO 8601 timestamp of the last successful message |
| `lastError` | string \| null | ISO 8601 timestamp of the last error, or `null` if no errors occurred |
| `uptime` | string | Time the route has been running (`HH:MM:SS`) |
| `connectedSince` | string | ISO 8601 timestamp of when the route established its current connection |
| `timestamp` | string | ISO 8601 timestamp of when this status update was generated |
### Checking Route Status
You can monitor any route's health by subscribing to its status topic. Use MQTT Explorer or any MQTT client to subscribe:
```
$SYS/Coreflux/Routes/+/status
```
This wildcard subscription shows status updates for **all** active routes at once.
***
## Use Cases
Bridge local edge data to cloud brokers:
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
DEFINE ROUTE EdgeToCloud WITH TYPE MQTT_BRIDGE
ADD SOURCE_CONFIG
WITH BROKER SELF
ADD DESTINATION_CONFIG
WITH BROKER_ADDRESS "iot.cloud-provider.com"
WITH BROKER_PORT '8883'
WITH USE_TLS "true"
ADD MAPPING allSensors
WITH SOURCE_TOPIC "local/sensors/#"
WITH DESTINATION_TOPIC "edge-01/sensors/#"
WITH DIRECTION "out"
```
Read data from PLCs via multiple protocols:
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
DEFINE ROUTE SiemensPLC WITH TYPE SIEMENS_S7
ADD S7_CONFIG
WITH IP "192.168.1.100"
WITH CPU_TYPE "S71500"
WITH RACK '0'
WITH SLOT '1'
ADD MAPPING ProcessData
WITH EVERY 500 MILLISECONDS
ADD TAG Temperature
WITH ADDRESS "DB1.DBD0"
WITH DATA_TYPE "REAL"
WITH SOURCE_TOPIC "plc/temperature"
```
Store sensor data for analysis:
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
DEFINE ROUTE AnalyticsStore WITH TYPE OPENSEARCH
ADD OPENSEARCH_CONFIG
WITH BASE_URL "https://analytics.example.com:9200"
WITH USERNAME "admin"
WITH PASSWORD "password"
WITH USE_SSL "true"
ADD EVENT IndexReading
WITH SOURCE_TOPIC "sensors/#"
WITH QUERY "CLEAN:{index: sensor-data, body: {timestamp: {timestamp}, value: {value.json}}}"
```
Email stakeholders when critical events occur:
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
DEFINE ROUTE CriticalAlerts WITH TYPE EMAIL
ADD SMTP_CONFIG
WITH HOST "smtp.office365.com"
WITH PORT '587'
WITH USE_TLS "true"
WITH USERNAME "alerts@company.com"
WITH PASSWORD "password"
ADD EVENT notify
WITH SOURCE_TOPIC "alarms/critical/+"
WITH SUBJECT "CRITICAL: {value.json.alarm_type}"
WITH RECIPIENT "ops-team@company.com"
```
## Next Steps
Connect to PLCs and industrial equipment.
Store MQTT data in databases.
# REST API Routes
Source: https://docs.coreflux.org/latest/lot-language/routes/rest-api-routes
Connect to external REST APIs or expose HTTP endpoints from your broker with client/server modes
## REST API Overview
The `REST_API` route enables HTTP communication in both directions:
* **Client Mode** - Make HTTP requests to external APIs triggered by MQTT messages
* **Server Mode** - Expose HTTP endpoints that accept data and publish it to MQTT topics
REST API routes are ideal for integrating with web services, webhooks, cloud APIs, and creating HTTP-to-MQTT bridges.
## Basic Syntax
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
DEFINE ROUTE APIConnector WITH TYPE REST_API
ADD REST_API_CONFIG
WITH BASE_ADDRESS "https://api.example.com"
WITH ENABLE_CLIENT "true"
WITH USE_SSL "true"
ADD EVENT GetData
WITH SOURCE_TOPIC "api/requests/data"
WITH DESTINATION_TOPIC "api/responses/data"
WITH METHOD "GET"
WITH ENDPOINT "/v1/data"
```
***
## How REST API Routes Work
A REST API route bridges the HTTP and MQTT worlds. It contains **events** that define what happens when data flows through—either from MQTT to HTTP (client mode) or from HTTP to MQTT (server mode).
```mermaid theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
flowchart LR
subgraph ClientMode
A[MQTT Message] -->|triggers| B[HTTP Request]
B -->|response| C[MQTT Topic]
end
subgraph ServerMode
D[HTTP Request] -->|triggers| E[MQTT Topic]
end
```
| Component | Purpose |
| ------------------- | --------------------------------------------------------------------- |
| `REST_API_CONFIG` | Connection settings—base URL, authentication, SSL, timeouts |
| `EVENT` | Defines a single HTTP operation with its trigger, method, and routing |
| `SOURCE_TOPIC` | MQTT topic that triggers the HTTP request (client mode) |
| `DESTINATION_TOPIC` | MQTT topic where results are published |
| `ENDPOINT` | The HTTP path to call (can include placeholders) |
***
## REST API Configuration
### REST\_API\_CONFIG Parameters
Base URL for HTTP requests (e.g., `https://api.example.com`).
Enable HTTP client functionality. Default: false.
Request timeout in seconds. Default: 30.
Default content type. Default: application/json.
Enable HTTP server functionality. Default: false.
HTTP server port. Default: 8080.
Basic authentication username.
Basic authentication password.
Enable SSL/TLS. Default: false.
Ignore certificate validation errors. Default: false.
Enable CORS headers. Default: false.
Allowed CORS origins. Default: \* (all).
***
## Client Mode
Use client mode when your broker needs to **call external APIs**. An MQTT message triggers the HTTP request, and the response is published back to MQTT.
```mermaid theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
flowchart LR
A["MQTT: weather/request {city: 'London'}"] -->|triggers| B[REST API Route]
B -->|GET /weather?q=London| C[External API]
C -->|JSON response| B
B -->|publishes| D["MQTT: weather/response {temp: 15, ...}"]
```
### Event Configuration
MQTT topic that triggers the HTTP request.
MQTT topic to publish HTTP response.
HTTP method: `GET`, `POST`, `PUT`, `DELETE`, `PATCH`.
API endpoint path (supports placeholders).
Request body for POST/PUT (supports placeholders).
### Placeholder Support
Use placeholders in endpoints and bodies to insert dynamic values from the MQTT message:
| Placeholder | Description |
| -------------------- | -------------------------------- |
| `{payload}` | Full MQTT payload as-is |
| `{value.json}` | Parsed JSON payload |
| `{value.json.field}` | Specific field from JSON payload |
| `{source_topic}` | Original MQTT topic |
| `{timestamp}` | Message timestamp |
### Client Examples
Fetch data from an external API when an MQTT message arrives:
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
DEFINE ROUTE WeatherAPI WITH TYPE REST_API
ADD REST_API_CONFIG
WITH BASE_ADDRESS "https://api.openweathermap.org"
WITH ENABLE_CLIENT "true"
WITH USE_SSL "true"
WITH REQUEST_TIMEOUT '30'
ADD EVENT GetWeather
WITH SOURCE_TOPIC "weather/request"
WITH DESTINATION_TOPIC "weather/response"
WITH METHOD "GET"
WITH ENDPOINT "/data/2.5/weather?q={value.json.city}&appid={value.json.apikey}"
```
When a message like `{"city": "London", "apikey": "abc123"}` arrives at `weather/request`, the route calls the API and publishes the weather data to `weather/response`.
Send data to an external API:
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
DEFINE ROUTE NotificationAPI WITH TYPE REST_API
ADD REST_API_CONFIG
WITH BASE_ADDRESS "https://api.notification-service.com"
WITH ENABLE_CLIENT "true"
WITH USE_SSL "true"
ADD EVENT SendNotification
WITH SOURCE_TOPIC "notifications/send"
WITH DESTINATION_TOPIC "notifications/result"
WITH METHOD "POST"
WITH ENDPOINT "/v1/notify"
WITH HEADERS
ADD "Content-Type" "application/json"
ADD "Authorization" "Bearer {value.json.api_key}"
WITH BODY "{payload}"
```
The entire MQTT payload is forwarded as the HTTP request body using `{payload}`.
Use basic authentication for protected APIs:
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
DEFINE ROUTE AuthenticatedAPI WITH TYPE REST_API
ADD REST_API_CONFIG
WITH BASE_ADDRESS "https://secure-api.example.com"
WITH ENABLE_CLIENT "true"
WITH USERNAME "api_user"
WITH PASSWORD "api_secret"
WITH USE_SSL "true"
ADD EVENT SecureRequest
WITH SOURCE_TOPIC "secure/request"
WITH DESTINATION_TOPIC "secure/response"
WITH METHOD "GET"
WITH ENDPOINT "/protected/data"
```
The username and password are sent as HTTP Basic Authentication headers automatically.
***
## Server Mode
Use server mode when external systems need to **send data to your broker via HTTP**. The broker exposes an HTTP endpoint that accepts requests and publishes the data to MQTT topics.
**Common use case:** Your web application, mobile app, or third-party service needs to push data into the MQTT broker without implementing an MQTT client.
### Why Use Server Mode?
| Scenario | How Server Mode Helps |
| ----------------------- | ---------------------------------------------------------- |
| Web dashboards | Submit commands or data via simple HTTP POST |
| Mobile apps | Send sensor data without MQTT libraries |
| Webhooks | Receive callbacks from services like GitHub, Stripe, Slack |
| Legacy systems | Integrate systems that only support HTTP |
| Firewalled environments | Accept data through standard HTTP ports |
### Server Event Configuration
The HTTP path to expose (e.g., `/api/sensors`).
HTTP method to accept: `GET`, `POST`, `PUT`, `DELETE`.
MQTT topic where incoming HTTP data is published.
### Server Examples
Expose an endpoint for devices to submit readings via HTTP:
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
DEFINE ROUTE SensorHTTPBridge WITH TYPE REST_API
ADD REST_API_CONFIG
WITH ENABLE_SERVER "true"
WITH SERVER_PORT '8080'
WITH ENABLE_CORS "true"
ADD EVENT ReceiveSensorData
WITH ENDPOINT "/api/sensors"
WITH METHOD "POST"
WITH DESTINATION_TOPIC "sensors/from/http"
```
**Usage:** Send sensor data with a simple HTTP POST:
```bash wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
curl -X POST http://broker-ip:8080/api/sensors \
-H "Content-Type: application/json" \
-d '{"sensor_id": "temp-01", "value": 25.5}'
```
The data is published to `sensors/from/http` and available to all MQTT subscribers.
Expose different endpoints for different data types:
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
DEFINE ROUTE MultiEndpointAPI WITH TYPE REST_API
ADD REST_API_CONFIG
WITH ENABLE_SERVER "true"
WITH SERVER_PORT '8080'
WITH ENABLE_CORS "true"
ADD EVENT ReceiveTemperature
WITH ENDPOINT "/api/temperature"
WITH METHOD "POST"
WITH DESTINATION_TOPIC "sensors/temperature/http"
ADD EVENT ReceivePressure
WITH ENDPOINT "/api/pressure"
WITH METHOD "POST"
WITH DESTINATION_TOPIC "sensors/pressure/http"
ADD EVENT ReceiveCommands
WITH ENDPOINT "/api/commands"
WITH METHOD "POST"
WITH DESTINATION_TOPIC "commands/from/http"
```
Each endpoint routes to a different MQTT topic for organized data handling.
Accept webhooks from external services:
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
DEFINE ROUTE WebhookReceiver WITH TYPE REST_API
ADD REST_API_CONFIG
WITH ENABLE_SERVER "true"
WITH SERVER_PORT '9000'
ADD EVENT GitHubWebhook
WITH ENDPOINT "/webhooks/github"
WITH METHOD "POST"
WITH DESTINATION_TOPIC "webhooks/github/events"
ADD EVENT StripeWebhook
WITH ENDPOINT "/webhooks/stripe"
WITH METHOD "POST"
WITH DESTINATION_TOPIC "webhooks/stripe/events"
```
Configure GitHub or Stripe to send webhooks to `http://your-broker:9000/webhooks/github`. Events appear on the corresponding MQTT topics.
Enable cross-origin requests for browser-based applications:
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
DEFINE ROUTE WebAppAPI WITH TYPE REST_API
ADD REST_API_CONFIG
WITH ENABLE_SERVER "true"
WITH SERVER_PORT '3000'
WITH ENABLE_CORS "true"
WITH CORS_ORIGINS "https://app.example.com, https://admin.example.com"
ADD EVENT SubmitData
WITH ENDPOINT "/api/submit"
WITH METHOD "POST"
WITH DESTINATION_TOPIC "webapp/submissions"
```
Only requests from the specified origins are allowed. Use `"*"` for development (not recommended for production).
***
## Combined Client and Server
A single route can operate in both modes—accepting HTTP requests AND calling external APIs:
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
DEFINE ROUTE BidirectionalHTTP WITH TYPE REST_API
ADD REST_API_CONFIG
WITH BASE_ADDRESS "https://external-api.com"
WITH ENABLE_CLIENT "true"
WITH ENABLE_SERVER "true"
WITH SERVER_PORT '8080'
WITH USE_SSL "true"
WITH ENABLE_CORS "true"
// Server: Accept HTTP, publish to MQTT
ADD EVENT ReceiveFromHTTP
WITH ENDPOINT "/api/receive"
WITH METHOD "POST"
WITH DESTINATION_TOPIC "http/incoming"
// Client: MQTT triggers external HTTP call
ADD EVENT CallExternalAPI
WITH SOURCE_TOPIC "http/outgoing"
WITH DESTINATION_TOPIC "http/response"
WITH METHOD "POST"
WITH ENDPOINT "/api/data"
```
***
## Complete Examples
Full API for a web dashboard to read sensor data and send commands:
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
DEFINE ROUTE DashboardAPI WITH TYPE REST_API
ADD REST_API_CONFIG
WITH ENABLE_SERVER "true"
WITH SERVER_PORT '8080'
WITH ENABLE_CORS "true"
WITH DEFAULT_CONTENT_TYPE "application/json"
ADD EVENT PostSensorData
WITH ENDPOINT "/api/sensors"
WITH METHOD "POST"
WITH DESTINATION_TOPIC "dashboard/sensors/incoming"
ADD EVENT PostCommand
WITH ENDPOINT "/api/commands"
WITH METHOD "POST"
WITH DESTINATION_TOPIC "commands/from/dashboard"
ADD EVENT PostAlert
WITH ENDPOINT "/api/alerts"
WITH METHOD "POST"
WITH DESTINATION_TOPIC "alerts/from/dashboard"
```
Integrate with an external CRM or ERP system:
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
DEFINE ROUTE CRMIntegration WITH TYPE REST_API
ADD REST_API_CONFIG
WITH BASE_ADDRESS "https://api.crm-system.com"
WITH ENABLE_CLIENT "true"
WITH USE_SSL "true"
WITH REQUEST_TIMEOUT '60'
ADD EVENT CreateRecord
WITH SOURCE_TOPIC "crm/create"
WITH DESTINATION_TOPIC "crm/created"
WITH METHOD "POST"
WITH ENDPOINT "/v2/records"
WITH HEADERS
ADD "Authorization" "Bearer {value.json.token}"
ADD "Content-Type" "application/json"
WITH BODY "{value.json.data}"
ADD EVENT UpdateRecord
WITH SOURCE_TOPIC "crm/update"
WITH DESTINATION_TOPIC "crm/updated"
WITH METHOD "PUT"
WITH ENDPOINT "/v2/records/{value.json.id}"
WITH BODY "{value.json.data}"
ADD EVENT DeleteRecord
WITH SOURCE_TOPIC "crm/delete"
WITH DESTINATION_TOPIC "crm/deleted"
WITH METHOD "DELETE"
WITH ENDPOINT "/v2/records/{value.json.id}"
```
***
## Troubleshooting
* Verify BASE\_ADDRESS is correct
* Check firewall allows outbound connections
* Increase REQUEST\_TIMEOUT if API is slow
* Verify network connectivity
* Ensure USE\_SSL matches the API requirements
* For self-signed certificates, set IGNORE\_CERT\_ERRORS (not recommended for production)
* Verify certificate chain is complete
* Verify USERNAME and PASSWORD are correct
* Check Authorization header format
* Ensure API key is valid and not expired
* Enable CORS on the server: `WITH ENABLE_CORS "true"`
* Configure specific allowed origins
* Check browser console for specific CORS error messages
* Verify DESTINATION\_TOPIC is configured
* Check MQTT broker connectivity
* Monitor broker logs for errors
***
## Best Practices
Always use SSL/TLS for external API connections:
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
WITH USE_SSL "true"
```
Configure timeouts based on expected API response times:
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
WITH REQUEST_TIMEOUT '30'
```
Subscribe to response topics to monitor API call results and handle failures in your Actions.
When exposing HTTP server endpoints:
* Use authentication where needed
* Limit CORS origins to specific domains
* Consider rate limiting at the network level
Configure different base URLs for development and production environments.
***
## Next Steps
Stream MQTT messages through Kafka.
See complete route configuration examples.
# Route Overload Configuration
Source: https://docs.coreflux.org/latest/lot-language/routes/route-config
Choose wait or drop with ADD ROUTE_CONFIG when a LoT route cannot keep up with incoming MQTT
## When MQTT outruns the destination
Incoming MQTT can outpace a database insert, a cloud bridge, or a PLC write. `ADD ROUTE_CONFIG` lets **each route** choose whether to wait or to drop the newest message — without crowding out every other route.
**Like a ticket window with a limited line.** People already being served always finish. When the line is full you can wait for a spot, wait a short time then turn the newest arrival away, or turn them away immediately.
### When to use it
* Leave the default (`balanced`) on most routes.
* Use `lossless` when a path must not lose messages (audit, compliance) and you accept that the publisher may wait.
* Use `realtime` when high-rate telemetry must never stall the client that published it.
* Set `OVERLOAD_CAPACITY` to cap one noisy route so it cannot crowd out others.
`ADD ROUTE_CONFIG` is the only LoT block that works on **every** route type. Place it next to that type's own config (`SQL_CONFIG`, `SOURCE_CONFIG`, `MODBUS_CONFIG`, and so on). Order among `ADD` blocks does not matter.
***
Let's look at how to set it, starting with the three common contracts.
## Quick Start
Use `lossless` on a forwarding path that must keep every message. The publisher waits until this route has room — it never drops.
```lot wrap focus={3-4} theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
DEFINE ROUTE CloudBridge WITH TYPE MQTT_BRIDGE
ADD ROUTE_CONFIG
WITH OVERLOAD_POLICY "lossless"
ADD SOURCE_CONFIG
WITH BROKER SELF
ADD DESTINATION_CONFIG
WITH BROKER_ADDRESS "broker.example.com"
WITH BROKER_PORT '8883'
WITH USE_TLS "true"
ADD MAPPING auditForward
WITH SOURCE_TOPIC "audit/#"
WITH DESTINATION_TOPIC "cloud/audit/#"
WITH DIRECTION "out"
```
Use `realtime` on high-rate telemetry. If the route is full, the arriving message is dropped immediately and the publishing client is not delayed.
```lot wrap focus={3-4} theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
DEFINE ROUTE SensorDB WITH TYPE POSTGRESQL
ADD ROUTE_CONFIG
WITH OVERLOAD_POLICY "realtime"
ADD SQL_CONFIG
WITH SERVER GET ENV "PG_HOST"
WITH DATABASE "factory"
WITH USERNAME GET ENV "PG_USER"
WITH PASSWORD GET SECRET "PG_PASSWORD"
ADD EVENT StoreReading
WITH SOURCE_TOPIC "sensors/+/reading"
WITH QUERY "INSERT INTO readings (ts, value) VALUES (NOW(), '{value.json}')"
```
Cap how many of **this route's** messages may be in flight. Extra messages from this route drop even while other routes still have room.
```lot wrap focus={3-4} theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
DEFINE ROUTE NoisySensors WITH TYPE MQTT_BRIDGE
ADD ROUTE_CONFIG
WITH OVERLOAD_CAPACITY 5000
ADD SOURCE_CONFIG
WITH BROKER SELF
ADD DESTINATION_CONFIG
WITH BROKER_ADDRESS "telemetry.example.com"
WITH BROKER_PORT '1883'
ADD MAPPING sensorsOut
WITH SOURCE_TOPIC "sensors/#"
WITH DESTINATION_TOPIC "remote/sensors/#"
WITH DIRECTION "out"
```
***
## Settings
All three settings are optional. An absent `ADD ROUTE_CONFIG` block inherits the broker default entirely.
```lot wrap focus={3-6} theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
DEFINE ROUTE CloudBridge WITH TYPE MQTT_BRIDGE
ADD ROUTE_CONFIG
WITH OVERLOAD_POLICY "balanced"
WITH OVERLOAD_TIMEOUT 200
WITH OVERLOAD_CAPACITY 20000
ADD SOURCE_CONFIG
WITH BROKER SELF
ADD DESTINATION_CONFIG
WITH BROKER_ADDRESS "broker.example.com"
WITH BROKER_PORT '8883'
WITH USE_TLS "true"
ADD MAPPING sensorSync
WITH SOURCE_TOPIC "sensors/#"
WITH DESTINATION_TOPIC "cloud/sensors/#"
WITH DIRECTION "out"
```
| Setting | Required | Default | Description |
| ------------------- | -------- | ---------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `OVERLOAD_POLICY` | No | `balanced` | `lossless` waits until there is room and never drops. `balanced` waits up to `OVERLOAD_TIMEOUT`, then drops the **arriving** message. `realtime` never waits — it drops immediately when the route is full. |
| `OVERLOAD_TIMEOUT` | No | `50` (ms) | Wait budget for `balanced` only. Ignored for `lossless` (waits indefinitely) and `realtime` (never waits). Must be zero or greater. |
| `OVERLOAD_CAPACITY` | No | unbounded | Maximum number of **this route's** messages allowed in flight at once. Once reached, further messages from this route drop even if other routes still have room. Must be greater than zero. |
A message already accepted for a route is never evicted. Only the arriving message can be dropped. That keeps sequences such as Sparkplug birth then data self-consistent under load.
Do not set `OVERLOAD_CAPACITY` together with `OVERLOAD_POLICY "lossless"`. A capacity cap drops overflow; `lossless` never drops. The broker logs an error naming the route, **ignores the capacity**, and runs the route as lossless. The same conflict applies if the route inherits `lossless` from the broker default and then declares a capacity.
***
## Defaults
Each setting resolves on its own:
1. **No `ADD ROUTE_CONFIG` block** — the route inherits the broker default for policy, timeout, and capacity.
2. **A block is present but a setting is omitted** — that setting does not copy the broker's timeout or capacity. `OVERLOAD_POLICY` inherits the broker default. `OVERLOAD_TIMEOUT` and `OVERLOAD_CAPACITY` use that policy's own default (`50` ms for `balanced`, unbounded capacity).
3. **An invalid value** (unknown policy name, negative timeout, non-positive capacity) does not stop the route loading. The broker logs an error and uses that field's default.
Broker-wide defaults are environment variables, read once at process start. Change them and **restart** the broker. A route's own `ADD ROUTE_CONFIG` always overrides these for that route.
| Variable | Default | Effect |
| ---------------------------------------- | ---------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `COREFLUX_ROUTE_OVERLOAD_POLICY` | `balanced` | Default for `OVERLOAD_POLICY` (`lossless`, `balanced`, or `realtime`). An unrecognized value logs an error and falls back to `balanced`. |
| `COREFLUX_ROUTE_CHANNEL_FULL_TIMEOUT_MS` | `50` | Default wait budget in milliseconds for `balanced`. Ignored when the resolved policy is `lossless` or `realtime`. A negative value logs an error and falls back to `50`. |
| `COREFLUX_ROUTE_CHANNEL_CAPACITY` | `100000` | Shared ceiling on how many route updates the broker will hold at once. Below the ceiling, matching messages are accepted. At the ceiling, the arriving message's wait-or-drop policy applies. Values below `1024` are raised to `1024`. Unset, non-numeric, zero, or negative values fall back to `100000`. |
`OVERLOAD_CAPACITY` on a route is a **private** budget for that route. The shared ceiling is separate: one route can hit its own cap and start dropping while other routes still have room.
See [Broker Configuration](/latest/mqtt-broker/configuration#performance-and-back-pressure) for how these sit alongside other broker limits.
***
## What happens under load
When a route is waiting, that wait can delay **the publishing client's own publish** — only that client, and only up to the wait budget. Other connected clients are not stalled by this route.
The broker logs two different saturation conditions (each at most once every five seconds):
* The shared route queue is full — every route waiting behind it is affected.
* A single route is over the `OVERLOAD_CAPACITY` it declared for itself — the shared queue may still have room.
If two routes could match the same topic (for example one exact mapping and another `sensors/#`), the broker applies the **stricter** wait-or-drop choice among those candidates. A `lossless` route on an overlapping pattern can make a `realtime` route wait, even if only the `realtime` route would have handled the message. The broker never drops a message because a more lenient route asked to drop it. If mixed policies on overlapping wildcards wait or drop more than you expect, check the patterns first — that behavior is by design.
***
## Troubleshooting
That route's own `OVERLOAD_CAPACITY` is exhausted. Look for the broker warning that a route is over its in-flight budget, not the warning that the shared queue is full.
Working as designed — `lossless` waits until there is room. Switch to `balanced` with a timeout if you would rather drop than stall, or cap a noisy route with `OVERLOAD_CAPACITY` so it drops its own overflow instead.
It is declared alongside `OVERLOAD_POLICY "lossless"` (or the broker default is `lossless`). Check the broker log for the lossless/capacity conflict — capacity is ignored, not applied.
Another route's overlapping wildcard pattern resolved a stricter policy for that topic. See the overlapping-pattern warning above.
The resolved policy is `realtime`. That is expected — `realtime` drops without waiting.
***
## Best practices
* Start with the default `balanced` policy. Change a route only when you have a reason (must not lose, must not stall, or must not crowd others).
* Use `lossless` sparingly. It protects delivery at the cost of delaying the publisher that caused the backlog.
* Put `OVERLOAD_CAPACITY` on the noisy route, not on the critical one — the critical route can stay unbounded while the noisy route drops its own overflow.
* Prefer exact topic mappings when two routes would otherwise share a broad `+` / `#` pattern with different policies.
* Invalid settings never fail the route silently: read the broker log after deploy if behavior does not match the block you wrote.
***
## Next Steps
Define routes, mappings, and status topics.
Set broker-wide route overload defaults and other limits.
# Kafka Route
Source: https://docs.coreflux.org/latest/lot-language/routes/streaming/kafka
Stream data both ways between your Coreflux broker and Apache Kafka clusters, including Confluent Cloud, AWS MSK, Aiven, and self-managed brokers
## Kafka Overview
The `KAFKA` route connects your Coreflux broker to an Apache Kafka cluster and moves messages in both directions. A single route can **produce** (MQTT → Kafka) and **consume** (Kafka → MQTT) at the same time, so device telemetry can flow into your data pipeline while commands and setpoints flow back to the field.
**Like a conveyor belt between two warehouses.** Your MQTT broker handles fast-moving device traffic; Kafka is the long-term, replayable record. The Kafka route is the belt that carries boxes (messages) between them in whichever direction you point it.
### When to use it
* Feed real-time MQTT telemetry into a Kafka-based analytics or data pipeline.
* Bring Kafka events (commands, setpoints, alarms) back into the broker for field devices.
* Bridge edge MQTT traffic to a managed cloud platform like Confluent Cloud or AWS MSK.
### Key capabilities
| Capability | What it means for you |
| ----------------------- | ------------------------------------------------------------------------------------------ |
| **Bidirectional** | Produce and consume in one route — no separate definitions needed |
| **Secure** | Supports `PLAINTEXT`, `SSL`, `SASL_PLAINTEXT`, and `SASL_SSL` |
| **Reliable delivery** | Per-message offset commit on the consume path (at-least-once) |
| **Message keys** | Route records to partitions by topic segment or JSON field for ordered, per-device streams |
| **Tunable throughput** | Configurable batching, compression, and acknowledgement levels |
| **Cloud & self-hosted** | Confluent Cloud, AWS MSK, Aiven, Redpanda, and self-managed clusters |
***
Let's look at how a Kafka route is defined, starting with the three common directions.
## Quick Start
Send every sensor reading from the broker into a Kafka topic, keyed by device:
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
DEFINE ROUTE SensorProducer WITH TYPE KAFKA
ADD KAFKA_CONFIG
WITH BROKER_ADDRESS GET ENV "KAFKA_BOOTSTRAP"
WITH GROUP_ID "coreflux-sensors"
WITH SECURITY_PROTOCOL "PLAINTEXT"
ADD EVENT SensorsToKafka
WITH SOURCE_TOPIC "factory/sensors/+"
WITH DESTINATION_TOPIC "factory.sensors.raw"
WITH DIRECTION "out"
WITH QUERY "{topic.3}"
```
A message on `factory/sensors/pump-01` becomes a Kafka record on `factory.sensors.raw` with key `pump-01`.
Forward Kafka commands into the MQTT broker:
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
DEFINE ROUTE CommandConsumer WITH TYPE KAFKA
ADD KAFKA_CONFIG
WITH BROKER_ADDRESS GET ENV "KAFKA_BOOTSTRAP"
WITH GROUP_ID "coreflux-commands"
WITH SECURITY_PROTOCOL "PLAINTEXT"
WITH AUTO_OFFSET_RESET "Latest"
ADD EVENT CommandsFromKafka
WITH SOURCE_TOPIC "factory.commands"
WITH DESTINATION_TOPIC "factory/commands/in"
WITH DIRECTION "in"
```
A Kafka record on `factory.commands` is published to the MQTT topic `factory/commands/in`.
One route handling both directions — telemetry out, commands in:
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
DEFINE ROUTE PlantKafka WITH TYPE KAFKA
ADD KAFKA_CONFIG
WITH BROKER_ADDRESS GET ENV "KAFKA_BOOTSTRAP"
WITH GROUP_ID "coreflux-plant"
WITH SECURITY_PROTOCOL "PLAINTEXT"
WITH AUTO_OFFSET_RESET "Earliest"
ADD EVENT SensorsToKafka
WITH SOURCE_TOPIC "plant/sensors/+"
WITH DESTINATION_TOPIC "plant.sensors.raw"
WITH DIRECTION "out"
ADD EVENT CommandsFromKafka
WITH SOURCE_TOPIC "plant.commands"
WITH DESTINATION_TOPIC "plant/commands/in"
WITH DIRECTION "in"
```
The Quick Start examples use `PLAINTEXT` for clarity. For any cluster reachable over a network — especially cloud services — use `SASL_SSL` and load credentials from environment variables and secrets. See [Security & Authentication](#security--authentication).
***
## How Kafka Compares to MQTT
Kafka and MQTT solve different problems. Coreflux bridges the two so each does what it's best at: MQTT for fast device traffic, Kafka for durable, replayable streams.
| Feature | Kafka | MQTT |
| ---------------------------- | --------------------------------- | ----------------------------- |
| **Delivery model** | Pull (consumer fetches) | Push (broker delivers) |
| **Retention** | Configurable (hours to forever) | Typically none |
| **Ordering** | Per-partition | Per-topic |
| **Fan-out / load balancing** | Built-in consumer groups | Shared subscriptions |
| **Throughput** | Millions of messages/sec | Thousands of messages/sec |
| **Typical use** | Data pipelines, analytics, replay | IoT telemetry, device control |
A few Kafka terms appear throughout this page:
| Term | Description |
| --------------------- | ----------------------------------------------------------------------------------- |
| **Bootstrap servers** | The initial broker addresses the route contacts to discover the rest of the cluster |
| **Topic** | A named, partitioned log of records |
| **Partition** | An ordered sub-stream within a topic — the unit of ordering and parallelism |
| **Consumer group** | A set of consumers that divide a topic's partitions between them |
| **Offset** | A record's position within a partition, committed once it has been processed |
***
## Supported Kafka Services
The Kafka route works with any Kafka-compatible cluster, cloud-managed or self-hosted.
| Service | Security Protocol | Notes |
| -------------------------------- | ----------------- | ------------------------------------------ |
| **Confluent Cloud** | `SASL_SSL` | `PLAIN` or `SCRAM` mechanism |
| **Amazon MSK** | `SASL_SSL` | IAM or SCRAM auth |
| **Aiven for Kafka** | `SASL_SSL` | SCRAM-SHA-512 typical |
| **Azure Event Hubs (Kafka API)** | `SASL_SSL` | `PLAIN` with connection string as password |
| **Redpanda Cloud** | `SASL_SSL` | Kafka-compatible |
| Cluster | Notes |
| ----------------------- | ----------------------------------------- |
| **Apache Kafka** (2.0+) | All security protocols supported |
| **Confluent Platform** | Same configuration as Apache Kafka |
| **Redpanda** | Drop-in Kafka-compatible cluster |
| **KRaft mode** | Kafka without ZooKeeper — fully supported |
***
## Connection Configuration
Every Kafka route starts with a `KAFKA_CONFIG` block describing the cluster, security, and throughput settings. Only `BROKER_ADDRESS` is required, but production routes should always set a `GROUP_ID` and use a secure protocol.
```lot wrap focus={3-4} theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
DEFINE ROUTE PlantKafka WITH TYPE KAFKA
ADD KAFKA_CONFIG
WITH BROKER_ADDRESS GET ENV "KAFKA_BOOTSTRAP"
WITH GROUP_ID "coreflux-plant"
ADD EVENT SensorsToKafka
WITH SOURCE_TOPIC "plant/sensors/+"
WITH DESTINATION_TOPIC "plant.sensors.raw"
WITH DIRECTION "out"
```
### Connection & Security
Bootstrap servers, e.g. `localhost:9092` or `host1:9092,host2:9092`. List two or three brokers in production so the route survives an individual broker restart.
Consumer group name. **Always set this in production.** If omitted, a random group is generated on every startup, causing the consumer to replay all retained messages from scratch after each restart.
Where to start reading when no committed offset exists: `Earliest` (from the beginning) or `Latest` (new messages only).
Leave `false` for per-message commit (at-least-once delivery). Set `true` to additionally enable periodic background commits.
`PLAINTEXT`, `SSL`, `SASL_PLAINTEXT`, or `SASL_SSL`.
Required with SASL: `PLAIN`, `SCRAM-SHA-256`, or `SCRAM-SHA-512`.
SASL username. Supports `GET ENV`.
SASL password. Use `GET SECRET` to keep it out of your LoT definition.
Path to the CA certificate (for `SSL` or `SASL_SSL`).
Path to the client certificate (mutual TLS).
Path to the client key (required with a client certificate).
Key or keystore password.
Producer durability: `0` (no acknowledgement), `1` (leader only), or `all` (all in-sync replicas).
How long the producer waits to fill a batch before sending. Higher values mean fewer, larger requests (better throughput) at the cost of a little added latency.
Maximum producer batch size in bytes. Default is 1 MB.
Producer compression: `none`, `gzip`, `snappy`, `lz4`, or `zstd`. Most effective on larger batches — pair with `LINGER_MS`.
Maximum bytes returned per consumer fetch. Default is 50 MB.
Maximum KB pre-fetched per partition into the consumer buffer (64 MB by default). Lower it to cap memory use on high-volume topics.
How long the route waits for in-flight produced messages to drain when stopping.
***
## Security & Authentication
Choose the security protocol that matches your broker's listener. For anything beyond a local development cluster, use `SASL_SSL`.
| `SECURITY_PROTOCOL` | TLS | SASL | Typical use |
| ------------------- | --- | ---- | --------------------------------------- |
| `PLAINTEXT` | No | No | Local development |
| `SSL` | Yes | No | TLS-only clusters |
| `SASL_PLAINTEXT` | No | Yes | Internal test environments |
| `SASL_SSL` | Yes | Yes | Confluent Cloud, MSK, Aiven, production |
No encryption or authentication — local clusters only:
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
DEFINE ROUTE LocalKafka WITH TYPE KAFKA
ADD KAFKA_CONFIG
WITH BROKER_ADDRESS "localhost:9092"
WITH GROUP_ID "coreflux-dev"
WITH SECURITY_PROTOCOL "PLAINTEXT"
ADD EVENT SensorsToKafka
WITH SOURCE_TOPIC "dev/sensors/+"
WITH DESTINATION_TOPIC "dev.sensors"
WITH DIRECTION "out"
```
Encrypted transport without SASL credentials:
```lot wrap focus={5-6} theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
DEFINE ROUTE TlsKafka WITH TYPE KAFKA
ADD KAFKA_CONFIG
WITH BROKER_ADDRESS "kafka.example.com:9093"
WITH GROUP_ID "coreflux-tls"
WITH SECURITY_PROTOCOL "SSL"
WITH SSL_CA_LOCATION "/etc/ssl/certs/kafka-ca.pem"
ADD EVENT SensorsToKafka
WITH SOURCE_TOPIC "plant/sensors/+"
WITH DESTINATION_TOPIC "plant.sensors"
WITH DIRECTION "out"
```
Encrypted transport with SCRAM authentication — the recommended production setup:
```lot wrap focus={5-10} theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
DEFINE ROUTE SecureKafka WITH TYPE KAFKA
ADD KAFKA_CONFIG
WITH BROKER_ADDRESS GET ENV "KAFKA_BOOTSTRAP"
WITH GROUP_ID "coreflux-prod"
WITH SECURITY_PROTOCOL "SASL_SSL"
WITH SASL_MECHANISM "SCRAM-SHA-512"
WITH SASL_USERNAME GET ENV "KAFKA_USER"
WITH SASL_PASSWORD GET SECRET "KAFKA_PASSWORD"
WITH SSL_CA_LOCATION "/etc/ssl/certs/ca.pem"
ADD EVENT SensorsToKafka
WITH SOURCE_TOPIC "plant/sensors/+"
WITH DESTINATION_TOPIC "plant.sensors"
WITH DIRECTION "out"
```
Store credentials with `GET SECRET` and non-sensitive values like hostnames with `GET ENV`. See [Environment Variables and Secrets](/latest/mqtt-broker/secrets-and-env) for setup across Windows, Linux, Docker, and Kubernetes.
***
## Events and Direction
Each `ADD EVENT` block maps one MQTT topic to one Kafka topic and sets the direction of flow. Think of the **broker** as the source side and **Kafka** as the destination side.
| `DIRECTION` | `SOURCE_TOPIC` | `DESTINATION_TOPIC` | Data flow |
| ----------- | -------------------------------- | ------------------------------- | ------------ |
| `"out"` | Broker MQTT topic (wildcards OK) | Kafka topic to produce to | MQTT → Kafka |
| `"in"` | Kafka topic to subscribe to | Broker MQTT topic to publish to | Kafka → MQTT |
### Event Parameters
Broker MQTT topic when `DIRECTION` is `"out"`; Kafka topic to subscribe to when `DIRECTION` is `"in"`.
Kafka topic when `DIRECTION` is `"out"`; broker MQTT publish topic when `DIRECTION` is `"in"`.
`"out"` (MQTT → Kafka) or `"in"` (Kafka → MQTT).
The Kafka record **key** for partitioning (produce events only). See [Message Keys and Partitioning](#message-keys-and-partitioning).
Produce on a timer instead of on each incoming message.
***
## Message Keys and Partitioning
Kafka sends records with the same key to the same partition, which preserves **order per key**. On produce events, set the key with `WITH QUERY`.
* **With a key** (`WITH QUERY`): same key → same partition → ordering preserved per key (for example, all readings from one device stay in order).
* **Without a key**: records spread across all partitions for higher throughput, with no per-key ordering guarantee.
* **Consume events** join the consumer group and have partitions assigned automatically — set a stable `GROUP_ID` so offsets survive restarts.
### Key Placeholders
The `WITH QUERY` value is resolved before each record is sent. Supported placeholders:
| Placeholder | Description | Example result |
| ---------------------- | ----------------------------------------- | --------------------------------------------------- |
| `{topic.N}` | Nth segment (1-based) of the source topic | `pump-01` from `plant/sensors/pump-01/reading` |
| `{topic}` | Full source topic string | `plant/sensors/pump-01/reading` |
| `{payload}` | Message payload as a string | `42.5` |
| `{payload.json}` | Full payload parsed as JSON | — |
| `{payload.json.field}` | A JSON field from the payload | `pump-01` from `{"device_id":"pump-01","value":42}` |
| `{timestamp}` | ISO 8601 timestamp | `2026-06-12T10:30:45.123Z` |
| `{env.NAME}` | Environment variable value | value of `NAME` |
| `{secret.NAME}` | Secret value | value of `NAME` |
### Key from a topic segment
Use a wildcard position from the source topic as the key — here the device ID in the third segment:
```lot wrap focus={10} theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
DEFINE ROUTE SensorsByDevice WITH TYPE KAFKA
ADD KAFKA_CONFIG
WITH BROKER_ADDRESS GET ENV "KAFKA_BOOTSTRAP"
WITH GROUP_ID "coreflux-sensors"
WITH SECURITY_PROTOCOL "SASL_SSL"
WITH SASL_MECHANISM "SCRAM-SHA-512"
WITH SASL_USERNAME GET ENV "KAFKA_USER"
WITH SASL_PASSWORD GET SECRET "KAFKA_PASSWORD"
ADD EVENT SensorsByDevice
WITH SOURCE_TOPIC "plant/sensors/+/reading"
WITH DESTINATION_TOPIC "plant.sensors.raw"
WITH DIRECTION "out"
WITH QUERY "{topic.3}"
```
A message on `plant/sensors/pump-01/reading` gets the key `pump-01`.
### Key from a JSON field
When the identifier lives inside the payload, pull it from a JSON field instead:
```lot wrap focus={13} theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
DEFINE ROUTE SensorsByPayloadId WITH TYPE KAFKA
ADD KAFKA_CONFIG
WITH BROKER_ADDRESS GET ENV "KAFKA_BOOTSTRAP"
WITH GROUP_ID "coreflux-sensors"
WITH SECURITY_PROTOCOL "SASL_SSL"
WITH SASL_MECHANISM "SCRAM-SHA-512"
WITH SASL_USERNAME GET ENV "KAFKA_USER"
WITH SASL_PASSWORD GET SECRET "KAFKA_PASSWORD"
ADD EVENT SensorsByPayloadId
WITH SOURCE_TOPIC "plant/sensors/+/reading"
WITH DESTINATION_TOPIC "plant.sensors.raw"
WITH DIRECTION "out"
WITH QUERY "{payload.json.device_id}"
```
For a payload of `{"device_id":"pump-01","value":42}`, the record key becomes `pump-01`.
***
## Complete Examples
Full production route with TLS, SCRAM authentication, telemetry out, and alarms in:
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
DEFINE ROUTE ProductionKafka WITH TYPE KAFKA
ADD KAFKA_CONFIG
WITH BROKER_ADDRESS GET ENV "KAFKA_BOOTSTRAP"
WITH GROUP_ID "coreflux-production"
WITH SECURITY_PROTOCOL "SASL_SSL"
WITH SASL_MECHANISM "SCRAM-SHA-512"
WITH SASL_USERNAME GET ENV "KAFKA_USER"
WITH SASL_PASSWORD GET SECRET "KAFKA_PASSWORD"
WITH SSL_CA_LOCATION "/etc/ssl/certs/ca.pem"
WITH ACKS "all"
WITH AUTO_OFFSET_RESET "Earliest"
ADD EVENT TelemetryOut
WITH SOURCE_TOPIC "production/+/telemetry"
WITH DESTINATION_TOPIC "production.telemetry"
WITH DIRECTION "out"
WITH QUERY "{topic.2}"
ADD EVENT AlarmsIn
WITH SOURCE_TOPIC "production.alarms"
WITH DESTINATION_TOPIC "production/alarms/incoming"
WITH DIRECTION "in"
```
Optimized for sustained high-rate telemetry with batching and compression:
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
DEFINE ROUTE TelemetryKafka WITH TYPE KAFKA
ADD KAFKA_CONFIG
WITH BROKER_ADDRESS GET ENV "KAFKA_BOOTSTRAP"
WITH GROUP_ID "coreflux-telemetry"
WITH SECURITY_PROTOCOL "SASL_SSL"
WITH SASL_MECHANISM "SCRAM-SHA-512"
WITH SASL_USERNAME GET ENV "KAFKA_USER"
WITH SASL_PASSWORD GET SECRET "KAFKA_PASSWORD"
WITH ACKS "1"
WITH LINGER_MS "50"
WITH COMPRESSION_TYPE "lz4"
WITH FLUSH_TIMEOUT_MS "10000"
ADD EVENT DeviceTelemetry
WITH SOURCE_TOPIC "devices/+/telemetry"
WITH DESTINATION_TOPIC "iot.telemetry"
WITH DIRECTION "out"
WITH QUERY "{topic.2}"
```
`ACKS "1"` avoids waiting for every replica on non-critical telemetry, `LINGER_MS "50"` batches bursts into fewer requests, and `lz4` compression cuts bandwidth with negligible CPU cost.
Route different asset types to different Kafka topics, keyed by a JSON field:
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
DEFINE ROUTE AssetKafka WITH TYPE KAFKA
ADD KAFKA_CONFIG
WITH BROKER_ADDRESS GET ENV "KAFKA_BOOTSTRAP"
WITH GROUP_ID "coreflux-assets"
WITH SECURITY_PROTOCOL "SASL_SSL"
WITH SASL_MECHANISM "SCRAM-SHA-512"
WITH SASL_USERNAME GET ENV "KAFKA_USER"
WITH SASL_PASSWORD GET SECRET "KAFKA_PASSWORD"
ADD EVENT PumpsOut
WITH SOURCE_TOPIC "assets/pumps/+/data"
WITH DESTINATION_TOPIC "assets.pumps"
WITH DIRECTION "out"
WITH QUERY "{payload.json.asset_id}"
ADD EVENT MotorsOut
WITH SOURCE_TOPIC "assets/motors/+/data"
WITH DESTINATION_TOPIC "assets.motors"
WITH DIRECTION "out"
WITH QUERY "{payload.json.asset_id}"
ADD EVENT SetpointsIn
WITH SOURCE_TOPIC "assets.setpoints"
WITH DESTINATION_TOPIC "assets/setpoints/incoming"
WITH DIRECTION "in"
```
Connect to Confluent Cloud using SASL/PLAIN with an API key and secret:
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
DEFINE ROUTE ConfluentCloud WITH TYPE KAFKA
ADD KAFKA_CONFIG
WITH BROKER_ADDRESS GET ENV "CONFLUENT_BOOTSTRAP"
WITH GROUP_ID "coreflux-cloud"
WITH SECURITY_PROTOCOL "SASL_SSL"
WITH SASL_MECHANISM "PLAIN"
WITH SASL_USERNAME GET ENV "CONFLUENT_API_KEY"
WITH SASL_PASSWORD GET SECRET "CONFLUENT_API_SECRET"
WITH ACKS "all"
WITH AUTO_OFFSET_RESET "Latest"
ADD EVENT TelemetryToCloud
WITH SOURCE_TOPIC "factory/+/telemetry"
WITH DESTINATION_TOPIC "factory.telemetry"
WITH DIRECTION "out"
WITH QUERY "{topic.2}"
```
`CONFLUENT_BOOTSTRAP` is the `pkc-xxxx.region.confluent.cloud:9092` address from your cluster settings; the API key and secret come from the Confluent Cloud credentials page.
List multiple bootstrap brokers so the route survives a single broker restart:
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
DEFINE ROUTE HaKafka WITH TYPE KAFKA
ADD KAFKA_CONFIG
WITH BROKER_ADDRESS "kafka-1:9092,kafka-2:9092,kafka-3:9092"
WITH GROUP_ID "coreflux-ha"
WITH SECURITY_PROTOCOL "SASL_SSL"
WITH SASL_MECHANISM "SCRAM-SHA-512"
WITH SASL_USERNAME GET ENV "KAFKA_USER"
WITH SASL_PASSWORD GET SECRET "KAFKA_PASSWORD"
WITH ACKS "all"
ADD EVENT TelemetryOut
WITH SOURCE_TOPIC "factory/sensors/+"
WITH DESTINATION_TOPIC "factory.sensors"
WITH DIRECTION "out"
```
***
## Performance Tuning
A few settings control the trade-off between throughput, latency, durability, and memory. Tune them to match each stream.
### Durability vs. throughput (produce)
`ACKS` decides how many replicas must confirm each record before it counts as sent:
| `ACKS` | Behavior | Use for |
| ------ | -------------------------------------------------------------- | ------------------------------------------------ |
| `all` | Wait for all in-sync replicas (most durable, lower throughput) | Mission-critical data |
| `1` | Wait for the partition leader only | High-rate telemetry |
| `0` | Don't wait at all | Fire-and-forget metrics where loss is acceptable |
### Batching and compression (produce)
Under burst load, many produces are in flight at once and Kafka groups them into shared batches. These settings shape that behavior:
| Setting | Effect |
| ------------------------ | --------------------------------------------------------- |
| `LINGER_MS "0"` | Send immediately — lowest latency, smallest batches |
| `LINGER_MS "50"` | Wait up to 50 ms to fill a batch — fewer, larger requests |
| `COMPRESSION_TYPE "lz4"` | Compress batches — lower bandwidth at negligible CPU cost |
| `BATCH_SIZE "2097152"` | Allow 2 MB batches — more messages per request |
### Consume throughput
Each consumed message is committed individually for at-least-once delivery, which adds a short round-trip per message. For very high-rate consume topics, use one Kafka route per logical stream rather than stacking many consume events on a single route.
### Memory use (consume)
The route pre-fetches messages into a buffer while it processes. On high-volume topics with many partitions this buffer can grow large. Cap it with `QUEUED_MAX_MESSAGES_KB` (for example `"4096"` for 4 MB per partition) and bound fetch responses with `FETCH_MAX_BYTES`.
### Watch for consume backpressure
When a consume route publishes into the broker, a busy MQTT subsystem (many subscribers, large payloads) can slow the consumer down. If you pair a high-rate consume topic with many subscribers, monitor consumer group lag to catch this early.
***
## Best Practices
A fixed group ID lets offsets survive restarts. Without one, a new random group is created on every startup and the consumer replays the entire topic.
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
WITH GROUP_ID "coreflux-plant-v1"
```
Never hardcode passwords in a route definition.
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
WITH SASL_PASSWORD GET SECRET "KAFKA_PASSWORD"
```
Reserve `PLAINTEXT` for local development; encrypt and authenticate everything else.
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
WITH SECURITY_PROTOCOL "SASL_SSL"
WITH SASL_MECHANISM "SCRAM-SHA-512"
```
Choose where a brand-new consumer starts reading.
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
WITH AUTO_OFFSET_RESET "Earliest" // replay history
WITH AUTO_OFFSET_RESET "Latest" // new messages only
```
Use a message key so all records from one device land on the same partition, in order.
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
WITH QUERY "{topic.2}"
```
A single broker address is a single point of failure; list several for resilience.
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
WITH BROKER_ADDRESS "kafka-1:9092,kafka-2:9092,kafka-3:9092"
```
Use your Kafka management UI (such as Confluent Control Center or Redpanda Console) or `kafka-consumer-groups --describe --group ` to watch for growing lag, which signals publish backpressure.
***
## Troubleshooting
Verify a route's connection at any time by publishing the following to `$SYS/Coreflux/Command` from any MQTT client:
```
-checkRouteConnection PlantKafka
```
The route validates the cluster connection and, when consume events are present, the consumer group join — catching configuration problems early.
* Confirm the bootstrap address and port are correct.
* Make sure `SECURITY_PROTOCOL` matches the broker's listener.
* Check that the port (default `9092`) isn't blocked by a firewall.
* Verify network reachability to the broker host.
The bootstrap connection can succeed while the cluster's advertised addresses point somewhere unreachable. This is common when Kafka runs in Docker. Make sure the broker advertises an address the Coreflux host can actually reach:
| Where Coreflux runs | `BROKER_ADDRESS` |
| ---------------------------- | ---------------- |
| On the host | `localhost:9092` |
| Same Docker network as Kafka | `kafka:9092` |
* Confirm `SECURITY_PROTOCOL` matches the listener (e.g. `SASL_SSL`, not `SASL_PLAINTEXT`).
* Make sure `SASL_MECHANISM` matches what the broker requires.
* Check that credentials are correct and the env/secret names resolve.
* Set `AUTO_OFFSET_RESET "Earliest"` if messages were produced before the consumer first started.
* Set a stable `GROUP_ID` so offsets are remembered across restarts.
* Give each route its own `GROUP_ID` unless you intentionally want them to share partitions.
| Symptom | Likely cause | Fix |
| ----------------------------- | ---------------------------------- | ------------------------------------------------- |
| Produce slower than expected | `ACKS "all"` on a high-rate stream | Use `ACKS "1"` for telemetry |
| High bandwidth usage | No compression | Add `COMPRESSION_TYPE "lz4"` and `LINGER_MS "50"` |
| High memory on consume routes | Large prefetch buffer | Set `QUEUED_MAX_MESSAGES_KB "4096"` |
| Growing consumer lag | MQTT publish backpressure | Reduce subscriber load on the destination topics |
| Messages lost on shutdown | Flush timeout too short | Raise `FLUSH_TIMEOUT_MS` |
***
## Next Steps
Configure MQTT bridges and Sparkplug roles.
Persist MQTT data in databases for analytics.
# System Routes
Source: https://docs.coreflux.org/latest/lot-language/routes/system-routes
Configure MQTT bridges and Sparkplug roles with Coreflux System Routes
## Build the broker topology around your data
System Routes define how your Coreflux broker participates in a wider MQTT system. Use them to bridge selected topics to another broker, or make Coreflux consume and produce Sparkplug B traffic.
**Think of System Routes as the roads around the broker.** Bridges connect MQTT brokers, and Sparkplug routes define how industrial MQTT traffic enters and leaves the system.
For **high-availability Coreflux clustering** (multi-node broker failover), see [High-Availability Clustering](/latest/mqtt-broker/clustering). That feature is broker configuration, not a System Route.
### When to use System Routes
Use this category when you need to:
* synchronize topics with another MQTT broker;
* decode Sparkplug B payloads into readable JSON;
* publish as a Sparkplug edge node; or
* make Coreflux the Sparkplug Primary Host Application.
## Choose a System Route
Synchronize selected MQTT topics with another broker.
Decode Sparkplug traffic or publish Coreflux metrics as an edge node.
Make Coreflux the Primary Host for a Sparkplug namespace.
***
Choose the route by the role Coreflux needs to play.
## Compare System Routes
| Route | Coreflux role | Connects to | Best for |
| ---------------- | ------------------------ | -------------------------- | --------------------------------------------------------------- |
| `MQTT_BRIDGE` | MQTT bridge | Another MQTT broker | Topic synchronization and edge-to-cloud links |
| `SPARKPLUG_B` | Decoder and/or edge node | A Sparkplug MQTT namespace | Translating Sparkplug to JSON or publishing Sparkplug telemetry |
| `SPARKPLUG_HOST` | Primary Host Application | Sparkplug edge nodes | Namespace authority, commands, and automatic rebirth requests |
A `SPARKPLUG_HOST` route and a `SPARKPLUG_B` route in `DECODE` or `BOTH` mode cannot run together on the same broker. Both consume the complete `spBv1.0/#` namespace. You can combine one Host route with separate `ENCODE`-only routes.
***
## Next Steps
Compare decode, edge-node, and Primary Host responsibilities.
Run multiple Coreflux brokers as a fault-tolerant cluster.
# MQTT Bridge Route
Source: https://docs.coreflux.org/latest/lot-language/routes/system/mqtt-bridge
Connect two MQTT brokers for edge-to-cloud synchronization, multi-site connectivity, and backup systems
## MQTT Bridge Overview
The `MQTT_BRIDGE` route connects your Coreflux broker to another MQTT broker and keeps selected topics in sync between them. Use it for edge-to-cloud synchronization, linking multiple sites, or mirroring data to a backup broker.
**Like call forwarding for messages.** You decide which topics get forwarded and in which direction, and the bridge relays every matching message between the two brokers automatically.
### When to use it
* Push local sensor data from an edge device up to a cloud broker.
* Pull commands or configuration from a central broker down to remote sites.
* Keep two brokers continuously in sync for redundancy.
***
Let's look at how to define a bridge, starting with the most common scenarios.
## Quick Start
Forward local sensor data up to a cloud broker over TLS:
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
DEFINE ROUTE EdgeToCloud WITH TYPE MQTT_BRIDGE
ADD SOURCE_CONFIG
WITH BROKER SELF
ADD DESTINATION_CONFIG
WITH BROKER_ADDRESS "iot.cloud-provider.com"
WITH BROKER_PORT '8883'
WITH CLIENT_ID "EdgeDevice001"
WITH USERNAME "edge_user"
WITH PASSWORD "secure_password"
WITH USE_TLS "true"
ADD MAPPING sensorData
WITH SOURCE_TOPIC "sensors/+/data"
WITH DESTINATION_TOPIC "edge-001/sensors/+/data"
WITH DIRECTION "out"
```
Pull commands from a cloud broker down to the local broker:
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
DEFINE ROUTE CloudCommands WITH TYPE MQTT_BRIDGE
ADD SOURCE_CONFIG
WITH BROKER SELF
ADD DESTINATION_CONFIG
WITH BROKER_ADDRESS "iot.cloud-provider.com"
WITH BROKER_PORT '8883'
WITH CLIENT_ID "EdgeDevice001-Commands"
WITH USE_TLS "true"
ADD MAPPING commands
WITH SOURCE_TOPIC "local/commands/#"
WITH DESTINATION_TOPIC "edge-001/commands/#"
WITH DIRECTION "in"
```
Keep two brokers fully in sync in both directions:
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
DEFINE ROUTE FullSync WITH TYPE MQTT_BRIDGE
ADD SOURCE_CONFIG
WITH BROKER SELF
ADD DESTINATION_CONFIG
WITH BROKER_ADDRESS "partner-broker.example.com"
WITH BROKER_PORT '1883'
WITH CLIENT_ID "SyncClient"
ADD MAPPING bidirectionalSync
WITH SOURCE_TOPIC "shared/data/#"
WITH DESTINATION_TOPIC "partner/data/#"
WITH DIRECTION "both"
```
***
## Connection Configuration
A bridge has two endpoints: a `SOURCE_CONFIG` (usually your local broker) and a `DESTINATION_CONFIG` (the remote broker). Use `WITH BROKER SELF` for the local Coreflux broker, or provide explicit connection details for a remote one.
IP address or hostname of the broker. Use `SELF` for the local broker.
MQTT port (typically 1883 for unencrypted, 8883 for TLS).
Unique client identifier for the bridge connection.
Authentication username.
Authentication password.
Enable TLS encryption. Default: false.
Allow untrusted certificates. Default: false.
Path to CA certificate file.
Path to client certificate for mTLS.
Client certificate password.
Number of reconnection attempts. Default: 5.
***
## Mapping Configuration
Each `ADD MAPPING` block defines which topics are bridged and in which direction.
Topic pattern to subscribe to on the source broker. Supports wildcards.
Topic to publish to on the destination broker.
Data flow direction: `out` (source→dest), `in` (dest→source), or `both`.
***
## Secure mTLS Bridge
For production links between sites, encrypt both endpoints and authenticate with mutual TLS:
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
DEFINE ROUTE SecureBridge WITH TYPE MQTT_BRIDGE
ADD SOURCE_CONFIG
WITH BROKER_ADDRESS "192.168.1.10"
WITH BROKER_PORT '8883'
WITH CLIENT_ID "SecureSource"
WITH USE_TLS "true"
WITH SERVER_CA_CERT_PATH "/certs/ca.pem"
WITH CLIENT_CERT_PATH "/certs/client.pem"
WITH CLIENT_CERT_PASS "cert_password"
ADD DESTINATION_CONFIG
WITH BROKER_ADDRESS "secure.cloud.com"
WITH BROKER_PORT '8883'
WITH CLIENT_ID "SecureDest"
WITH USE_TLS "true"
WITH ALLOW_UNTRUSTED_CERTS "false"
ADD MAPPING secureMapping
WITH SOURCE_TOPIC "secure/+/data"
WITH DESTINATION_TOPIC "cloud/+/data"
WITH DIRECTION "out"
```
***
## Best Practices
Always enable TLS when connecting to external brokers:
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
WITH USE_TLS "true"
```
Each bridge connection needs a unique CLIENT\_ID to avoid conflicts:
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
WITH CLIENT_ID "EdgeDevice001-SensorBridge"
```
Use `WITH BROKER SELF` during development:
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
ADD SOURCE_CONFIG
WITH BROKER SELF
```
Store passwords securely and use app-specific passwords where available.
Configure appropriate retry settings:
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
WITH RECONNECTION_RETRIES '10'
```
***
## Troubleshooting
* Verify IP address and port are correct
* Check firewall allows outbound connections on the configured port
* Ensure remote broker is running and accessible
* Try increasing `RECONNECTION_RETRIES`
* Verify certificate paths are correct
* Ensure certificates are valid and not expired
* Check certificate chain is complete
* For testing, use `ALLOW_UNTRUSTED_CERTS "true"` (not recommended for production)
* Verify SOURCE\_TOPIC and DESTINATION\_TOPIC are correct
* Check DIRECTION setting matches your expected data flow
* Ensure CLIENT\_ID is unique across all connections
* Check broker logs for subscription errors
***
## Next Steps
Compare bridging and Sparkplug roles.
Publish and consume structured industrial MQTT data.
# Sparkplug B Route
Source: https://docs.coreflux.org/latest/lot-language/routes/system/sparkplug-b
Decode Sparkplug B traffic to JSON or publish Coreflux MQTT topics as a Sparkplug edge node
## Give industrial MQTT data a shared structure
The `SPARKPLUG_B` route connects Coreflux to a Sparkplug B namespace. It can decode binary Sparkplug payloads into readable JSON, publish local MQTT topics as a Sparkplug edge node, or run both directions in one route.
**Sparkplug is a shared vocabulary for industrial MQTT.** MQTT carries the messages; Sparkplug defines how devices announce themselves, report values, and signal whether they are online.
### When to use it
Use this route when you need to:
* consume `spBv1.0/#` traffic in LoT Actions, dashboards, or storage Routes;
* publish Coreflux topics as `NBIRTH`, `NDATA`, and `NDEATH`;
* group edge-node metrics into Sparkplug devices;
* accept `NCMD` or `DCMD` writes for selected metrics; or
* gate edge-node publishing on an external Primary Host.
Use the separate [Sparkplug Host Route](./sparkplug-host) when Coreflux must be the Primary Host Application. For a **custom** plant `.proto` (not Sparkplug), use [Protobuf Models](/latest/lot-language/models/protobuf) instead of this route.
***
Start by choosing the direction in which Coreflux participates.
## Quick Start
Decode every Sparkplug B publisher on the local broker into readable MQTT topics:
```lot wrap focus={3} theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
DEFINE ROUTE PlantDecoder WITH TYPE SPARKPLUG_B
ADD SPARKPLUG_CONFIG
WITH MODE "DECODE"
```
This mode listens to all `spBv1.0/#` traffic. You do not need a Group ID or Edge Node ID.
Publish a local temperature topic as a Sparkplug edge-node metric:
```lot wrap focus={3-5,6-8} theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
DEFINE ROUTE Line1Edge WITH TYPE SPARKPLUG_B
ADD SPARKPLUG_CONFIG
WITH MODE "ENCODE"
WITH GROUP_ID "Factory"
WITH EDGE_NODE_ID "Line1"
ADD METRIC "Temperature"
WITH DATA_TYPE "Float"
WITH SOURCE_TOPIC "factory/line1/temperature"
```
Coreflux publishes a birth certificate when the route connects and sends data when the source topic changes.
Decode the namespace while publishing Coreflux metrics as a gateway edge node:
```lot wrap focus={3-5} theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
DEFINE ROUTE PlantGateway WITH TYPE SPARKPLUG_B
ADD SPARKPLUG_CONFIG
WITH MODE "BOTH"
WITH GROUP_ID "Factory"
WITH EDGE_NODE_ID "CorefluxGateway"
ADD METRIC "Uptime"
WITH DATA_TYPE "Int64"
WITH SOURCE_TOPIC "gateway/uptime"
```
The decode and encode paths operate independently inside the same route.
***
## Choose the Coreflux role
| Mode | Coreflux role | Inbound behavior | Outbound behavior |
| -------- | ------------------------ | ------------------------------------------ | ----------------------------------------- |
| `DECODE` | Translator and consumer | Decodes all `spBv1.0/#` traffic | None |
| `ENCODE` | Sparkplug edge node | Handles commands for writable metrics | Publishes birth, data, and death messages |
| `BOTH` | Translator and edge node | Decodes the namespace and handles commands | Publishes Coreflux metrics |
`MODE` is the canonical setting. `DIRECTION` remains accepted as a deprecated alias with the same values.
Only one namespace consumer can run per broker. Deploy either one `SPARKPLUG_B` route in `DECODE` or `BOTH` mode, or one `SPARKPLUG_HOST` route. Multiple `ENCODE`-only routes are allowed.
## Configure the Sparkplug connection
The `SPARKPLUG_CONFIG` block controls the route mode, edge-node identity, target MQTT broker, and optional Primary Host gating.
| Setting | Required | Default | Description |
| ----------------- | ------------------- | ----------- | ------------------------------------------------------------------ |
| `MODE` | No | `DECODE` | `DECODE`, `ENCODE`, or `BOTH` |
| `GROUP_ID` | For `ENCODE`/`BOTH` | — | Group identity Coreflux publishes on the Sparkplug wire |
| `EDGE_NODE_ID` | For `ENCODE`/`BOTH` | — | Unique edge-node identity within the group |
| `PRIMARY_HOST_ID` | No | — | External Primary Host whose retained `STATE` gates edge publishing |
| `SERVER` | No | `localhost` | MQTT broker used by the encode path |
| `PORT` | No | `1883` | Target MQTT broker port |
| `USERNAME` | No | — | Target broker username |
| `PASSWORD` | No | — | Target broker password; use `GET SECRET` |
| `USE_TLS` | No | `false` | Enables TLS for the target broker connection |
`GROUP_ID` and `EDGE_NODE_ID` do not filter decoded traffic. The decode path always consumes the complete `spBv1.0/#` namespace.
## Declare edge-node metrics
In `ENCODE` or `BOTH` mode, add node-level metrics directly to the route and device-level metrics inside `ADD DEVICE`.
| Setting | Required | Description |
| ------------------------------------ | ---------------------- | -------------------------------------------------- |
| `DATA_TYPE` | Yes | Sparkplug datatype for the metric |
| `SOURCE_TOPIC` | Yes | Local MQTT topic whose value Coreflux publishes |
| `WRITE_TOPIC` or `DESTINATION_TOPIC` | For command write-back | Local topic that receives an inbound command value |
| `WRITABLE` | No | Set to `"true"` to accept `NCMD` or `DCMD` writes |
This complete route publishes a node metric and a writable device metric to a remote broker:
```lot wrap focus={10-15} theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
DEFINE ROUTE Line1Edge WITH TYPE SPARKPLUG_B
ADD SPARKPLUG_CONFIG
WITH MODE "ENCODE"
WITH GROUP_ID "Factory"
WITH EDGE_NODE_ID "Line1"
WITH SERVER GET ENV "SPARKPLUG_BROKER"
WITH PORT 8883
WITH USERNAME GET ENV "SPARKPLUG_USER"
WITH PASSWORD GET SECRET "SPARKPLUG_PASSWORD"
WITH USE_TLS true
ADD METRIC "Uptime"
WITH DATA_TYPE "Int64"
WITH SOURCE_TOPIC "factory/line1/uptime"
ADD DEVICE "Press01"
ADD METRIC "Setpoint"
WITH DATA_TYPE "Float"
WITH SOURCE_TOPIC "factory/line1/press01/setpoint"
WITH WRITE_TOPIC "factory/line1/press01/setpoint/write"
WITH WRITABLE "true"
```
Supported scalar and array datatypes include signed and unsigned integers, `Float`, `Double`, `Boolean`, `String`, `Text`, `UUID`, `DateTime`, and `Bytes`. `DataSet` and `Template` values can be decoded but not encoded.
## Understand the message lifecycle
When the encode path connects, Coreflux:
1. publishes `NBIRTH`, including `bdSeq` and `Node Control/Rebirth`;
2. publishes one `DBIRTH` for each declared device;
3. publishes `NDATA` or `DDATA` when a source topic changes;
4. republishes births after a `Node Control/Rebirth` command; and
5. publishes death messages on a graceful stop or delivers `NDEATH` through the MQTT Will after a crash.
Writable node and device metrics accept inbound `NCMD` and `DCMD` values. Coreflux republishes each accepted value to its local write topic.
### Gate publishing on a Primary Host
Set `PRIMARY_HOST_ID` when an external Primary Host must be online before the edge node publishes:
```lot wrap focus={6} theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
DEFINE ROUTE GatedEdge WITH TYPE SPARKPLUG_B
ADD SPARKPLUG_CONFIG
WITH MODE "ENCODE"
WITH GROUP_ID "Factory"
WITH EDGE_NODE_ID "Line1"
WITH PRIMARY_HOST_ID "PlantHost"
ADD METRIC "Temperature"
WITH DATA_TYPE "Float"
WITH SOURCE_TOPIC "factory/line1/temperature"
```
The route watches the retained `spBv1.0/STATE/PlantHost` topic. It defers births and suppresses data until the host reports `online:true`, then publishes fresh births and resumes data.
Primary Host gating does not buffer values. Data produced while the host is offline is dropped, and multi-server Primary Host failover is not supported.
***
## Consume decoded topics
The decode path converts the fixed Sparkplug wire namespace into a JSON topic tree:
```text wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
spBv1.0////
→ sparkplug/decoded////
spBv1.0///
→ sparkplug/decoded///
```
Each decoded metric contains its canonical name, value, datatype, timestamp, and quality:
```json wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
{
"name": "Inputs/Temperature",
"value": 24.6,
"type": "Double",
"timestamp": 1717160400123,
"quality": "GOOD"
}
```
The route also publishes:
| Topic suffix | Retained | Purpose |
| ------------ | -------- | ------------------------------------------------ |
| `/` | No | Latest decoded metric event |
| `/$status` | Yes | Node or device online state from BIRTH and DEATH |
| `/$error` | No | Decode or alias-resolution error |
Metric names containing `/` create deeper topic levels. The `name` field in the JSON payload remains authoritative.
## Verify the route
Publish the connection check command to `$SYS/Coreflux/Command`:
```text wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
-checkRouteConnection PlantDecoder
```
Confirm `$SYS/Coreflux/Routes/PlantDecoder/status` reports the route as connected or active.
Use any MQTT client to confirm messages arrive under `spBv1.0/#`.
Subscribe to `sparkplug/decoded/#`. After a BIRTH message, named metric topics and retained `$status` topics appear.
## Troubleshooting
Confirm the route is active and that a Sparkplug B publisher is sending valid protobuf payloads under `spBv1.0/#`. Decoded output appears under `sparkplug/decoded/#`.
Alias-only DATA arrived before its matching BIRTH. Wait for or request a rebirth and monitor `$error` for `alias_map_unavailable`.
If `PRIMARY_HOST_ID` is configured, confirm the matching host publishes retained `{"online":true}` state on the same target broker configured by `SERVER` and `PORT`.
Confirm the metric has `WRITABLE "true"` and a `WRITE_TOPIC` or `DESTINATION_TOPIC`. Read-only metrics reject inbound commands.
## Best Practices
* Give every edge node a unique `GROUP_ID` and `EDGE_NODE_ID` pair.
* Build automations on `sparkplug/decoded/#`, not on opaque protobuf topics.
* Use retained `$status` topics for availability checks.
* Load broker credentials with `GET ENV` and `GET SECRET`.
* Monitor `$error` and route status for repeated alias or decode failures.
***
## Next Steps
Make Coreflux the Primary Host and issue commands to edge nodes.
Persist readable Sparkplug metrics in a database.
# Sparkplug Host Route
Source: https://docs.coreflux.org/latest/lot-language/routes/system/sparkplug-host
Make Coreflux the Sparkplug B Primary Host Application for namespace state, commands, decoding, and rebirth control
## Make Coreflux the namespace authority
The `SPARKPLUG_HOST` route makes Coreflux the Sparkplug B Primary Host Application. It publishes authoritative host state, decodes the complete Sparkplug namespace, issues commands to edge nodes, and requests rebirth when it detects missing aliases or sequence gaps.
**The Primary Host is the control room for a Sparkplug namespace.** Edge nodes report their catalogues and values; the host watches their health and sends commands back.
### When to use it
Use this route when Coreflux must:
* publish retained `STATE` as the namespace's Primary Host;
* consume and decode all edge-node traffic;
* issue `NCMD` and `DCMD` commands from LoT;
* detect sequence gaps and request a fresh BIRTH; or
* prevent two hosts from asserting the same Host ID.
Use [Sparkplug B Route](./sparkplug-b) instead when Coreflux only needs to decode traffic or publish as an edge node.
***
Start with the broker where the Sparkplug edge nodes publish.
## Quick Start
Make Coreflux the Primary Host for edge nodes connected to its local broker:
```lot wrap focus={3-4} theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
DEFINE ROUTE PlantHost WITH TYPE SPARKPLUG_HOST
ADD SPARKPLUG_HOST_CONFIG
WITH SERVER "localhost"
WITH HOST_ID "CorefluxHost"
```
Coreflux publishes retained state at `spBv1.0/STATE/CorefluxHost` and decodes the local namespace.
Connect the Host route to the broker already used by plant edge nodes:
```lot wrap focus={3-9} theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
DEFINE ROUTE PlantHost WITH TYPE SPARKPLUG_HOST
ADD SPARKPLUG_HOST_CONFIG
WITH SERVER "plant-broker.local"
WITH PORT 8883
WITH HOST_ID "CorefluxHost"
WITH HOST_ID_CONFLICT "REFUSE"
WITH USE_TLS true
WITH USERNAME GET ENV "SPARKPLUG_USER"
WITH PASSWORD GET SECRET "SPARKPLUG_PASSWORD"
```
Decoded metrics are republished on the local Coreflux broker under `sparkplug/decoded/#`.
Add an explicit command target that works even before Coreflux sees the edge node's BIRTH:
```lot wrap focus={6-9} theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
DEFINE ROUTE PlantHost WITH TYPE SPARKPLUG_HOST
ADD SPARKPLUG_HOST_CONFIG
WITH SERVER "plant-broker.local"
WITH HOST_ID "CorefluxHost"
WITH HOST_ID_CONFLICT "REFUSE"
ADD METRIC "Setpoint"
WITH DATA_TYPE "Float"
WITH TARGET "Factory/Line1/Press01"
WITH COMMAND_TOPIC "commands/line1/press01/setpoint"
```
Publishing a value to the command topic sends a typed `DCMD` to `Press01`.
***
## Understand the Host role
A Host route connects two MQTT contexts:
| Context | Purpose |
| ------------------------------- | ----------------------------------------------------------------- |
| Target broker (`SERVER`/`PORT`) | Carries `spBv1.0/#`, retained host `STATE`, and outbound commands |
| Local Coreflux broker | Receives `sparkplug/decoded/#` JSON and local command triggers |
The target and local brokers can be the same broker. If they differ, configure `SERVER` to the broker used by the Sparkplug edge nodes.
A `SPARKPLUG_HOST` route consumes the complete Sparkplug namespace. Do not run it alongside a `SPARKPLUG_B` route in `DECODE` or `BOTH` mode on the same broker. You can run separate `SPARKPLUG_B` routes in `ENCODE` mode.
## Configure the Primary Host
| Setting | Required | Default | Description |
| ------------------ | -------- | ----------- | ------------------------------------------------------------------------------------ |
| `HOST_ID` | Yes | — | Identity published in `spBv1.0/STATE/` |
| `HOST_ID_CONFLICT` | No | `REFUSE` | `REFUSE` backs off when another host appears to own the ID; `WARN` proceeds and logs |
| `SERVER` | No | `localhost` | Target broker where edge nodes publish |
| `PORT` | No | `1883` | Target broker port |
| `USE_TLS` | No | `false` | Enables TLS for the target broker |
| `USERNAME` | No | — | Target broker username |
| `PASSWORD` | No | — | Target broker password; use `GET SECRET` |
There is no `MODE` setting: `SPARKPLUG_HOST` defines the role.
### Prevent Host ID conflicts
With the default `HOST_ID_CONFLICT "REFUSE"`, Coreflux watches the configured state topic before asserting the role. If it sees a foreign `online:true` state for the same Host ID, it disconnects and stands down for that session.
Use `WARN` only when you know that the other state is stale and Coreflux should proceed.
## Follow the STATE lifecycle
The Host route publishes retained state with QoS 1:
1. Before connecting, it registers an MQTT Will with `online:false`.
2. After connecting, it publishes `online:true` using the same session timestamp.
3. On graceful stop, it publishes `online:false`.
4. After a crash, the target broker delivers the registered Will.
5. After reconnecting, it publishes a new offline-to-online session state.
The payload follows this shape:
```json wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
{
"online": true,
"timestamp": 1717160400123
}
```
Edge nodes configured with the matching `PRIMARY_HOST_ID` use this state to decide when to publish.
***
## Issue commands to edge nodes
The Host route supports two command methods.
### Use the convention topic tree
Publish a value on the local broker without declaring command metrics:
```text wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
sparkplug/command///
sparkplug/command////
```
The node form sends `NCMD`; the device form sends `DCMD`. Coreflux uses the datatype learned from the target's BIRTH catalogue.
This Action forwards a UI value to a node-level setpoint:
```lot wrap focus={3} theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
DEFINE ACTION SetLine1Setpoint
ON TOPIC "ui/line1/setpoint" DO
PUBLISH TOPIC "sparkplug/command/Factory/Line1/Setpoint" WITH PAYLOAD
```
### Declare a command overlay
Use `ADD METRIC` when you need a friendly trigger topic, an explicit datatype, or commands before the first BIRTH:
| Setting | Required | Description |
| --------------- | -------- | ------------------------------------------------------------------- |
| `DATA_TYPE` | Yes | Sparkplug datatype used to encode the command |
| `TARGET` | Yes | `/` for `NCMD` or `//` for `DCMD` |
| `COMMAND_TOPIC` | Yes | Local MQTT topic that triggers the command |
The declared datatype takes precedence over a learned catalogue. If neither source provides a datatype, Coreflux drops the command instead of guessing.
## Decode and repair the namespace
The Host route feeds the same readable topic tree as a `SPARKPLUG_B` decode route:
```text wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
spBv1.0///[/]
→ sparkplug/decoded//[/]/
```
It also monitors sequence integrity. When alias-only DATA arrives before BIRTH or a node's `seq` skips a value, Coreflux rate-limits and sends a node-level `Node Control/Rebirth` request.
| Output | Purpose |
| -------------------------------- | ------------------------------------ |
| `sparkplug/decoded/.../` | Decoded metric JSON |
| `sparkplug/decoded/.../$status` | Retained node or device availability |
| `sparkplug/decoded/.../$error` | Decode and alias-resolution failures |
***
## Verify the Host route
Publish the following command to `$SYS/Coreflux/Command`:
```text wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
-checkRouteConnection PlantHost
```
Confirm `$SYS/Coreflux/Routes/PlantHost/status` reports the target connection as active.
Use any MQTT client connected to the target broker and subscribe to `spBv1.0/STATE/CorefluxHost`. Confirm the retained payload reports `online:true`.
Subscribe to `sparkplug/decoded/#` on the local Coreflux broker, then connect or rebirth an edge node.
Publish a correctly typed value to a convention or declared command topic and confirm the edge node receives `NCMD` or `DCMD`.
## Troubleshooting
Confirm `SERVER`, `PORT`, credentials, and TLS settings point to the broker used by the edge nodes. Check route status for a `HOST_ID_CONFLICT` refusal.
Confirm edge nodes publish valid Sparkplug B traffic to the target broker and that no `SPARKPLUG_B` decode consumer conflicts with this Host route.
Use the correct convention topic or declared `COMMAND_TOPIC`. If the edge node has not published BIRTH, declare the command with an explicit `DATA_TYPE`.
Persistent alias or sequence gaps usually indicate an unstable edge node. Monitor decoded `$error` topics and the route status.
## Limitations
* The route connects to one target `SERVER` and `PORT`; multi-server Primary Host failover is not supported.
* The route does not provide durable store-and-forward replay.
* `DataSet` and `Template` metrics decode to JSON but cannot be sent as commands.
## Best Practices
* Keep `HOST_ID_CONFLICT` set to `REFUSE` unless you intentionally take ownership.
* Match every edge node's `PRIMARY_HOST_ID` to the Host route's `HOST_ID`.
* Point `SERVER` at the broker where the edge nodes actually publish.
* Prefer declared command overlays for production automations.
* Load credentials with `GET ENV` and `GET SECRET`.
***
## Next Steps
Decode traffic or publish Coreflux metrics as an edge node.
Automate decoded metrics and Sparkplug command topics.
# RULE Overview
Source: https://docs.coreflux.org/latest/lot-language/rules/overview
Define access control, permissions, and governance rules for your MQTT infrastructure
## Securing Your Broker
Rules define **who can do what** in your Coreflux broker. They control access to system operations, topic publishing/subscribing, and administrative functions—ensuring only authorized users can perform sensitive actions.
**Think of Rules like a security guard at a building entrance.** The guard checks your badge (permissions or group) and decides if you're allowed through the door (the operation). Rules do the same for every broker operation.
***
## When to Use Rules
| Scenario | Rule Type |
| ------------------------------------------- | ---------------------------------------- |
| Restrict who can create/delete users | User Management Rules |
| Control who deploys Actions, Models, Routes | Entity Management Rules |
| Limit access to system configuration | System Configuration Rules |
| Secure MQTT topic access by user or group | Publish/Subscribe Rules + [RBAC](./rbac) |
| Protect administrative `$SYS` topics | System Topic Rules |
For groups, precedence (**lower priority number wins**), default rules, and Visu panel access, see [RBAC with LoT Rules](./rbac).
***
## In This Section
* [Rules Syntax](./syntax) — Conditions, scopes, and patterns
* [RBAC with LoT Rules](./rbac) — Groups, precedence, cookbook, Visu access
***
## How to Deploy a Rule
Rules can be deployed in two ways: through a **LoT Notebook** (recommended) or via **MQTT commands**.
Custom topic and management ACL rules (`-addRule`) require a **Growth or Enterprise** license. Shipped default rules always apply.
Write your rule definition in a code cell and run it—the extension detects `DEFINE RULE` and sends the proper command to the broker.
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
DEFINE RULE Zone1Access WITH PRIORITY 100 FOR Subscribe TO TOPIC "zone1/#"
IF USER IN GROUP "zone1-staff" THEN
ALLOW
ELSE
DENY
```
Click the **Run** button. The notebook recognizes the LoT code and deploys it instantly.
This approach is ideal for:
* Developing and testing rules interactively
* Documenting your security configuration alongside the code
* Sharing rule sets with your team as `.lotnb` files
Deploy rules by publishing to the broker's command topic using any MQTT client.
**Topic:** `$SYS/Coreflux/Command`
**Payload:**
```bash wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
-addRule DEFINE RULE Zone1Access WITH PRIORITY 100 FOR Subscribe TO TOPIC "zone1/#"
IF USER IN GROUP "zone1-staff" THEN
ALLOW
ELSE
DENY
```
Subscribe to `$SYS/Coreflux/Command/Output` to receive the response confirming deployment.
***
## Removing Rules
Remove a rule by name using the `-removeRule` command. Publish to `$SYS/Coreflux/Command`:
```bash wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
-removeRule Zone1Access
```
You can also use the **Coreflux Entities** panel in VS Code to view and remove deployed rules. Locked built-in system rules cannot be removed.
***
## Updating Rules
To update an existing rule, deploy a new rule with the **same name**. The new definition replaces the existing one.
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
-- Original rule
DEFINE RULE Zone1Access WITH PRIORITY 100 FOR Subscribe TO TOPIC "zone1/#"
IF USER IN GROUP "zone1-staff" THEN
ALLOW
ELSE
DENY
-- Updated rule (same name, also allow supervisors)
DEFINE RULE Zone1Access WITH PRIORITY 100 FOR Subscribe TO TOPIC "zone1/#"
IF USER IN GROUP "zone1-staff" OR USER IN GROUP "supervisors" THEN
ALLOW
ELSE
DENY
```
***
## Viewing Active Rules
To see all deployed rules, subscribe to the rules system topic:
```bash wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
SUBSCRIBE "$SYS/Coreflux/Rules/#"
```
Or use the **Coreflux Entities** panel in VS Code if you have the LoT Notebooks extension installed.
***
## Default Rules Reference
Coreflux ships two tiers of defaults:
| Tier | Behavior |
| ----------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------- |
| **Locked system rules** | Management gates and `$SYS` access in the reserved band (`0–99`) — cannot be removed or overridden |
| **Seed catch-alls** | Priority `1 000 000` allow-all for connect, publish, and subscribe so clients keep working; the weakest band, so any custom rule overrides them |
| **Root backstop** | `root` can always recover command and rule management access |
Custom rules use priorities from `100` to `1 000 000`. **Lower priority number wins**; ties deny-wins; no match means deny. Details: [RBAC with LoT Rules](./rbac).
***
## Next Steps
Groups, precedence, cookbook, and Visu panel access.
Conditions, scopes, and complete rule patterns.
# RBAC with LoT Rules
Source: https://docs.coreflux.org/latest/lot-language/rules/rbac
Role-based access control with user groups, LoT Rules, permission flags, and Visu panel access
## Control Who Can Do What
Role-based access control (RBAC) in Coreflux combines **user groups**, **LoT Rules**, **permission flags**, and optional **Visu panel** sharing so you can lock down topics, management operations, and dashboards without external ACL servers.
**Like badge groups in a factory.** Operators get a badge that opens production doors; supervisors get a badge that also opens the emergency-stop cabinet. Groups name the badges; Rules decide which doors each badge opens.
### When to Use This Guide
| Goal | Use |
| ------------------------------------------- | ------------------------------------------------- |
| Restrict MQTT publish/subscribe by role | Groups + `DEFINE RULE` with `USER IN GROUP` |
| Gate who can create users, routes, or rules | Permission flags + management scopes |
| Hide or lock Visu panels/controls | `WITH SHARE TO GROUP` / `WITH INTERACT FOR GROUP` |
| Understand which rule wins | Precedence model below |
Adding custom topic or management ACL rules (`-addRule`) requires a **Growth or Enterprise** license. Shipped default rules always apply. Group membership commands and permission flags are available on all tiers.
***
Write rules with correct layout and conditions in [Rules Syntax](./syntax), then return here for groups, precedence, and operator patterns.
## User Groups
Each broker user can belong to one or more named groups. Group names are **case-insensitive** at evaluation time (`"Operators"` and `"operators"` match the same group). Prefer kebab-case or lowercase identifiers without spaces (`zone1-staff`, not `Zone 1 Staff`).
Groups are created by naming them — assign a user to a group and reference that name in rules or Visu clauses.
### Manage Groups via MQTT
Publish to `$SYS/Coreflux/Command` from a session authorized for user management (`AllowedUserManagement`, `AllowedSystemConfiguration`, or `root`). The requesting identity is the **authenticated MQTT session** — do not pass a requester in the payload.
```bash wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
-addUserToGroup alice operators
-removeUserFromGroup alice operators
-listUserGroups alice
```
Responses arrive on `$SYS/Coreflux/Command/Output`. See [Broker Commands](/latest/mqtt-broker/commands) for the full command set.
***
## Rule Precedence
**Lower priority number wins.** This is the same convention as BACnet write priority and ISA-18.2 alarm priority: the lower the number, the stronger the statement. The very strongest numbers are reserved for the broker's own rules, so `100` is the strongest priority you can write.
For a given operation (and topic, for topic operations), the broker considers only the rules with the **lowest** priority number among those that match. Within that winning band:
* If **any** matching rule evaluates to **DENY**, the operation is denied (**deny-wins**)
* Otherwise, if a rule allows, the operation is allowed
* If **no** rule matches at all, the operation is **denied** (default-deny)
Weaker (higher-numbered) rules are consulted only where no stronger rule matches the topic.
| Priority | Use case |
| ------------- | --------------------------------------------------------------------------------------------------------------- |
| `0–99` | **Reserved** for locked system rules — rejected for user/project rules |
| `100` | The strongest rule you can write — use it for the most specific case |
| `100–999 999` | The user band. Layer exceptions by stepping *down* toward `100` as rules get more specific |
| `1 000 000` | Shipped permissive catch-alls (`AllowPublishTopic` / `AllowSubscribeTopic` / `AllowConnect`) — the weakest band |
| `> 1 000 000` | **Rejected** — weaker than the catch-alls, so the rule could never apply |
Because the catch-alls sit at the weakest priority `1 000 000`, **any** rule you add in the user band overrides them for its topic (or, for Connect, for the whole connection). You do not need to remove or reprioritise a built-in to deny (or further allow) a topic.
**Start broad rules high — you can only carve out downwards.** An exception needs a *lower* number than the rule it overrides, and `100` is the floor of the user band. Give a namespace-wide rule a large number (`1000`, or higher if you expect several layers) and keep `100` for the most specific case. A broad rule placed at `100` leaves no room to carve out later without renumbering what is already deployed.
Prefer distinct priorities, or combine conditions with `OR` in one rule. Same-priority ties resolve **deny-wins**.
### Layout requirements
The LoT lexer is indentation-sensitive (one level = 4 spaces):
* Keep the full header on **one line**: `DEFINE RULE Name WITH PRIORITY n FOR …`
* Put each `IF` / `ELSE` / `ALLOW` / `DENY` on its own line (a single-line `THEN ALLOW ELSE DENY` body is rejected)
* Omitting `ELSE` is valid: `THEN DENY` allows everyone else, and `THEN ALLOW` denies everyone else. An explicit `ELSE` is still the clearer form.
* Rule names are bare identifiers — **no quotes** around the rule name
```lot wrap focus={2-5} theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
DEFINE RULE Zone1Access WITH PRIORITY 100 FOR Subscribe TO TOPIC "zone1/#"
IF USER IN GROUP "zone1-staff" THEN
ALLOW
ELSE
DENY
```
***
## Quick Start
Only members of `operators` may publish commands under `machines/+/cmd`:
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
DEFINE RULE OperatorControl WITH PRIORITY 100 FOR Publish TO TOPIC "machines/+/cmd"
IF USER IN GROUP "operators" THEN
ALLOW
ELSE
DENY
```
Supervisors or operators may subscribe to production topics:
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
DEFINE RULE SupOrOp WITH PRIORITY 100 FOR Subscribe TO TOPIC "production/#"
IF USER IN GROUP "supervisors" OR USER IN GROUP "operators" THEN
ALLOW
ELSE
DENY
```
Deny a config subtree at a coarse priority, then carve out one leaf for supervisors at a stronger (lower) one:
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
DEFINE RULE DenyConfig WITH PRIORITY 1000 FOR Publish TO TOPIC "site/config/#"
DENY
DEFINE RULE AllowSetpt WITH PRIORITY 100 FOR Publish TO TOPIC "site/config/setpoint"
IF USER IN GROUP "supervisors" THEN
ALLOW
ELSE
DENY
```
Deploy with `-addRule ` on `$SYS/Coreflux/Command`, or run the cell in a [LoT Notebook](/latest/quick-start/vscode). Remove with `-removeRule `.
***
## Access-Control Cookbook
### Deny a whole subtree
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
DEFINE RULE DenyRawArea WITH PRIORITY 100 FOR Publish TO TOPIC "factory/raw/#"
DENY
```
Beats the weakest-band publish catch-all (priority `1 000 000`) for `factory/raw/#`; other topics still fall through to the default allow.
### Restrict a topic to named users
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
DEFINE RULE ControlWriters WITH PRIORITY 100 FOR Publish TO TOPIC "line1/control/#"
IF USER IS "operator1" OR USER IS "operator2" THEN
ALLOW
ELSE
DENY
```
### Gate by permission flag
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
DEFINE RULE DiagnosticsReaders WITH PRIORITY 100 FOR Subscribe TO TOPIC "diagnostics/#"
IF USER HAS AllowedSystemConfiguration OR USER IS "root" THEN
ALLOW
ELSE
DENY
```
### Switch a namespace toward default-deny
Blanket-deny at a coarse priority, then allow only what you intend at a stronger (lower) one:
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
DEFINE RULE DenyAllPub WITH PRIORITY 1000 FOR Publish TO TOPIC "#"
DENY
DEFINE RULE AllowApp WITH PRIORITY 100 FOR Publish TO TOPIC "app/#"
ALLOW
```
Alternatively, remove the removable publish seed (`-removeRule AllowPublishTopic`) so unmatched publishes fall through to default-deny. Locked system rules and the root backstop still protect recovery. Restore the three shipped seeds later with [`-restoreRules`](/latest/mqtt-broker/commands#restoring-seeded-rules). Do **not** expect `-removeRule AllowConnect` alone to lock CONNECT — see [Connection admission](#connection-admission).
### Precedence walk-through
The table reads top-down in precedence order — the first band that matches decides:
| Rule | Priority | Topic | Decision |
| ------------------------------ | --------- | ----------------- | -------- |
| `AllowSensors` | 100 | `plant/sensors/#` | ALLOW |
| `DenyArea` | 1 000 | `plant/#` | DENY |
| `AllowPublishTopic` (built-in) | 1 000 000 | `#` | ALLOW |
* Publish to `office/x` → only the catch-all matches → **ALLOW**
* Publish to `plant/motor` → strongest match is `DenyArea` (1 000) → **DENY**
* Publish to `plant/sensors/t1` → strongest match is `AllowSensors` (100) → **ALLOW**
***
## Permission Flags vs Groups
| Mechanism | What it is for |
| -------------------- | ------------------------------------------------------------------ |
| **Permission flags** | Coarse system access — checked with `USER HAS` |
| **Groups** | Fine-grained topic and panel access — checked with `USER IN GROUP` |
| Flag | Gates |
| ---------------------------- | --------------------------------------------------------------- |
| `AllowedSystemConfiguration` | System config, creating routes/models/actions, many admin paths |
| `AllowedUserManagement` | User CRUD and group membership commands |
| `AllowedLogManagement` | Log access |
Set a flag via MQTT:
```bash wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
-changeUserSettings alice AllowedUserManagement true
```
In Coreflux HUB User Management, **Full Access** maps to `AllowedSystemConfiguration`; **User Management** and **Log Management** map to the matching flags. Manage groups with the MQTT commands above (see also [User Management](/hub/system/user-management)).
| Scenario | Prefer |
| ------------------------------------- | ------------------------- |
| Can this user create LoT actions? | Permission flag |
| Can this user see Zone 1 data/panels? | Group + rule / Visu share |
| Can this user manage other users? | `AllowedUserManagement` |
`USER IN GROUP`, `USER IS` / `USER EQUALS`, and `USER HAS` also work inside LoT Action bodies (for topic-triggered actions). Scheduled actions (`ON EVERY` / `ON START`) have no triggering user — group checks return false there.
***
## Default Rules and System Access
### Locked system rules
Management gates (`CommandCall`, user/rule/route/action management, and so on) and `$SYS` access rules are **locked** built-ins in the reserved band (`0–99`) — the strongest priorities on the number line. They cannot be removed, cannot be overridden by a same-named rule, and outrank every user rule (which are floored at `100`), so no rule you write can revoke admin access and lock the broker.
The band is split the same way user rules are — more specific means a lower number: base locked rules sit at `10`, and more-specific `$SYS` carve-outs at `1`.
The flip side of that protection: a custom rule on a management scope, or on `$SYS/#` subscribe, never decides either — the locked rule always forms the winning band. Adjust those with permission flags instead. See [Operation Scopes](./syntax#operation-scopes).
### Seed catch-alls
Three permissive defaults sit at priority `1 000 000` — the weakest possible band — so MQTT clients keep working out of the box while losing to every rule an operator writes:
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
DEFINE RULE AllowPublishTopic WITH PRIORITY 1000000 FOR Publish TO TOPIC "#"
ALLOW
DEFINE RULE AllowSubscribeTopic WITH PRIORITY 1000000 FOR Subscribe TO TOPIC "#"
ALLOW
DEFINE RULE AllowConnect WITH PRIORITY 1000000 FOR Connect
ALLOW
```
These are the only defaults an operator may remove or replace via MQTT. To change a seed catch-all's body, `-removeRule` it and then `-addRule` your replacement — rule names must be unique. Project merges skip every built-in name — including these seeds — so redefine them with MQTT commands, not project files.
To put the three seeds back (including resetting an override of those names to the shipped ALLOW body), run [`-restoreRules`](/latest/mqtt-broker/commands#restoring-seeded-rules). That command requires `root` or `AllowedSystemConfiguration` and does not require an RBAC license. Locked built-ins and your other custom rules are left alone.
Loading or unloading a project keeps locked built-ins and operator-authored rules. Only rules the broker attributes to the outgoing project are removed.
### Connection admission
A rule written `FOR Connect` decides whether the broker accepts an MQTT CONNECT after authentication succeeds. Connect rules take no `TO TOPIC` clause. Any user-band Connect rule (`100`–`999 999`) outranks `AllowConnect`.
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
DEFINE RULE DenyGuestConnect WITH PRIORITY 100 FOR Connect
IF USER IS "guest" THEN
DENY
```
A rule set with **no** `Connect` rule at all leaves CONNECT ungated — that is how upgrades from brokers that had no Connect operation keep accepting clients. `-removeRule AllowConnect` without adding a replacement therefore does **not** lock the door. To control who may connect, add an explicit `FOR Connect` policy. `root` remains admitted by the anti-lockout backstop.
### Rejected priorities
`-addRule` and project rule merges reject both ends of the number line, and report the reason per rule:
| Priority | Result |
| ------------- | ------------------------------------------------------------------------------------------------------------ |
| `0–99` | **Rejected** — nothing user-defined may tie with or outrank a locked system rule |
| `100–999 999` | Accepted — the user band |
| `1 000 000` | Accepted, so you can re-add a replaced catch-all in its own band |
| `> 1 000 000` | **Rejected** — weaker than the catch-alls, so the rule would never be consulted and would silently fail open |
### Root anti-lockout
Independently of all rules, `root` can always connect, run commands, manage rules, and reach `$SYS/Coreflux/Command` and `$SYS/Coreflux/Command/Output`.
### Upgrade note
**Review any custom rule that used the `1–99` range.** That band is now reserved for locked system rules, so such rules are **clamped to `100`** when the broker loads them (each clamp is logged with its old and new priority). Clamping keeps them in force and keeps them from outranking the locked safety rules, but several rules that used to be distinct can land on `100` together — where ties resolve **deny-wins**. Re-space them deliberately across the user band, putting the *more specific* rule at the *lower* number.
Locked built-ins are rewritten to their canonical definitions and priorities on every start, and a seed catch-all you kept has its body preserved but its priority normalized to `1 000 000`. A seed you deliberately removed stays removed. Fresh brokers also ship `AllowConnect` at `1 000 000`; see [Connection admission](#connection-admission).
***
## Visu Panel RBAC
LoTV panels can restrict who sees content and who can interact with controls.
### Visibility
```lot wrap focus={4-5} theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
DEFINE PANEL "Zone1Control"
WITH TITLE "Zone 1 Control"
WITH VISIBILITY SHARED
WITH SHARE TO GROUP "zone1-staff"
WITH SHARE TO GROUP "supervisors"
WITH STATE PUBLISHED
```
Published shared panels generate Layer 1 subscribe rules so only listed groups (or users via `WITH SHARE TO USER`) receive the panel. Others do not see it in `$Visu/Catalog`.
### Interaction
| Keyword | Who can interact |
| ------------------------------- | --------------------------------- |
| `WITH INTERACT PUBLIC` | Any client who can view the panel |
| `WITH INTERACT FOR GROUP ""` | Members of group `` |
| `WITH INTERACT FOR USER ""` | Named user `` |
| *(omitted)* | View-only — no Interact topic |
Stack multiple `FOR GROUP` lines or multiple `FOR USER` lines. Do **not** mix `FOR GROUP` and `FOR USER` on the same component — only the last scope is enforced. Put named users into a group when both are needed.
Denial modes: `WITH INTERACT_DENY SILENT` (default) or `WITH INTERACT_DENY NOTIFY` (publishes to `$Visu/Session//Denied`).
### Two-layer enforcement
| Layer | What |
| ------- | ----------------------------------------------------------------------- |
| Layer 1 | MQTT subscribe/publish to `$Visu` topics from panel visibility |
| Layer 2 | Runtime check of `WITH INTERACT FOR GROUP` / `FOR USER` per interaction |
***
## Best Practices
* Start from the shipped defaults; every rule you add already overrides the catch-alls for its topic
* Give namespace-wide rules a coarse number (`1000` or higher) and reserve `100` for the most specific carve-outs
* Prefer groups over hard-coded usernames for topic and panel access
* Keep rule headers on one line; use consistent 4-space indentation
* Keep custom priorities inside the user band (`100`–`1 000 000`)
* Document who owns each group name in your operations runbook
Never attempt to override locked system rules or rely on removing `root` protections. The reserved priority band and root backstop exist so the broker remains recoverable.
***
## Next Steps
Conditions, operation scopes, and complete rule patterns.
Group, user, and rule management command reference.
# RULE Syntax
Source: https://docs.coreflux.org/latest/lot-language/rules/syntax
Complete reference for LoT rule syntax, conditions, operation scopes, and access control patterns
## Write Your Broker's Access Policy
Every broker operation—creating users, deploying Actions, publishing to topics—passes through Rules before it executes. This page is the complete syntax reference for writing those rules: the conditions you can check, the scopes you can target, and the patterns that tie them together.
**Like writing house rules on a whiteboard.** Each rule says "IF you are on the approved list, THEN you may enter the room—ELSE the door stays locked." LoT rule syntax works the same way: you name the rule, pick the door (scope), and write the condition.
### When You Need This Reference
| Scenario | You'll use |
| -------------------------------------------------- | ------------------------------------------------ |
| Restricting who can create or delete users | User Management scopes |
| Controlling who deploys Actions, Models, or Routes | Entity Management scopes |
| Locking down MQTT topic access per user or group | Publish / Subscribe scopes with `TO TOPIC` |
| Protecting `$SYS` command topics | System topic scopes |
| Building role-based access across teams | Groups with `USER IN GROUP` — see [RBAC](./rbac) |
**Deploying rules:** In a [LoT Notebook](/latest/quick-start/vscode), write the rule and run the cell. For MQTT clients, use `-addRule` ([broker commands](/latest/mqtt-broker/commands#rules)). Custom ACL rules require a Growth or Enterprise license.
***
## Rule Structure
Every rule follows this structure:
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
DEFINE RULE WITH PRIORITY FOR [TO TOPIC ""]
IF THEN
ALLOW
ELSE
DENY
```
| Component | Description |
| ----------- | ------------------------------------------------------------------------------------------------------------------------------- |
| `RuleName` | Bare identifier (letters, digits, underscores) — **do not quote** the name |
| `PRIORITY` | Integer `100`–`1 000 000`; **lower number wins** among matching rules (see [Priority and Evaluation](#priority-and-evaluation)) |
| `Scope` | The operation type this rule controls |
| `TO TOPIC` | Optional topic pattern for Publish/Subscribe (and `$SYS` variants) |
| `condition` | Logic that determines access (user, group, permission flags) |
Keep the entire header on **one line**. Indent the body one level (4 spaces); nest `ALLOW`/`DENY` under `IF`/`ELSE` another level. A single-line `THEN ALLOW ELSE DENY` body is rejected.
Unconditional forms are valid too:
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
DEFINE RULE OpenPublic WITH PRIORITY 100 FOR Subscribe TO TOPIC "public/#"
ALLOW
```
***
## Conditions
Rules evaluate conditions to decide whether to ALLOW or DENY an operation.
### User Identity Checks
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
USER IS "root"
USER EQUALS "alice"
```
### Group Membership
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
USER IN GROUP "operators"
USER IN GROUP "zone1-staff"
```
Group lookup is case-insensitive. See [RBAC with LoT Rules](./rbac) for group management commands and patterns.
### Permission Flags
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
USER HAS AllowedUserManagement
USER HAS AllowedSystemConfiguration
USER HAS AllowedLogManagement
```
These are the three broker permission flags. Prefer groups for fine-grained topic access rather than inventing custom flag names.
### Combining Conditions
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
IF USER HAS AllowedUserManagement OR USER IS "root" THEN
ALLOW
IF USER IN GROUP "supervisors" AND USER EQUALS "alice" THEN
ALLOW
```
Omitting `ELSE` is valid. `THEN DENY` allows everyone else; `THEN ALLOW` denies everyone else — the same as writing the opposite decision in an `ELSE` branch. An explicit `ELSE` is still the clearer form.
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
DEFINE RULE DenyDashboardPublish WITH PRIORITY 120 FOR Publish TO TOPIC "factory/#"
IF USER IS "dashboard" THEN
DENY
```
***
## Operation Scopes
Rules control different categories of operations. They split into two groups that behave very differently in practice.
**Topic scopes** — `Publish`, `Subscribe`, `PublishSys`, `SubscribeSys` — are the scopes you write rules for. The only built-ins that compete with you are the permissive catch-alls in the weakest band, so your rules decide.
**Connect** — `FOR Connect` decides whether the broker accepts an MQTT CONNECT after authentication. It takes no `TO TOPIC` clause. See [Connection admission](./rbac#connection-admission).
**Management scopes** — everything below — are already covered by locked built-in rules in the reserved band (`0–99`) that key off [permission flags](#permission-flags). Because your rules are floored at `100` and only the lowest-numbered matching band decides, a rule you write on a management scope is **never** the deciding band.
Do not try to tighten or loosen a management gate with a custom rule. The locked built-in always wins, and `-addRule` rejects both a priority below `100` and any attempt to reuse a built-in rule name. Change who can perform these operations by setting **permission flags** with `-changeUserSettings` instead.
The tables below name each scope and the flags its locked built-in accepts. In every case `root` is also accepted.
### User Management
| Scope | Controls | Built-in gate accepts |
| ------------------------------ | -------------------------------------------- | ----------------------------------------------------- |
| `UserManagementCreation` | Creating new users | `AllowedUserManagement`, `AllowedSystemConfiguration` |
| `UserManagementRemove` | Deleting users | `AllowedUserManagement`, `AllowedSystemConfiguration` |
| `UserManagementUpdate` | Modifying user settings and group membership | `AllowedUserManagement`, `AllowedSystemConfiguration` |
| `UserManagementPasswordChange` | Changing user passwords | `AllowedUserManagement`, `AllowedSystemConfiguration` |
### Entity Management
| Scope | Controls | Built-in gate accepts |
| -------------------------- | ------------------------------ | ------------------------------------- |
| `ActionManagementCreation` | Creating Actions and panels | `AllowedSystemConfiguration`, `admin` |
| `ActionManagementRemove` | Deleting Actions and panels | `AllowedSystemConfiguration`, `admin` |
| `ActionManagementRun` | Manually running Actions | `AllowedSystemConfiguration`, `admin` |
| `ModelManagementCreation` | Creating Models | `AllowedSystemConfiguration`, `admin` |
| `ModelManagementRemove` | Deleting Models | `AllowedSystemConfiguration`, `admin` |
| `RouteManagementCreation` | Creating Routes | `AllowedSystemConfiguration`, `admin` |
| `RouteManagementRemove` | Deleting Routes | `AllowedSystemConfiguration`, `admin` |
| `RuleManagementCreation` | Creating Rules (`-addRule`) | `AllowedSystemConfiguration` |
| `RuleManagementRemove` | Deleting Rules (`-removeRule`) | `AllowedSystemConfiguration` |
### System Operations
| Scope | Controls | Built-in gate accepts |
| --------------------- | --------------------------------------------------- | -------------------------------------------------------------- |
| `SystemConfiguration` | Changing broker configuration | `AllowedSystemConfiguration` |
| `ShellCommand` | Executing shell commands | `AllowedSystemConfiguration` |
| `CommandCall` | Calling broker commands via `$SYS/Coreflux/Command` | `AllowedUserManagement`, `AllowedSystemConfiguration`, `admin` |
### Log Management
| Scope | Controls | Built-in gate accepts |
| ----------------------- | -------------------- | ---------------------------------------------------- |
| `LogManagementCreation` | Creating log traces | `AllowedLogManagement`, `AllowedSystemConfiguration` |
| `LogManagementRemove` | Deleting log traces | `AllowedLogManagement`, `AllowedSystemConfiguration` |
| `LogManagementUpdate` | Modifying log traces | `AllowedLogManagement`, `AllowedSystemConfiguration` |
***
## Topic Access Rules
### Standard Topics
Use `Publish` and `Subscribe` with `TO TOPIC`:
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
DEFINE RULE OperatorPublish WITH PRIORITY 100 FOR Publish TO TOPIC "machines/+/cmd"
IF USER IN GROUP "operators" THEN
ALLOW
ELSE
DENY
```
The broker ships catch-alls at the weakest priority (`1 000 000`) that allow all connect, publish, and subscribe. Any rule you add in the user band already outranks them for its topic — see [RBAC](./rbac).
### System Topics (`$SYS`)
`$SYS` topics use the `PublishSys` and `SubscribeSys` scopes. Most of this namespace is already locked down for you:
| Path | Coverage |
| -------------------------------------------------------------- | ------------------------------------------------------------------------------------ |
| `SubscribeSys` on `$SYS/#` | Locked built-in — admins only. Cannot be tightened or loosened by a custom rule |
| `PublishSys` on the broker's own command and management topics | Locked built-ins — flag-gated |
| `PublishSys` on any other `$SYS` subtree | No built-in, so **default-deny**. A rule you write is the only candidate and decides |
Because the locked `$SYS` rules sit in the reserved band, a custom rule for `$SYS/#` subscribe never decides. Write `PublishSys` rules only for subtrees the built-ins do not already cover:
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
DEFINE RULE AppStatusWriters WITH PRIORITY 100 FOR PublishSys TO TOPIC "$SYS/MyApp/#"
IF USER IN GROUP "app-services" THEN
ALLOW
ELSE
DENY
```
System topics (`$SYS/#`) contain sensitive broker information and command interfaces. Rely on the locked built-ins rather than writing your own `$SYS` subscribe rules, and never grant `$SYS` access to untrusted clients.
***
## Permission Flags
| Permission Flag | Grants Access To |
| ---------------------------- | -------------------------------------------------------- |
| `AllowedUserManagement` | User creation, removal, updates, group membership |
| `AllowedSystemConfiguration` | System config, many entity management paths, shell/admin |
| `AllowedLogManagement` | Log trace creation, removal, updates, read |
Assign flags with `-changeUserSettings` or in [Coreflux HUB User Management](/hub/system/user-management) (Full Access → `AllowedSystemConfiguration`).
***
## Priority and Evaluation
**Lower priority number wins** — the same convention as BACnet write priority and ISA-18.2 alarm priority. Among rules that match the operation (and topic), only the lowest-numbered band decides:
1. If any rule in that band evaluates to **DENY** → deny (**deny-wins**)
2. Else if any allows → allow
3. If **no** rule matches → **deny** (default-deny)
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
-- Strongest band for this topic (100) decides first
DEFINE RULE AllowSensors WITH PRIORITY 100 FOR Publish TO TOPIC "plant/sensors/#"
ALLOW
DEFINE RULE DenyPlant WITH PRIORITY 1000 FOR Publish TO TOPIC "plant/#"
DENY
```
| Priority band | Guideline |
| ------------- | --------------------------------------------------------------------- |
| `0–99` | Reserved for locked system rules — rejected for your rules |
| `100` | The strongest rule you can write — keep it for the most specific case |
| `100–999 999` | The user band; step *down* toward `100` as rules get more specific |
| `1 000 000` | Shipped connect/publish/subscribe catch-alls — the weakest band |
| `> 1 000 000` | Rejected — weaker than the catch-alls, so the rule could never apply |
Use a **lower** priority number than the rule you intend to override. Because the seed catch-alls sit at `1 000 000`, any rule in the user band is already enough to restrict a subtree — start broad rules at `1000` or higher so you have room to carve out exceptions later.
For cookbook patterns, Visu RBAC, and default-rule tiers, see [RBAC with LoT Rules](./rbac).
***
## Complete Examples
Operators and supervisors share a plant namespace; only supervisors may emergency-stop:
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
DEFINE RULE Zone1Cmd WITH PRIORITY 1000 FOR Publish TO TOPIC "zone1/+/cmd"
IF USER IN GROUP "operators" OR USER IN GROUP "supervisors" THEN
ALLOW
ELSE
DENY
DEFINE RULE EmergencyStop WITH PRIORITY 100 FOR Publish TO TOPIC "zone1/emergency/stop"
IF USER IN GROUP "supervisors" THEN
ALLOW
ELSE
DENY
```
Confine device accounts to their own tree, and deny them everything else with a coarse fallback:
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
DEFINE RULE DenyDeviceElsewhere WITH PRIORITY 1000 FOR Publish TO TOPIC "#"
IF USER IN GROUP "devices" THEN
DENY
ELSE
ALLOW
DEFINE RULE DevicePublish WITH PRIORITY 100 FOR Publish TO TOPIC "devices/#"
IF USER IN GROUP "devices" THEN
ALLOW
ELSE
DENY
```
Device accounts need no rule to keep them off `$SYS/Coreflux/Command` — the locked built-in already requires a permission flag, so simply leave all three flags unset on those users.
Separate publish access by group:
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
DEFINE RULE EngineeringPublish WITH PRIORITY 100 FOR Publish TO TOPIC "engineering/#"
IF USER IN GROUP "engineering" THEN
ALLOW
ELSE
DENY
DEFINE RULE OperationsPublish WITH PRIORITY 100 FOR Publish TO TOPIC "operations/#"
IF USER IN GROUP "operations" THEN
ALLOW
ELSE
DENY
DEFINE RULE SharedTopicAccess WITH PRIORITY 100 FOR Publish TO TOPIC "shared/#"
IF USER IN GROUP "engineering" OR USER IN GROUP "operations" THEN
ALLOW
ELSE
DENY
```
***
## Best Practices
Use `USER IN GROUP` for data and panel access. Reserve permission flags for system-level capabilities.
You do not need to remove `AllowPublishTopic` / `AllowSubscribeTopic` / `AllowConnect` to deny a subtree (or a connection). They sit in the weakest band (`1 000 000`), so any rule you write for that pattern already wins.
Exceptions need a *lower* number than the rule they override, and `100` is the floor. Place namespace-wide rules at `1000` or higher and keep `100` for the most specific case, so you never have to renumber deployed rules.
Rely on the locked built-ins — they already gate `$SYS` on permission flags and cannot be overridden. Keep those flags off device and operator accounts rather than writing your own `$SYS` rules.
Prefer `DenyRawArea` or `OperatorControl` over `Rule1`.
***
## Next Steps
Groups, precedence deep-dive, cookbook, and Visu access.
Deploy rules and manage groups via MQTT commands.
# Alarms and monitoring
Source: https://docs.coreflux.org/latest/lot-language/usage-patterns/alarms-and-monitoring
Threshold crossings, device and route liveness, and optional email notifications.
## Reacting to Threshold Crossings
Most alarm logic boils down to "if a value crosses a threshold, alert someone." LoT does this in a handful of lines: read the current value, read the threshold from config, compare, publish if exceeded.
**Like a kitchen oven timer.** It does one thing, watches one number, and beeps when the number's wrong. Quiet the rest of the time.
### When to Reach for This
Any time you need to detect that something has gone outside an acceptable range — temperature too high, tank too full, vibration too strong, fill level too low, pollution above a limit. This is one of the first patterns most projects need.
### The Simple Version
A CO2 monitor in a conference room:
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
DEFINE ACTION HighCO2Alarm
ON TOPIC "sensors/co2" DO
SET "current" WITH PAYLOAD AS DOUBLE
SET "max" WITH (GET TOPIC "config/co2_max" AS DOUBLE)
IF {current} > {max} THEN
PUBLISH TOPIC "alerts/co2_high"
WITH ("CO2 too high: " + {current})
```
That's it. Three things to notice:
1. **The threshold lives in `config/`, not in the Action.** A facilities manager can change it at runtime without redeploying anything. If the limit shifts from 1000 ppm to 1200 ppm, just publish the new value to `config/co2_max` and the next reading uses the new limit.
2. **The alert topic is separate from the data topic.** Anyone subscribing to `alerts/#` gets every alert in the system, regardless of source. Notifiers, on-call systems, and [dashboard `alert` and `indicator` components](/hub/dashboards/components/display) all use the same channel — the visualization end of an alarm pipeline.
3. **No state tracking in this version.** The Action fires every time a reading exceeds the threshold, so you'll get repeated alerts while CO2 stays high. If you have downstream notification infrastructure that handles deduplication, that's fine. If you don't, see the state-tracking variant below.
### Optional: Standardize the Alarm Format with a Model
Plain text alerts work, but as your system grows you'll want consistent structure — every alarm in the same JSON shape so dashboards and databases can parse them uniformly. Define a `COLLAPSED` Model and use `PUBLISH MODEL` instead of `PUBLISH TOPIC`. (`COLLAPSED` means the Model doesn't publish on its own — it only fires when something explicitly calls `PUBLISH MODEL`. See the [Models reference](/latest/lot-language/models/overview) for the full options.)
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
DEFINE MODEL Alarm COLLAPSED
ADD STRING "type"
ADD STRING "severity"
ADD DOUBLE "value"
ADD DOUBLE "threshold"
ADD STRING "timestamp"
DEFINE ACTION HighCO2Alarm
ON TOPIC "sensors/co2" DO
SET "current" WITH PAYLOAD AS DOUBLE
SET "max" WITH (GET TOPIC "config/co2_max" AS DOUBLE)
IF {current} > {max} THEN
PUBLISH MODEL Alarm TO "alerts/co2_high" WITH
type = "CO2_HIGH"
severity = "WARNING"
value = {current}
threshold = {max}
timestamp = TIMESTAMP "UTC"
```
Now every alarm — whether it's high CO2, low water level, or door left open — publishes the same JSON shape. A single database table, dashboard panel, or notification rule can handle all of them. This pairs especially well with [model inheritance](/latest/lot-language/usage-patterns/data-layer#standardizing-data-across-device-types).
### Optional: Deduplicate with State Tracking
If you don't have a downstream notifier that deduplicates — or you want a clean audit trail of "alarm started" and "alarm cleared" events — track the previous state in `state/` and only publish on transitions.
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
DEFINE ACTION HighCO2Alarm
ON TOPIC "sensors/co2" DO
SET "current" WITH PAYLOAD AS DOUBLE
SET "max" WITH (GET TOPIC "config/co2_max" AS DOUBLE)
SET "previous" WITH (GET TOPIC "state/co2_alarm")
IF {current} > {max} THEN
IF {previous} != "active" THEN
PUBLISH TOPIC "alerts/co2_high"
WITH ("CO2 too high: " + {current})
KEEP TOPIC "state/co2_alarm" WITH "active"
ELSE
IF {previous} EQUALS "active" THEN
PUBLISH TOPIC "alerts/co2_normal"
WITH ("CO2 back to normal: " + {current})
KEEP TOPIC "state/co2_alarm" WITH "normal"
```
What's happening:
* The action reads three things: current value, threshold, and the previous alarm state stored in `state/co2_alarm`.
* If CO2 is over the threshold **and** the alarm wasn't already active, it fires a "high" alert and marks the state active. Subsequent over-threshold readings while still active produce no extra alerts.
* If CO2 is back below threshold **and** the alarm was active, it fires a "normal" alert and marks the state normal.
The `state/` topic uses `KEEP TOPIC` (not `PUBLISH TOPIC`) so it's internal to the alarm logic — retained on the broker, but not noise on the wire.
### Optional: Send the Alarm by Email
For alarms that need to reach humans directly, add an `EMAIL` route that subscribes to your alert topics and sends a templated message via SMTP. The route does all the work — your Action just publishes to the alert topic as before. (Full reference: [Email Route](/latest/lot-language/routes/notifications/email).)
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
DEFINE ROUTE AlertEmail WITH TYPE EMAIL
ADD SMTP_CONFIG
WITH HOST GET ENV "SMTP_HOST"
WITH PORT '587'
WITH USE_TLS "true"
WITH USERNAME GET ENV "SMTP_USER"
WITH PASSWORD GET SECRET "SMTP_PASSWORD"
ADD EVENT NotifyOnHighCO2
WITH SOURCE_TOPIC "alerts/co2_high"
WITH SUBJECT "CO2 alarm: {value.json.value} ppm"
WITH RECIPIENT "facilities@acmecorp.com"
WITH TEMPLATE_PATH "/templates/alarm_email.html"
```
How it fits together:
1. The threshold Action publishes a structured alarm to `alerts/co2_high` (using the Model variant from above).
2. The email route is subscribed to that topic and fires automatically whenever a new alarm appears.
3. Placeholders like `{value.json.value}` pull fields out of the JSON payload to build the email subject and body.
Credentials come from `ENV` and `SECRET` (see [Keeping Credentials Out of Your Code](/latest/lot-language/usage-patterns/integrations#keeping-credentials-out-of-your-code)). Pair with the **state tracking** variant if you want only one email per alarm transition rather than one per reading.
***
## Knowing When a Device or Route Goes Offline
Two different things can go silent in a Coreflux system, and you want to know about both.
**Devices** are the sensors, machines, and gateways out in the world. They go offline when their network drops, their battery dies, or they crash. The signal that something's wrong is the *absence of incoming messages*.
**Routes** are the broker's own connections to databases, cloud brokers, REST APIs, and other external systems. They go offline when credentials change, the remote system is unreachable, or the network between them breaks. The signal here is the *absence of an outbound connection*, which the broker tracks itself using [SYS topics](/latest/mqtt-broker/sys-topics) (the built-in SYS hierarchy).
Both need monitoring. The data sources are different, so the patterns are different.
**Like a roll call at the start of class.** Everyone present says "here" — students through the door (devices), staff through the office intercom (broker SYS topics). After a beat of silence on a name, the teacher knows who's missing.
### When to Reach for This
Whenever uptime matters: critical infrastructure, paid telemetry, anything you'd want to dispatch a technician for. Skip it for opportunistic devices where occasional silence is normal (mobile phones, ad-hoc sensors). For routes, this is almost always worth doing — a silent integration is a silent data loss.
### Approach 1: Heartbeats for Devices
Each device publishes a timestamp to its own status topic at a regular interval. Then a scheduled Action checks for any device whose last timestamp is too old.
If your devices already send data regularly, you can stamp the heartbeat directly from the Action that processes their readings:
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
DEFINE ACTION RecordHeartbeat
ON TOPIC "sensors/+/+" DO
SET "device_id" WITH TOPIC POSITION 2
KEEP TOPIC "state/" + {device_id} + "/last_seen" WITH TIMESTAMP "UTC"
```
Every time any sensor sends any data, this Action stamps a `last_seen` timestamp for that device. The wildcard means it covers every sensor automatically, and `KEEP TOPIC` retains the value internally without broadcasting it.
A scheduled Action then wakes up periodically, compares each timestamp to "now," and publishes an alert for anything stale. Time arithmetic is awkward in LoT, so this is a natural place to call a small Python helper (see [Running Python from a LoT Action](/latest/lot-language/usage-patterns/reusable-logic#running-python-from-a-lot-action)):
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
DEFINE ACTION CheckDeviceLiveness
ON EVERY 60 SECONDS DO
CALL PYTHON "Liveness.check_all"
WITH (300)
RETURN AS {result}
SET "offline_count" WITH (GET JSON "count" IN {result} AS INT)
IF {offline_count} > 0 THEN
PUBLISH MODEL Alarm TO "alerts/devices/offline" WITH
type = "DEVICES_OFFLINE"
severity = "WARNING"
value = {offline_count}
threshold = 0
timestamp = TIMESTAMP "UTC"
```
The Python function reads every `state/+/last_seen` topic, compares to the current time, and returns the count and list of devices whose last\_seen is older than 300 seconds.
### Approach 2: SYS topics for Routes
The broker exposes a live view of itself through [SYS topics](/latest/mqtt-broker/sys-topics) — a built-in dashboard in MQTT form.
For monitoring route health, read the JSON array published on the topic below. It lists every client currently connected to the broker, with timestamps and connection details. Routes that maintain MQTT connections (bridges, REST endpoints in client mode, anything with a connected client identity) appear in this list. When one drops, it disappears from the array.
```
$SYS/Coreflux/Comms/Clients
```
The pattern: keep an "expected routes" list in a config topic, subscribe to that clients topic, and have an Action that flags any expected route that isn't in the live connection list.
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
DEFINE ACTION CheckRoutesOnline
ON EVERY 60 SECONDS DO
SET "expected" WITH (GET TOPIC "config/expected_routes")
SET "clients" WITH (GET TOPIC "$SYS/Coreflux/Comms/Clients")
CALL PYTHON "RouteMonitor.find_missing"
WITH ({expected}, {clients})
RETURN AS {result}
SET "missing_count" WITH (GET JSON "count" IN {result} AS INT)
IF {missing_count} > 0 THEN
SET "missing_list" WITH (GET JSON "list" IN {result} AS STRING)
PUBLISH MODEL Alarm TO "alerts/routes/offline" WITH
type = "ROUTES_OFFLINE"
severity = "CRITICAL"
value = {missing_count}
threshold = 0
timestamp = TIMESTAMP "UTC"
PUBLISH TOPIC "system/offline_routes" WITH {missing_list}
```
The Python helper does the actual list comparison — straightforward in Python, fiddly in LoT:
```python wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
# /scripts/RouteMonitor.py
import json
def find_missing(expected_csv, clients_json):
expected = [name.strip() for name in expected_csv.split(",") if name.strip()]
try:
clients = json.loads(clients_json)
connected_ids = {c.get("Id", "") for c in clients}
except (json.JSONDecodeError, TypeError):
connected_ids = set()
missing = [name for name in expected if name not in connected_ids]
return {"count": len(missing), "list": ",".join(missing)}
```
What's happening:
1. The expected route list lives in a config topic — for example, you'd publish `"DatabaseRoute,CloudBridge,WeatherAPI"` to `config/expected_routes`. Operators can update it without redeploying anything.
2. The Action reads the live JSON from `GET TOPIC` on the path above and hands both lists to Python.
3. Python parses the connections, computes the set difference, and returns the count plus a comma-separated list of missing routes.
4. If anything is missing, the Action publishes a critical alarm and a list of which routes are down.
### Combining the Two Approaches
A complete monitoring layer runs both Actions side by side: heartbeats catch devices that have stopped reporting even though their network looks fine, and the SYS clients-topic check catches routes that have lost their integration to the outside world. Both publish into the same `alerts/` branch, so a single subscriber — a dashboard, a notification rule, an [email route](#optional-send-the-alarm-by-email) — gives you a unified "what's not working right now" view.
Pair either Action with the [state tracking variant](#optional-deduplicate-with-state-tracking) to avoid alert spam: one alert when something disappears, one when it comes back, silence in between.
***
## Next Steps
Send alerts and reports to people from MQTT events.
Built-in broker topics for monitoring clients and comms.
# Anti-patterns
Source: https://docs.coreflux.org/latest/lot-language/usage-patterns/anti-patterns
Common LoT mistakes and proven alternatives — for authors and reviewers.
## Common LoT mistakes
These are the most frequent mistakes in LoT development. Avoid them whether you're writing new code or reviewing existing output.
| Anti-Pattern | Why It's Bad | Do This Instead |
| ------------------------------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| Omitting type casts in math | Implicit type handling leads to silent errors | Always cast: `PAYLOAD AS DOUBLE`, `GET TOPIC ... AS DOUBLE` — see [Operations](/latest/lot-language/actions/operations) |
| Triggering models on timestamps | Timestamps update every tick — model fires constantly | Trigger on the primary data field: `AS TRIGGER` on value, not timestamp — see [Schema definition](/latest/lot-language/models/schema-definition) |
| Inconsistent naming across models | `sensorID` in one model, `sensor_id` in another | Standardize on `snake_case` for all field names — see [Naming conventions](./naming-conventions) |
| Using Python for native LoT tasks | Python adds overhead for simple publish/get/if operations | Use Python only for math libraries, ML, API calls, or complex parsing |
| Handing out permission flags to open up `$SYS/#` | `$SYS` carries broker commands and sensitive data, and its access is gated by permission flags — a flag granted for convenience opens the whole namespace | Leave the three flags unset on device and operator accounts; the broker's locked `$SYS` rules then deny them. Custom Rules cannot tighten `$SYS/#` further — see [Rules syntax](/latest/lot-language/rules/syntax#system-topics-sys) |
| Generic entity names | `Action1`, `Rule1`, `MyRoute` are meaningless in a system with 50 entities | Name by purpose: `MonitorPressure`, `ProtectSysTopics`, `SensorDatabase` |
| `PUBLISH` for internal state | Broadcasts data that only your own Actions need | Use `KEEP TOPIC` for internal state; `PUBLISH` for external subscribers — see [Writing reusable logic](./reusable-logic#internal-state-keep-vs-publish) |
| Monolithic Actions | One Action doing 15 things is hard to test and debug | Split into callable Actions with `INPUT`/`OUTPUT` — see [One Action, One Job](./reusable-logic#one-action-one-job) |
***
## Next Steps
Entity, variable, and topic naming standards.
Callables, wildcards, and KEEP vs PUBLISH patterns.
# Designing your data layer
Source: https://docs.coreflux.org/latest/lot-language/usage-patterns/data-layer
Topic trees (UNS), payload formats (scalar vs JSON), Models, and inheritance — patterns for how data is shaped and addressed.
## Designing Your Topic Tree
Your topic tree is the backbone of every Coreflux project. It decides how Actions trigger, how Models publish, how Routes map data into databases, and how dashboards subscribe. Get it right early and the system stays clean as it grows. Get it wrong and you'll be untangling spaghetti for years.
A **Unified Namespace (UNS)** is the idea that every piece of data in your system has one predictable address — a single MQTT topic — that any consumer can subscribe to without knowing or caring where the data physically came from. The concept applies to any domain: a factory floor, an office tower, a solar park, a city's traffic lights, or a fleet of delivery trucks.
**Like a postal address that works anywhere in the world.** "Country / State / City / Street / Building / Apartment" lets a letter find any home, even one the postman has never visited. A UNS does the same for your data — a fresh subscriber can read or write to any device without ever being told it exists.
### When to Reach for This
Always — but the deeper hierarchy patterns earn their keep once you have more than a handful of devices, more than one site, or more than one team subscribing to the data. Even a single-device hobby project benefits from a consistent two-level structure.
### Common Principles Across Every UNS
Whatever your domain, a good hierarchy answers three questions:
1. **Who owns this data?** Top levels usually identify the organization or operator.
2. **Where does it physically live?** Middle levels mirror the real-world layout — sites, buildings, zones, machines.
3. **What is it?** The leaves identify the actual measurement, status, or event.
The deeper you go, the more specific the topic. Subscribers can pick exactly the level they care about and ignore the rest.
### Functional namespaces
Organize topics into functional namespaces that reflect the data lifecycle. These prefixes work alongside any domain hierarchy (ISA-95, buildings, fleets):
| Namespace | Purpose | Example Topics |
| ------------ | -------------------------------------------- | ---------------------------------------------------- |
| `sensors/` | Raw incoming sensor data | `sensors/temp001/raw`, `sensors/+/temperature` |
| `processed/` | Transformed or enriched data | `processed/temp001/fahrenheit`, `processed/+/status` |
| `alerts/` | Threshold violations and notifications | `alerts/critical/pump03`, `alerts/+/temperature` |
| `config/` | Configuration values read by Actions | `config/setpoint`, `config/max_temperature` |
| `state/` | Internal persistent state (used with `KEEP`) | `state/counter`, `state/last_run` |
| `cache/` | Cached values for quick reference | `cache/temp001/last_reading` |
| `system/` | Heartbeats, health checks, diagnostics | `system/heartbeat`, `system/status` |
| `commands/` | Inbound instructions from external systems | `commands/devices/+/restart` |
### Designing for Your Domain
Different domains use different hierarchy patterns. Here are five common ones — pick the closest match to yours and adapt.
#### Manufacturing — ISA-95
For factories, the industry standard is **ISA-95**, which divides operations from the enterprise level down to individual machines. The full MQTT path is a single string; the folder view below shows how each **topic level** lines up with **ISA-95** (Enterprise → Site → Area → Line / Cell → Device → **Metric** at the leaf).
`acme/dallas/packaging/line1/filler01/temperature`
```
acme/ enterprise
└── dallas/ site
└── packaging/ area
└── line1/ line / cell
└── filler01/ device
└── temperature metric
```
The ISA-95 levels: **Enterprise / Site / Area / Line / Cell / Device / Metric**. Use it for any project where you have a corporate hierarchy that maps onto a physical plant layout.
#### Smart Buildings
Buildings have a natural physical hierarchy: campus → building → floor → zone → device. The same operator prefix can branch like a folder tree:
```
acmecorp/
└── headquarters/
├── floor3/
│ └── conference_a/
│ ├── thermostat/
│ │ └── temperature
│ └── lighting/
│ └── state
└── garage/
└── level_b1/
└── sensors/
└── co2
```
Subscribe to `acmecorp/headquarters/floor3/#` to see every reading on the third floor. Subscribe to `acmecorp/+/+/+/thermostat/temperature` to compare temperatures across every conference room in the company.
#### Energy and Solar Parks
Energy operators typically organize by site, then by physical infrastructure (arrays, strings, inverters). One site can group weather separately from the array:
```
solarop/
└── lisbon-park/
├── array-a/
│ └── string-3/
│ └── inverter-01/
│ ├── power
│ └── voltage
└── weather/
└── irradiance
windop/
└── north-sea-cluster/
└── turbine-12/
└── rotor_speed
```
A maintenance team subscribes to `solarop/lisbon-park/#` for one park. A fleet operations center subscribes to `solarop/+/+/+/+/power` to monitor every inverter across every park.
#### Smart Cities
Cities organize by district, then by asset type (since asset categories are the meaningful grouping, not departments):
```
lisboa/
├── baixa/
│ ├── traffic_lights/
│ │ └── tl-042/
│ │ └── state
│ └── parking/
│ └── sensor-117/
│ └── occupied
└── parqueeduardo/
└── air_quality/
└── aq-03/
└── pm25
porto/
└── centro/
└── waste_bins/
└── bin-218/
└── fill_level
```
A transport team subscribes to `lisboa/+/traffic_lights/#`. An air quality dashboard subscribes to `+/+/air_quality/#` to see every monitor in every city.
#### Vehicle Fleets
Fleets organize by operator, region, vehicle type, and individual vehicle:
```
deliveryco/
└── iberia/
├── vans/
│ └── v-2840/
│ ├── location
│ └── fuel_level
└── refrigerated/
└── r-118/
└── temperature
```
A regional dispatcher subscribes to `deliveryco/iberia/#`. A predictive-maintenance system subscribes to `+/+/+/+/engine/diagnostic_codes` across every fleet.
### Optional: Data Classification
Many UNS designs add a classification segment after the device, splitting **measurements**, **state**, **events**, and **alarms** into separate branches under the same device:
```
acmecorp/
└── headquarters/
└── floor3/
└── conference_a/
└── thermostat/
├── metrics/
│ └── temperature
├── state/
│ └── mode
├── alarms/
│ └── sensor_fault
└── events/
└── setpoint_changed
```
This makes it trivial to subscribe to "all alarms in the building" (`acmecorp/headquarters/+/+/+/alarms/#`) without picking up a flood of ordinary readings. Use it when your project has clearly different *kinds* of data per device.
### Don't Over-Engineer Small Projects
A single-room weather station or a hobby solar setup doesn't need six levels of hierarchy. Even a flat `sensors//` is fine until you have enough scale to need more. Pick a structure that fits today's complexity, and leave room to grow.
***
## Choosing Data Formats
Your topic tree answers **where** data lives. The next decision is **what shape** each message carries — a single value on the payload, a JSON object, structured JSON produced by a **Model**, or **binary protobuf**. These choices are independent: you can mix scalar topics, JSON ingress, Model output, and protobuf edge topics in the same project.
**Like labels on shipping boxes.** The address (topic) tells you which shelf it belongs on; the box contents (payload) tell you what's inside. Sometimes one number on the label is enough; sometimes you need a packing list (JSON); sometimes you standardize the box size (Model) so every downstream handler knows what to expect.
### Scalar payloads (value only)
Use a **plain payload** — one number, string, or boolean with no JSON wrapper — when the **topic name already identifies** the measurement and you only ever send one value per message.
| Best for | Example topic | Payload | In Actions |
| -------------------------------------------------------- | ---------------------------------------- | ------------- | ------------------- |
| PLC registers, simple sensors, Modbus/OPC-UA mapped tags | `acme/dallas/line1/filler01/temperature` | `23.5` | `PAYLOAD AS DOUBLE` |
| Status flags, on/off | `.../running` | `true` or `1` | `PAYLOAD AS BOOL` |
| Commands with a single argument | `commands/device/restart` | `now` | `PAYLOAD AS STRING` |
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
DEFINE ACTION CheckHighTemperature
ON TOPIC "acme/dallas/+/+/+/temperature" DO
SET "temp" WITH PAYLOAD AS DOUBLE
IF {temp} > 80 THEN
PUBLISH TOPIC "alerts/high_temperature" WITH {temp}
```
**Practices:**
* Put meaning in the **topic path** (site, device, metric) so subscribers do not have to parse the body.
* Prefer scalar leaves under `processed/` or your UNS metric level when data is already normalized at the edge.
* Avoid cramming multiple measurements into one topic without JSON — use separate topics or switch to JSON.
### JSON payloads (structured body)
Use **JSON** when one MQTT message carries **several fields**, nested data, or metadata (timestamps, units, quality flags) that do not belong in the topic string.
| Best for | Example topic | Payload shape |
| -------------------------------------- | ----------------------------- | ----------------------------------------------------- |
| Devices, gateways, REST/webhook Routes | `sensors/temp001/telemetry` | `{"temperature":23.5,"humidity":65,"unit":"celsius"}` |
| Partner APIs with variable schemas | `integrations/partner/events` | Nested objects and arrays |
In **Actions**, extract fields with [`GET JSON`](/latest/lot-language/actions/operations#get-json). For nested paths, missing keys, and publishing cleaned JSON from an Action, see **[Working with JSON](/latest/lot-language/usage-patterns/working-with-json)** — that page is the hands-on guide; this section is about when to choose JSON at the data-layer level.
**Practices:**
* Keep **raw JSON** on ingress topics (`sensors/`, `integrations/`) and publish **normalized** data to `processed/` (scalar or Model output) so dashboards and databases see a stable shape.
* Always cast types in Actions (`AS DOUBLE`, `AS STRING`) — do not rely on implicit conversion.
* If many consumers need the same JSON schema, promote the shape with a Model instead of rebuilding JSON manually in every Action.
### Protobuf payloads (binary body)
Use **protobuf** when a gateway or SCADA system already speaks a `.proto` on MQTT. Decode with [`GET PROTO`](/latest/lot-language/actions/operations#get-proto) and emit bytes with `PUBLISH MODEL` on a `WITH FORMAT PROTOBUF` model. Tag topics in the middle can stay JSON. Guide: [Protobuf Models](/latest/lot-language/models/protobuf).
### Models (typed, consistent output)
A **Model** defines the JSON schema your broker publishes — field names, types, and triggers. Use Models when **multiple subscribers** (Routes, dashboards, other Actions) must see the **same structure** every time.
| Approach | When to use | How it publishes |
| ---------------------------------------------------- | --------------------------------------------------------------- | ------------------------------------------------------------------------------------------- |
| **Triggered Model** (`WITH TOPIC` + `AS TRIGGER`) | One device type, automatic publish when a source field updates | Broker builds JSON when the trigger field changes |
| **COLLAPSED Model** + `PUBLISH MODEL` from an Action | You control timing, destination, or merge JSON + scalar sources | Action decides when and where to publish |
| **Base + `FROM` inheritance** | Families of devices share most fields | See [Standardizing data across device types](#standardizing-data-across-device-types) below |
Triggered example — raw scalar in, structured JSON out:
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
DEFINE MODEL SensorReading WITH TOPIC "acme/dallas/line1/filler01/formatted"
ADD STRING "device_id" WITH "filler01"
ADD DOUBLE "temperature" WITH TOPIC "acme/dallas/line1/filler01/temperature" AS TRIGGER
ADD STRING "unit" WITH "celsius"
ADD STRING "timestamp" WITH TIMESTAMP "UTC"
```
Action-driven example — JSON in, COLLAPSED Model out (full walkthrough in [Working with JSON](/latest/lot-language/usage-patterns/working-with-json#end-to-end-extract-json-and-publish-a-model)):
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
DEFINE MODEL ProcessedSensorReading COLLAPSED
ADD STRING "device_id"
ADD DOUBLE "temperature"
ADD STRING "unit"
ADD STRING "timestamp"
DEFINE ACTION NormalizeTelemetry
ON TOPIC "sensors/+/telemetry" DO
SET "sensor_id" WITH TOPIC POSITION 2
SET "temp" WITH (GET JSON "temperature" IN PAYLOAD AS DOUBLE)
PUBLISH MODEL ProcessedSensorReading TO "processed/" + {sensor_id} WITH
device_id = {sensor_id}
temperature = {temp}
unit = "celsius"
timestamp = TIMESTAMP "UTC"
```
**Practices:**
* Use **scalar topics** for simple ingestion; use **Models** for the contract your analytics and APIs depend on.
* Do not hand-build JSON strings in Actions when `PUBLISH MODEL` can do it — fewer quoting bugs and consistent field names.
* Mark the **primary measurement** as `AS TRIGGER`, not timestamps or metadata ([schema definition](/latest/lot-language/models/schema-definition)).
* Exploding incoming JSON into per-field topics (Models overview) helps when other Models or Actions need single fields without `GET JSON` on every step — see [Models overview](/latest/lot-language/models/overview).
### How the three formats work together
A common pipeline:
1. **Ingress** — device sends JSON to `sensors/+/telemetry` (or scalar to `.../temperature`).
2. **Action** — optional `GET JSON`, validation, unit conversion.
3. **Model** — publishes stable JSON to `processed/+/formatted` (or triggered Model from scalar sources).
4. **Route** — stores `processed/#` or specific leaves in a database.
```mermaid theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
flowchart LR
device[Device or Route]
rawTopic[Raw topic scalar or JSON]
action[Action optional]
model[Model]
processed[processed/ topics]
consumers[Dashboards Routes Actions]
device --> rawTopic
rawTopic --> action
rawTopic --> model
action --> model
model --> processed
processed --> consumers
```
| If your payload is… | Prefer | Avoid |
| ------------------------------ | ------------------------------ | -------------------------------------------------------------- |
| One number per topic | Scalar + topic semantics | JSON with a single `"value"` field unless the device forces it |
| Multiple fields per message | JSON ingress + Action or Model | Parsing JSON with string splits in Actions |
| Same schema for many consumers | Model on `processed/` | Copy-pasting JSON templates in five Actions |
| Variant device types | Base Model + `FROM` | Separate unrelated schemas per device |
***
## Standardizing Data Across Device Types
When several devices in your system share most of their data fields but each has a few extras, define the shared fields in a **base Model**, then extend it for each variant. A LoT Model is essentially a typed schema that publishes structured JSON onto a topic. Inheritance works exactly like in object-oriented programming — the base holds what's common, the children add what's specific. Full reference: [Model Inheritance](/latest/lot-language/models/model-inheritance).
**Like a base recipe for bread dough.** Sourdough, focaccia, and pizza all start from the same base — they just add their own toppings. Improve the base recipe, and every variant gets the improvement automatically.
### When to Reach for This
When your system has more than one *kind* of device that mostly produces the same data — pumps and chillers, vans and trucks, conference rooms and labs. The break-even point is around three variants: at two it's still tempting to copy-paste, at three the inheritance pays off, at five it's the only sane way.
### The Pattern
A smart-building example: every room in the building has temperature, humidity, and occupancy sensors. But conference rooms also need to track presentation equipment, and labs need extra safety monitoring. Start with the shared base — it's `COLLAPSED`, meaning it doesn't publish on its own and exists only to be inherited:
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
DEFINE MODEL BaseRoom COLLAPSED
ADD STRING "room_id"
ADD DOUBLE "temperature"
ADD DOUBLE "humidity"
ADD INT "occupancy"
ADD STRING "status"
ADD STRING "timestamp"
```
A standard office uses the base directly. Specialized rooms extend it using `FROM`:
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
DEFINE MODEL ConferenceRoom FROM BaseRoom
ADD BOOL "projector_on"
ADD BOOL "video_call_active"
ADD INT "scheduled_meetings"
DEFINE MODEL Lab FROM BaseRoom
ADD BOOL "fume_hood_running"
ADD DOUBLE "co2_ppm"
ADD STRING "hazmat_status"
```
`ConferenceRoom` automatically has all six fields from `BaseRoom` plus three of its own. `Lab` gets the six base fields plus three lab-specific ones. If you later add a `last_cleaned` field to `BaseRoom`, every child Model gains it without any edits.
**Two rules of thumb for designing the base:**
* Put a field in the base only if **every variant** truly needs it. Identifiers, timestamps, status, and core measurements belong in the base. Type-specific fields belong in the children.
* Don't put `AS TRIGGER` on a base Model field. The base exists to be inherited — it's the children that decide when and how they publish, either through their own trigger or via `PUBLISH MODEL` from an Action.
The payoff: every team consuming room data — facilities dashboards, energy management, AI agents — sees a consistent shape. They can rely on `room_id`, `temperature`, and `occupancy` being there for any room, and only need to handle the extra fields for variants they actually care about.
The same pattern fits anywhere you have a family of related devices: `BaseInverter` with grid-tied and hybrid variants in solar, `BaseSensor` with temperature, vibration, and pressure variants in industrial, `BaseVehicle` with van, truck, and refrigerated variants in fleets.
For JSON-heavy ingress before you standardize with Models, see [Choosing data formats](#choosing-data-formats) and [Working with JSON](/latest/lot-language/usage-patterns/working-with-json).
***
## Next Steps
GET JSON patterns, nested payloads, and PUBLISH MODEL workflows.
Deep dive on `FROM`, base models, and extending schemas.
# Connecting to the outside world
Source: https://docs.coreflux.org/latest/lot-language/usage-patterns/integrations
REST server and client Routes, database KPIs, and ENV/Secrets for integrations.
## Letting Web Apps Send Data to MQTT
Sometimes the data you need isn't coming from a sensor — it's coming from a web app, a mobile app, a partner's webhook, or a system that only speaks HTTP. Coreflux's `REST_API` route can run in **server mode**, exposing an HTTP endpoint on the broker itself. When something POSTs to that endpoint, the route publishes the body to an MQTT topic, just like a sensor message.
**Like leaving a drop box at the front door.** Anyone with the right key can drop a package in, and once it's inside, it's handled the same way as everything else.
### When to Reach for This
* A web dashboard or mobile app needs to push commands or readings without implementing an MQTT client
* A third-party service (Stripe, GitHub, Slack, your CRM, an IoT vendor's cloud) supports HTTP webhooks but not MQTT
* A legacy system can call HTTP endpoints but doesn't speak MQTT
* You're behind a firewall that allows HTTP but not MQTT
### The Pattern
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
DEFINE ROUTE WebhookIngest WITH TYPE REST_API
ADD REST_API_CONFIG
WITH ENABLE_SERVER "true"
WITH SERVER_PORT '8080'
WITH USERNAME GET ENV "WEBHOOK_USER"
WITH PASSWORD GET SECRET "WEBHOOK_TOKEN"
WITH ENABLE_CORS "true"
ADD EVENT ReceiveSensorReading
WITH ENDPOINT "/api/sensors"
WITH METHOD "POST"
WITH DESTINATION_TOPIC "sensors/from/http"
```
How it works:
1. The route opens an HTTP server on port 8080.
2. When something POSTs to `http://your-broker:8080/api/sensors` with the right Basic Auth credentials, the body of the request is published to `sensors/from/http`.
3. From there, any Action subscribed to that topic processes it like a normal MQTT message — type-cast, normalize, publish into your UNS, store in the database.
A web app sending a reading would do something like:
```bash wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
curl -X POST http://broker.example.com:8080/api/sensors \
-u "$WEBHOOK_USER:$WEBHOOK_TOKEN" \
-H "Content-Type: application/json" \
-d '{"sensor_id": "temp-01", "value": 25.5}'
```
And an Action on the broker normalizes it into the UNS:
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
DEFINE ACTION RouteWebhookReading
ON TOPIC "sensors/from/http" DO
SET "sensor_id" WITH (GET JSON "sensor_id" IN PAYLOAD AS STRING)
SET "value" WITH (GET JSON "value" IN PAYLOAD AS DOUBLE)
PUBLISH TOPIC "acmecorp/headquarters/floor3/sensors/" + {sensor_id} + "/temperature" WITH {value}
```
That's the whole pattern. The web app doesn't need an MQTT library, the broker doesn't need an external middleware service, and the credentials sit safely in `ENV` and `SECRET`. Use the same approach for receiving CRM events, payment notifications, AI service callbacks, or anything else the outside world wants to push to you.
***
## Pulling Data from External APIs
The mirror image of webhook ingestion: when *you* need to call out to a third-party API. The same `REST_API` route in **client mode** turns an MQTT message into an outbound HTTP request, then publishes the response back to MQTT. Combined with `GET ENV` and `GET SECRET`, the credentials never appear in the route definition.
**Like a vending machine.** Drop the right token in (an MQTT request), and the answer pops out a slot below (an MQTT response).
### When to Reach for This
Whenever a Coreflux system needs information that lives outside it: weather forecasts for HVAC control, exchange rates for billing, geocoding for a fleet, traffic data for routing, AI/ML inference, public data feeds.
### The Pattern
Weather data is useful in nearly every domain — solar parks need irradiance forecasts, smart buildings adjust HVAC for outdoor temperature, fleets reroute around storms, agriculture schedules irrigation. Here's a route that calls a weather API. The base URL and credentials come from environment variables and secrets, so the same route works in dev and prod:
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
DEFINE ROUTE WeatherAPI WITH TYPE REST_API
ADD REST_API_CONFIG
WITH BASE_ADDRESS GET ENV "WEATHER_API_URL"
WITH ENABLE_CLIENT "true"
WITH USE_SSL "true"
WITH USERNAME GET ENV "WEATHER_API_USER"
WITH PASSWORD GET SECRET "WEATHER_API_KEY"
WITH REQUEST_TIMEOUT '30'
ADD EVENT GetCurrentWeather
WITH SOURCE_TOPIC "weather/request"
WITH DESTINATION_TOPIC "weather/response"
WITH METHOD "GET"
WITH ENDPOINT "/v1/current?location={value.json.location}"
```
How it works:
1. Some Action publishes a JSON message like `{"location": "Lisbon"}` to `weather/request`.
2. The route picks it up, builds the URL with the location filled in, and fires an authenticated HTTP GET.
3. The API responds with the weather data.
4. The route publishes that response to `weather/response`.
Trigger it from any Action — say, every fifteen minutes:
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
DEFINE ACTION FetchWeather
ON EVERY 15 MINUTES DO
PUBLISH TOPIC "weather/request" WITH '{"location":"Lisbon"}'
```
Then handle the response in another Action that subscribes to `weather/response`:
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
DEFINE ACTION StoreWeather
ON TOPIC "weather/response" DO
SET "temp" WITH (GET JSON "temperature" IN PAYLOAD AS DOUBLE)
SET "humidity" WITH (GET JSON "humidity" IN PAYLOAD AS DOUBLE)
PUBLISH TOPIC "processed/weather/temperature" WITH {temp}
PUBLISH TOPIC "processed/weather/humidity" WITH {humidity}
```
The HTTP credentials sit encrypted in `secrets.json`, the API URL sits in `.env`, and the Route definition stays portable across deployments. If the API key rotates, you update the secret and nothing else changes.
***
## Live KPIs from a Database
Database routes are not just for inserting data. The same `EVENT` syntax supports `SELECT` queries — either triggered by an MQTT message or run on a fixed schedule. This is how you turn a historian into a live KPI feed: one query, repeated automatically, results published as ordinary MQTT messages that any consumer can subscribe to.
**Like a live scoreboard at a stadium.** The board updates every minute with fresh totals, and everyone in the stadium sees the same numbers without having to do the math themselves.
### When to Reach for This
When you need rolling KPIs (averages, counts, peaks) that consumers should subscribe to like any other topic, or when you need on-demand historical lookups (last hour of readings for a device) triggered by an MQTT request. Skip it for one-off ad-hoc queries — for those, just query the database directly.
### Scheduled Queries: Live Aggregates
A common need in solar operations: publish the **average power output of the last 60 seconds** every minute, for every inverter, with no polling code anywhere.
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
DEFINE ROUTE InverterKpis WITH TYPE POSTGRESQL
ADD SQL_CONFIG
WITH SERVER GET ENV "DB_HOST"
WITH PORT GET ENV "DB_PORT"
WITH DATABASE "energy_data"
WITH USERNAME GET ENV "DB_USER"
WITH PASSWORD GET SECRET "DB_PASSWORD"
ADD EVENT AvgPowerLast60s
WITH EVERY 60 SECONDS
WITH DESTINATION_TOPIC "kpi/inverters/avg_power_60s"
WITH QUERY "SELECT inverter_id, AVG(power_kw) AS avg_power FROM inverter_readings WHERE ts > NOW() - INTERVAL '60 seconds' GROUP BY inverter_id"
```
What happens here:
1. Every 60 seconds, the route automatically runs the SQL query against your historian.
2. The result set is serialized as JSON and published to `kpi/inverters/avg_power_60s`.
3. Anyone listening — a dashboard, an Action, an AI agent — sees a fresh KPI every minute as a regular MQTT message.
No polling code, no glue script, no scheduler to maintain. The SQL is the schedule.
### On-Demand Queries: Historical Lookups
For queries that should run when *asked* rather than on a clock, swap `WITH EVERY` for `WITH SOURCE_TOPIC`:
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
ADD EVENT GetInverterHistory
WITH SOURCE_TOPIC "kpi/request/history"
WITH DESTINATION_TOPIC "kpi/result/history"
WITH QUERY "SELECT ts, power_kw FROM inverter_readings WHERE inverter_id = '{payload}' AND ts > NOW() - INTERVAL '1 hour' ORDER BY ts"
```
Now publishing `inv-01` to `kpi/request/history` returns the last hour of readings on `kpi/result/history`. Operators, AI agents, or other systems can pull historical context on demand.
**The pattern at a glance:** raw data lives in the database, KPIs and historical lookups are exposed as MQTT topics. Whether you're tracking solar inverters, traffic light timings, or refrigerated truck temperatures, your dashboards and downstream consumers don't care where the data physically lives — they just subscribe to topics like everything else.
***
## Keeping Credentials Out of Your Code
Never hardcode database passwords, API keys, or broker credentials in your code. Coreflux gives you two runtime stores — one for ordinary configuration, one for secrets that need to be encrypted.
**Like the difference between leaving your keys taped to the front door versus keeping them in a safe.** ENV is the wallet you carry openly (which city, which port, which feature flag). Secrets is the safe you only open when you need it (passwords, API keys, tokens).
### When to Reach for This
The first time you deploy outside your laptop — or earlier if you're committing your notebook to git from day one. Get the habit before you have any real credentials in flight; retrofitting `GET SECRET` into a working system is more painful than starting with it.
### The Two Operations
* `GET ENV "NAME"` reads from a managed `.env` file. Use it for non-sensitive configuration: hostnames, ports, region tags, feature flags. Safe to log.
* `GET SECRET "NAME"` reads from `secrets.json`, which is encrypted at rest with AES-256-GCM. Use it for anything sensitive: passwords, API keys, tokens. Never logged.
Both can be referenced directly inside Route configurations, which is exactly where you need them most:
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
DEFINE ROUTE ProductionDB WITH TYPE POSTGRESQL
ADD SQL_CONFIG
WITH SERVER GET ENV "DB_HOST"
WITH PORT GET ENV "DB_PORT"
WITH DATABASE GET ENV "DB_NAME"
WITH USERNAME GET ENV "DB_USER"
WITH PASSWORD GET SECRET "DB_PASSWORD"
WITH USE_SSL "true"
ADD EVENT StoreSensorReading
WITH SOURCE_TOPIC "sensors/+/reading"
WITH QUERY "INSERT INTO readings (ts, sensor_id, value) VALUES (NOW(), '{sensor_id}', '{value.json}')"
```
The same Route definition now works in development, staging, and production — only the `.env` file and `secrets.json` differ between environments.
To register a secret, run this once from a notebook or the broker console:
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
KEEP SECRET "DB_PASSWORD" WITH "your-actual-password"
```
After that, the password lives encrypted on the broker. Your route definitions, your git history, your AI assistant transcripts, and your logs all stay clean.
***
## Next Steps
Client and server mode, endpoints, and authentication.
`.env`, `secrets.json`, and secure deployment.
# Naming conventions
Source: https://docs.coreflux.org/latest/lot-language/usage-patterns/naming-conventions
PascalCase entities, snake_case variables and fields, and topic naming basics for maintainable LoT systems.
## Why naming matters
Consistent naming is the single most impactful practice for maintainable LoT systems. These conventions apply to all LoT entities and keep notebooks readable as your project grows.
**Like street names in a city.** Predictable patterns let anyone find an address without a map — and let AI assistants generate code that matches your project.
***
## Entity names
All LoT entities use **PascalCase**. Names should be descriptive and purpose-driven.
| Entity | Convention | Good Examples | Bad Examples |
| ---------------- | ----------------------------- | ---------------------------------------------------------------- | ------------------------------------- |
| Actions | PascalCase, verb-first | `ProcessTemperature`, `MonitorPressure`, `SendDailyReport` | `temp_process`, `action1`, `myAction` |
| Models | PascalCase, noun-based | `SensorReading`, `EquipmentStatus`, `ProductionRecord` | `sensor_model`, `Model1`, `data` |
| Rules | PascalCase, descriptive scope | `AllowAdminActions`, `ProtectSysTopics`, `RestrictDevicePublish` | `Rule1`, `myRule`, `newRule` |
| Routes | PascalCase, destination-based | `CloudBridge`, `SensorDatabase`, `AlertEmail` | `route1`, `my_route`, `dbRoute` |
| Callable Actions | PascalCase, function-like | `CalculateAverage`, `ConvertCelsiusToFahrenheit` | `calc`, `helper`, `util` |
***
## Variable names
Variables use **snake\_case** inside double quotes for `SET` declarations, and **curly braces** for references.
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
DEFINE ACTION ProcessSensorData
ON TOPIC "sensors/+/raw" DO
SET "sensor_id" WITH TOPIC POSITION 2
SET "raw_value" WITH (GET JSON "value" IN PAYLOAD AS DOUBLE)
SET "converted_value" WITH ({raw_value} * 1.8 + 32)
PUBLISH TOPIC "processed/" + {sensor_id} + "/fahrenheit" WITH {converted_value}
```
| Context | Convention | Examples |
| ------------------- | ----------------------------- | ---------------------------------------------------- |
| Declaration (`SET`) | `snake_case` in double quotes | `"sensor_id"`, `"raw_value"`, `"cycle_time"` |
| Reference | Curly braces | `{sensor_id}`, `{raw_value}`, `{cycle_time}` |
| Model fields | `snake_case` in double quotes | `"equipment_id"`, `"runtime_hours"`, `"last_update"` |
| Action inputs | `snake_case` after keyword | `INPUT value AS DOUBLE`, `INPUT threshold AS DOUBLE` |
***
## Topic names
Topics use **lowercase with forward-slash separators**. Multi-word segments use **snake\_case**.
Whenever possible, follow a **Unified Namespace (UNS)** format for topic naming. This enables readability and scalability as systems grow.
| Pattern | Example | Use Case |
| --------------------------- | ----------------------------------------------------------------- | ---------------------- |
| `domain/entity/attribute` | `sensors/temperature/value` | General data |
| `domain/instance/attribute` | `sensors/temp001/raw` | Instance-specific data |
| `domain/category/instance` | `alerts/critical/pump03` | Categorized events |
| Prefixed namespaces | `processed/`, `alerts/`, `config/`, `cache/`, `state/`, `system/` | Functional separation |
For hierarchy design, functional namespaces, and domain-specific UNS patterns, see [Designing your data layer](./data-layer).
***
## Next Steps
Topic trees, payload formats, and model inheritance.
Common mistakes and what to do instead.
# Production architecture
Source: https://docs.coreflux.org/latest/lot-language/usage-patterns/production-architecture
End-to-end ingestion and edge-to-cloud bridges — synthesis patterns that pull everything together.
## From Device to Database, End to End
Putting the patterns together, a full ingestion pipeline has four stages, each one a single LoT entity. This is the canonical recipe for "get data from a real device, normalize it, and store it for analytics."
**Like a kitchen production line.** The delivery comes in (sensor), the chef cleans and preps (Action), the dish is plated to the same standard every time (Model), and the waiter takes it to the table (Database).
### When to Reach for This
The first time you need persistent, queryable history of your data — usually somewhere between "it works on my laptop" and "we need to show this to the customer." It's also the natural foundation for [Live KPIs from a Database](/latest/lot-language/usage-patterns/integrations#live-kpis-from-a-database) and any reporting or analytics layer you'll add later.
### The Worked Example
A smart-building HVAC chiller, communicating via Modbus, normalized into a unified schema, and stored in PostgreSQL.
#### Stage 1 — Industrial Route Ingests from the Device
Pick the route type that matches your protocol: `MODBUS` for HVAC controllers, `OPCUA` for PLCs, `S7` for Siemens, `ALLEN_BRADLEY` for Rockwell, `BACNET` for building automation. Map each register or tag to a UNS topic.
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
DEFINE ROUTE ChillerGateway WITH TYPE MODBUS
ADD MODBUS_CONFIG
WITH HOST GET ENV "CHILLER_HOST"
WITH PORT '502'
WITH UNIT_ID '1'
ADD MAPPING coolingPower
WITH REGISTER '40001'
WITH DATA_TYPE "FLOAT32"
WITH DESTINATION_TOPIC "acmecorp/headquarters/floor3/hvac/chiller-01/metrics/cooling_power"
```
#### Stage 2 — Action Processes and Normalizes
Type-cast, scale, validate, then call a Model to produce structured output. A wildcard trigger means the same Action handles every chiller in every building.
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
DEFINE ACTION NormalizeChillerReading
ON TOPIC "acmecorp/+/+/hvac/+/metrics/cooling_power" DO
SET "building" WITH TOPIC POSITION 2
SET "floor" WITH TOPIC POSITION 3
SET "device_id" WITH TOPIC POSITION 5
SET "raw_kw" WITH PAYLOAD AS DOUBLE
PUBLISH MODEL ChillerReading TO "acmecorp/" + {building} + "/" + {floor} + "/hvac/" + {device_id} + "/processed" WITH
device_id = {device_id}
building = {building}
cooling_power_kw = {raw_kw}
timestamp = TIMESTAMP "UTC"
```
#### Stage 3 — Model Standardizes the Schema
One JSON shape for every chiller in every building. Add fields here once, every consumer downstream sees them.
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
DEFINE MODEL ChillerReading COLLAPSED
ADD STRING "device_id"
ADD STRING "building"
ADD DOUBLE "cooling_power_kw"
ADD STRING "timestamp"
```
#### Stage 4 — Database Route Persists
A single insert per published reading. Credentials come from `ENV` and `SECRET` so the route is portable between environments.
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
DEFINE ROUTE ChillerHistorian WITH TYPE POSTGRESQL
ADD SQL_CONFIG
WITH SERVER GET ENV "DB_HOST"
WITH USERNAME GET ENV "DB_USER"
WITH PASSWORD GET SECRET "DB_PASSWORD"
WITH DATABASE "building_data"
ADD EVENT StoreChillerReading
WITH SOURCE_TOPIC "acmecorp/+/+/hvac/+/processed"
WITH QUERY "INSERT INTO chiller_readings (ts, device_id, building, cooling_power_kw) VALUES ('{value.json.timestamp}', '{value.json.device_id}', '{value.json.building}', {value.json.cooling_power_kw})"
```
Four entities, one pipeline. Every chiller in every building flows the same way. Adding a new chiller means adding a Mapping in the gateway route — nothing else changes. Adding a new field to the schema means adding it to `ChillerReading` and updating the SQL — all the Actions stay untouched.
The same four-stage recipe fits any vertical: a solar inverter via Modbus, a traffic light controller via REST, a delivery van via cellular MQTT. Different devices, different protocols, same shape.
***
## Splitting Edge from Cloud with a Bridge
In a typical production setup, the **edge broker** runs on-site and handles all the high-frequency, device-facing work — every reading, every command, every alarm. The **cloud broker** sits centrally and only sees what the business needs to see — minute aggregates, alerts, cross-site rollups. An `MQTT_BRIDGE` route is what selectively forwards data between the two.
**Like a local newspaper sending a daily summary to head office.** The full story stays in the newsroom; only the headlines and totals make the trip to corporate. Cheaper, safer, faster.
### When to Reach for This
| Reason | What it gives you |
| ------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| **Bandwidth and cost** | Raw sensor data at 1 Hz from 100 devices is a lot. Aggregating to one summary per minute can shrink uplink traffic by 99% — and your cloud bill with it. |
| **Edge processing speed** | Local alarms react in milliseconds without waiting for a cloud round-trip. Even if the WAN goes down, the edge keeps running. |
| **Resilience** | If the cloud connection drops, the edge buffers and reconnects automatically. Operations never stop because of a flaky uplink. |
| **Data and security boundary** | Sensitive raw data never leaves the site. Only the summarized, business-relevant information crosses the boundary. |
| **Multi-site rollups** | The cloud sees every site through the same shape (`lisbon-park/...`, `madrid-park/...`), enabling cross-site analytics and dashboards without changing edge logic. |
This is the right pattern when you have **multiple sites**, **strict bandwidth or cost limits**, **regulatory or security requirements** to keep raw data local, or **devices that must keep operating during cloud outages**. For a single-site project with a fast and cheap connection, a single broker is simpler.
### What's Where: A Concrete Example
Imagine an edge broker at a solar park with multiple inverters. Each inverter publishes voltage, current, and power readings several times a second. Here's what each broker actually contains:
| Topic on **edge broker** (at the park) | What it is | Goes to cloud? |
| -------------------------------------- | -------------------------------------- | -------------- |
| `inverters/inv-01/voltage` | Raw reading every second | ❌ Stays local |
| `inverters/inv-01/current` | Raw reading every second | ❌ Stays local |
| `inverters/inv-01/power` | Raw reading every second | ❌ Stays local |
| `inverters/inv-02/power` | Raw reading from another inverter | ❌ Stays local |
| `state/inv-01/last_seen` | Internal state via `KEEP TOPIC` | ❌ Stays local |
| `aggregates/inv-01/1min` | Computed every 60 seconds by an Action | ✅ Forwarded |
| `aggregates/inv-02/1min` | Same, for the second inverter | ✅ Forwarded |
| `alerts/inv-01/low_output` | Threshold alarm | ✅ Forwarded |
| Topic on **cloud broker** (after bridge) | Source |
| ---------------------------------------- | ------------------------------- |
| `lisbon-park/aggregates/inv-01/1min` | Forwarded from the Lisbon edge |
| `lisbon-park/aggregates/inv-02/1min` | Forwarded from the Lisbon edge |
| `lisbon-park/alerts/inv-01/low_output` | Forwarded from the Lisbon edge |
| `madrid-park/aggregates/inv-A/1min` | Forwarded from a different site |
| ... | (one prefix per park) |
The cloud sees a clean, aggregated, multi-site view. The edge keeps everything raw, locally and privately.
### Step 1: Create the Aggregate at the Edge
An Action on the edge computes a summary every minute, per inverter. This is what gets sent up.
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
DEFINE ACTION PublishMinuteAggregate
ON EVERY 60 SECONDS DO
SET "inverter_id" WITH "inv-01"
SET "avg_power" WITH (GET TOPIC "cache/" + {inverter_id} + "/power_avg" AS DOUBLE)
SET "peak_power" WITH (GET TOPIC "cache/" + {inverter_id} + "/power_peak" AS DOUBLE)
PUBLISH MODEL InverterAggregate TO "aggregates/" + {inverter_id} + "/1min" WITH
inverter_id = {inverter_id}
avg_power_kw = {avg_power}
peak_power_kw = {peak_power}
timestamp = TIMESTAMP "UTC"
```
(The `cache/` topics get filled by other Actions that consume the raw `inverters/` stream — kept internal to the edge using `KEEP TOPIC`.)
### Step 2: Define the Bridge
Three topic mappings. Aggregates and alerts go up; commands come down. Everything else stays local.
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
DEFINE ROUTE EdgeToCloud WITH TYPE MQTT_BRIDGE
ADD SOURCE_CONFIG
WITH BROKER SELF
ADD DESTINATION_CONFIG
WITH BROKER_ADDRESS GET ENV "CLOUD_BROKER_HOST"
WITH BROKER_PORT '8883'
WITH CLIENT_ID GET ENV "EDGE_SITE_ID"
WITH USERNAME GET ENV "CLOUD_BROKER_USER"
WITH PASSWORD GET SECRET "CLOUD_BROKER_PASSWORD"
WITH USE_TLS "true"
ADD MAPPING aggregateUplink
WITH SOURCE_TOPIC "aggregates/#"
WITH DESTINATION_TOPIC "lisbon-park/aggregates/#"
WITH DIRECTION "out"
ADD MAPPING alertUplink
WITH SOURCE_TOPIC "alerts/#"
WITH DESTINATION_TOPIC "lisbon-park/alerts/#"
WITH DIRECTION "out"
ADD MAPPING commandsDownlink
WITH SOURCE_TOPIC "lisbon-park/commands/#"
WITH DESTINATION_TOPIC "commands/#"
WITH DIRECTION "in"
```
The site prefix (`lisbon-park`) is added on the cloud side, so multiple parks — or buildings, or cities, or fleet regions — can coexist on the same cloud broker without topic collisions.
### Complementary Patterns on the Cloud
Once the cloud is receiving aggregates and alerts as MQTT topics, you can compose more patterns on top of it:
* **Cloud database route for central analytics.** Add a PostgreSQL or CrateDB route on the cloud broker that subscribes to `+/aggregates/#` and inserts into a multi-site table. Now you have one historian with data from every site, queryable for trends, capacity planning, and cross-park benchmarks. The exact same `EVENT ... QUERY` pattern from the [Live KPIs from a Database](/latest/lot-language/usage-patterns/integrations#live-kpis-from-a-database) section works here.
* **Cloud dashboards.** Coreflux HUB Dashboards on the cloud broker subscribe to `+/aggregates/#` and `+/alerts/#` to give corporate or operations teams a unified view of every site.
* **Cross-site KPIs.** A scheduled `EVENT` on the cloud's database can publish company-wide KPIs (`SELECT AVG(...) GROUP BY site_id`) as MQTT topics, just like a single-site KPI — but rolled up across the whole portfolio.
* **AI / MCP routes on the cloud.** Plug an `AGENT` route into the cloud broker so an AI assistant can answer questions about the entire fleet without ever needing to touch the edge.
The bridge is the boundary; everything above it is business; everything below it is operations. Each side can evolve independently.
***
## Next Steps
Historians, inserts, and scheduled queries across database types.
REST, KPIs, and credentials from this guide.
# Project organization
Source: https://docs.coreflux.org/latest/lot-language/usage-patterns/project-organization
Keeping LoT notebooks clean, readable, and reviewable — single-file vs multi-file layouts.
## Keeping Notebooks Clean and Readable
A well-organized notebook is living documentation. Six months from now, a teammate (or you) should be able to open the file and immediately understand what every cell does and why.
**Like a well-kept recipe binder.** Each chapter has a clear theme, every recipe has a one-line description above the ingredients, and you can find anything in seconds because nothing's jumbled together.
### When to Reach for This
From day one, regardless of project size. Good notebook discipline is cheap to start and expensive to retrofit — by the time your single notebook is hard to navigate, splitting it up means re-reading every cell to figure out where it belongs.
### Single File or Multiple Files?
There's no single right answer — it's a project preference. Both approaches are valid, and both can produce clean, maintainable systems.
| Approach | Best for | Pros | Trade-offs |
| ------------------------------ | -------------------------------------------------------------- | ---------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------- |
| **Single file** | Small projects, demos, single-concern systems, getting started | Easy to share and review, everything in one place, simple to deploy as a unit | Long files take longer to navigate, harder to deploy parts selectively, awkward for teams editing in parallel |
| **Multiple files in a folder** | Larger projects, multi-concern systems, team collaboration | Easy to find specific concerns, parallel work without conflicts, selective deployment per file | More files to track, requires a clear naming convention |
A small smart-thermostat demo or a single-greenhouse irrigation controller probably belongs in one notebook. A multi-site solar park monitoring platform with industrial routes, normalization, alarms, and storage usually wants the folder layout. When in doubt, start single-file and split when it stops feeling comfortable.
The discipline below applies to both approaches.
### Cell Discipline (Applies Either Way)
A few habits make notebooks self-explanatory regardless of how many files you have:
* **One markdown cell before every LoT cell.** State what the next cell does, why it exists, and what topics it reads or publishes. The markdown cell is the contract; the LoT cell is the implementation.
* **One definition per LoT cell.** One Action, one Model, or one Route per cell — never bundled. This makes selective re-deployment trivial and keeps execution traces readable.
* **Section headers as standalone markdown cells.** Use `##` headers to break content into logical sections (`## Inverter Models`, `## Aggregation Actions`, `## Alarm Logic`). VS Code's outline view turns these into a clickable table of contents within the file.
* **An intro markdown cell at the top.** What's in this file, what other notebooks (if any) it depends on, what topics it owns.
### Single-File Layout
When everything lives in one notebook, lean on section headers to keep it navigable:
```
my-thermostat-demo.lotnb
├── # Smart Thermostat Demo (intro markdown cell)
├── ## Models
│ └── (LoT cells: each Model in its own cell, with markdown above)
├── ## Routes
│ └── (LoT cells: each Route in its own cell)
├── ## Processing Actions
│ └── (LoT cells: one Action per cell)
└── ## Alarm Actions
└── (LoT cells: alarm logic with markdown context)
```
Read top to bottom: it's a self-contained tour of your system.
### Multi-File Layout
For larger projects, one notebook per concern. Numbering reflects the deploy order — define Models before the Actions that publish them, define Routes before the Actions that trigger them.
```
my-solar-park/
├── 00_overview.lotnb // What this project does, who it's for
├── 01_models.lotnb // All Model definitions
├── 02_routes_ingestion.lotnb // Industrial routes, REST clients
├── 03_actions_processing.lotnb // Normalization, calculations
├── 04_actions_alarms.lotnb // Threshold detection, alerts
├── 05_routes_storage.lotnb // Database and bridge routes
├── 06_rules.lotnb // Access control
├── AGENTS.md // AI assistant conventions
└── README.md // How to deploy
```
Inside each file, the same cell discipline applies: section headers, markdown before each LoT cell, one definition per cell.
### What This Looks Like in Practice
A snippet from a Models notebook (or the `## Models` section of a single-file project):
> ## Inverter Models
>
> Standard schemas for every inverter in the fleet. The base model `BaseInverter` is the contract — every inverter variant inherits from it. Used by the processing actions and stored by the historian route.
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
DEFINE MODEL BaseInverter COLLAPSED
ADD STRING "inverter_id"
ADD DOUBLE "power_kw"
ADD DOUBLE "voltage"
ADD STRING "timestamp"
```
> Hybrid inverter variant — adds battery state for sites with on-site storage.
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
DEFINE MODEL HybridInverter FROM BaseInverter
ADD DOUBLE "battery_soc"
ADD DOUBLE "battery_power_kw"
```
Each LoT cell has a markdown cell right above it explaining the *why*. Six months from now, you'll thank past-you.
### Treat Notebooks Like Code
`.lotnb` files are text — diff them, branch them, review them. Commit your notebooks to git alongside your `AGENTS.md` and `README.md`. When a teammate joins, the project's git repository is everything they need to understand and reproduce the system.
Pair the notebooks with an `AGENTS.md` at the project root so AI assistants generate code that matches your conventions automatically. See [AI-Assisted Development Best Practices](/ai/best-practices) for the template.
***
## Next Steps
Installing and running the VS Code extension.
Conventions for `AGENTS.md` and AI-generated LoT.
# Writing reusable logic
Source: https://docs.coreflux.org/latest/lot-language/usage-patterns/reusable-logic
Callable actions, wildcards, counters, and Python — techniques for Actions that scale and stay maintainable.
## Building a Library of Reusable Logic
Any Action without a trigger clause is a **callable** — a reusable function with typed inputs and outputs. Build a small library of utility callables for the calculations and transformations your project does repeatedly, then call them from your trigger-based Actions.
**Like saving a custom button on a calculator.** You don't rebuild the multiplication function every time you need to multiply — you press the button. Callables are your custom buttons, named after exactly what they do.
### When to Reach for This
The moment you write the same expression a second time, or any time a logic block deserves its own name to make the parent Action readable. Also useful when several Actions need to share a calculation that might evolve over time — fix it in the callable, and every caller is updated.
### The Pattern
A callable has three parts: declared inputs, the logic that runs, and declared outputs to return:
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
DEFINE ACTION CalculatePercentage
INPUT value AS DOUBLE
INPUT total AS DOUBLE
DO
SET "pct" WITH ({value} / {total} * 100)
RETURN
OUTPUT pct
```
Supported `INPUT` types are `STRING`, `INT`, `DOUBLE`, `BOOL`, and `JSON`. A callable can return one or many `OUTPUT` values — the caller names the return variables in the order they're declared.
Here's a callable that returns three values from one calculation:
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
DEFINE ACTION AnalyzeReading
INPUT current AS DOUBLE
INPUT threshold AS DOUBLE
DO
SET "is_above" WITH ({current} > {threshold})
SET "difference" WITH ({current} - {threshold})
SET "percent_of_max" WITH ({current} / {threshold} * 100)
RETURN
OUTPUT is_above
OUTPUT difference
OUTPUT percent_of_max
```
Now a regular trigger-based Action can use both callables instead of duplicating the math itself. Whether you're tracking water tanks on a farm, fuel storage at a depot, or chemical reservoirs at a plant — the logic is the same:
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
DEFINE ACTION ProcessTankLevel
ON TOPIC "tanks/+/level" DO
SET "tank_id" WITH TOPIC POSITION 2
SET "level" WITH PAYLOAD AS DOUBLE
SET "capacity" WITH (GET TOPIC "config/" + {tank_id} + "/capacity" AS DOUBLE)
CALL ACTION CalculatePercentage
WITH value = {level}, total = {capacity}
RETURN fill_pct
CALL ACTION AnalyzeReading
WITH current = {level}, threshold = ({capacity} * 0.9)
RETURN above, diff, pct_of_max
PUBLISH TOPIC "processed/" + {tank_id} + "/fill_percent" WITH {fill_pct}
PUBLISH TOPIC "processed/" + {tank_id} + "/over_threshold" WITH {above}
```
The main Action stays focused on *what to do* (read level, publish results); the callables hold *how to calculate*. If the percentage formula changes, you fix it in one place.
***
## One Action, One Job
Each Action should do **one thing well**. Break complex logic into callable Actions with `INPUT`/`OUTPUT`:
Separate reusable logic into callable Actions, then compose them in a main Action:
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
DEFINE ACTION ConvertCelsiusToFahrenheit
INPUT celsius AS DOUBLE
DO
SET "fahrenheit" WITH ({celsius} * 9 / 5 + 32)
RETURN
OUTPUT fahrenheit
DEFINE ACTION ProcessTemperatureReading
ON TOPIC "sensors/+/celsius" DO
SET "sensor_id" WITH TOPIC POSITION 2
SET "temp_c" WITH PAYLOAD AS DOUBLE
CALL ACTION ConvertCelsiusToFahrenheit
WITH celsius = {temp_c}
RETURN temp_f
PUBLISH TOPIC "sensors/" + {sensor_id} + "/fahrenheit" WITH {temp_f}
```
All logic crammed into one Action, no reuse:
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
DEFINE ACTION DoEverything
ON TOPIC "sensors/+/celsius" DO
SET "sensor_id" WITH TOPIC POSITION 2
SET "temp_c" WITH PAYLOAD AS DOUBLE
SET "temp_f" WITH ({temp_c} * 9 / 5 + 32)
SET "temp_k" WITH ({temp_c} + 273.15)
SET "max" WITH (GET TOPIC "config/max_temperature" AS DOUBLE)
SET "pct" WITH ({temp_c} / {max} * 100)
IF {temp_c} > {max} THEN
PUBLISH TOPIC "alerts/" + {sensor_id} WITH "High"
PUBLISH TOPIC "sensors/" + {sensor_id} + "/fahrenheit" WITH {temp_f}
PUBLISH TOPIC "sensors/" + {sensor_id} + "/kelvin" WITH {temp_k}
PUBLISH TOPIC "sensors/" + {sensor_id} + "/capacity" WITH {pct}
```
***
## Internal State: KEEP vs PUBLISH
Use `KEEP TOPIC` for internal persistent state and `PUBLISH TOPIC` for external broadcast. Never use `PUBLISH` for values only your system reads:
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
DEFINE ACTION PersistentCounter
ON EVERY 10 SECONDS DO
SET "current" WITH (GET TOPIC "state/counter")
IF {current} == EMPTY THEN
KEEP TOPIC "state/counter" WITH 1
ELSE
KEEP TOPIC "state/counter" WITH ({current} + 1)
PUBLISH TOPIC "stats/count" WITH (GET TOPIC "state/counter")
```
| Operation | Use When |
| --------------- | ----------------------------------------------------------------------- |
| `PUBLISH TOPIC` | Other systems or subscribers need to see the value |
| `KEEP TOPIC` | Only your own Actions read the value (internal state, caches, counters) |
***
## One Action for Many Devices
The moment you find yourself about to copy-paste an Action and change one device ID, stop. Use a **wildcard** — the MQTT character `+` matches exactly one topic level, so `sensors/+/power` matches `sensors/inv-01/power`, `sensors/inv-47/power`, and any other topic with that shape. The broker fires your Action once per matching message, and `TOPIC POSITION` lets you read which device the message came from.
### Wildcard guidelines
| Wildcard | Meaning | Where to Use |
| -------- | -------------------------- | -------------------------------------------------------- |
| `+` | Matches exactly one level | Action triggers, Model topics, Route mappings |
| `#` | Matches one or more levels | Route mappings, Rule topic patterns, broad subscriptions |
Use `+` in **Actions and Models** for precise instance matching. Use `#` in **Routes and Rules** for broad coverage.
**Like writing a form letter that says "Dear customer."** One letter, delivered to thousands, and each recipient feels it's addressed to them.
### When to Reach for This
The moment you have two or more devices that should be processed the same way. Wildcards are also the foundation that makes [Models that publish for every device](/latest/lot-language/usage-patterns/data-layer#standardizing-data-across-device-types) work — and they let new devices join your system without any code changes.
### The Pattern
Imagine a solar park with a hundred inverters, each publishing power readings. One Action handles all of them:
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
DEFINE ACTION ProcessInverterPower
ON TOPIC "inverters/+/power" DO
SET "inverter_id" WITH TOPIC POSITION 2
SET "power_w" WITH PAYLOAD AS DOUBLE
SET "power_kw" WITH ({power_w} / 1000)
PUBLISH TOPIC "processed/" + {inverter_id} + "/power_kw" WITH {power_kw}
KEEP TOPIC "cache/" + {inverter_id} + "/last_seen" WITH TIMESTAMP "UTC"
```
**On `KEEP TOPIC` in this example.** The last-seen line uses [`KEEP TOPIC`](/latest/lot-language/actions/operations) instead of `PUBLISH TOPIC`. `PUBLISH` broadcasts a fresh message to every subscriber; `KEEP` stores a value on the broker without broadcasting. Use `KEEP` for internal state other Actions read later — counters, last-seen timestamps, alarm flags — not for data every dashboard must see live.
Walk through what's happening:
1. The trigger `inverters/+/power` matches `inverters/inv-01/power`, `inverters/inv-47/power`, and any other topic that fits.
2. `TOPIC POSITION 2` reads whatever the `+` matched — so on one trigger it's `inv-01`, on the next it's `inv-47`. Position counting is 1-based and starts at the topic root.
3. The published outputs use that value to build per-inverter topics, so each inverter gets its own processed reading and last-seen cache.
This is called **wildcard context inheritance**: the same `+` value flows through every `GET TOPIC` and `PUBLISH TOPIC` inside the Action, so you don't have to parse the topic string yourself.
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
DEFINE ACTION ProcessWithContext
ON TOPIC "sensors/+/temperature" DO
SET "sensor_id" WITH TOPIC POSITION 2
SET "humidity" WITH (GET TOPIC "sensors/+/humidity" AS DOUBLE)
PUBLISH TOPIC "processed/" + {sensor_id} + "/combined" WITH {humidity}
```
If triggered by `sensors/temp001/temperature`, the `GET TOPIC` reads from `sensors/temp001/humidity` — the wildcard resolves to the same instance.
A single Action like this can replace fifty hand-written copies — one for every device. When a new inverter comes online, you don't deploy new code; the existing Action just starts handling it.
The same approach works equally well for hotel rooms (`rooms/+/temperature`), parking sensors (`parking/+/occupied`), or delivery vans (`vans/+/fuel_level`). Anywhere you have many devices doing the same thing, one wildcard Action covers them all.
***
## Counting Things Over Time
Plenty of real-world questions are counting questions. How many cycles has this machine completed today? How many vehicles entered the garage this hour? How many error events has this device emitted this week? LoT has no built-in counter type, but a counter is just a number you keep on a topic and increment whenever something happens.
**Like a tally on a bar napkin.** Each time something happens, you make another mark. The napkin keeps the count even after you walk away.
### When to Reach for This
Whenever you need a value that **persists across messages** — not just within a single Action run. Production cycles, error totals, visitor counts, button presses, alarms-this-shift, anything that should keep counting whether you reset the system or not.
### The Pattern
The trick is two-part: on the very first run there's no value yet, so you initialize to zero. On every run after that, you read the current value, add one, and write it back.
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
DEFINE ACTION CountCycles
ON TOPIC "machines/cycle_complete" DO
IF GET TOPIC "counters/cycles" == EMPTY THEN
PUBLISH TOPIC "counters/cycles" WITH 0
ELSE
PUBLISH TOPIC "counters/cycles" WITH (GET TOPIC "counters/cycles" + 1)
```
What's happening:
1. Every time a `cycle_complete` message arrives, the Action runs.
2. The first time ever, `counters/cycles` doesn't exist yet, so `GET TOPIC` returns `EMPTY`. The Action creates the counter at zero.
3. Every subsequent run reads the current value, adds one, and publishes the new total.
The counter survives broker restarts as long as the topic is retained, and any dashboard or other Action can subscribe to `counters/cycles` to see the live count.
### Counters Per Device
Combine with a [wildcard](#one-action-for-many-devices) to keep one counter per device:
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
DEFINE ACTION CountCyclesPerMachine
ON TOPIC "machines/+/cycle_complete" DO
SET "machine_id" WITH TOPIC POSITION 2
IF GET TOPIC "counters/" + {machine_id} + "/cycles" == EMPTY THEN
PUBLISH TOPIC "counters/" + {machine_id} + "/cycles" WITH 0
ELSE
PUBLISH TOPIC "counters/" + {machine_id} + "/cycles"
WITH (GET TOPIC "counters/" + {machine_id} + "/cycles" + 1)
```
Now `counters/machine-01/cycles`, `counters/machine-02/cycles`, and so on each track their own machine independently — and one Action handles all of them.
### Resetting on a Schedule
For shift counters or daily totals, use a scheduled Action to reset:
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
DEFINE ACTION ResetDailyCounter
ON EVERY 24 HOURS DO
PUBLISH TOPIC "counters/cycles" WITH 0
```
Pair it with a scheduled SQL insert to archive the previous total before resetting, and you've got a daily history with two short Actions.
***
## Running Python from a LoT Action
LoT is great for the operations that 90% of IoT logic actually needs — math, conditionals, JSON extraction, MQTT publishes. But sometimes you need real programming: regex parsing, statistical analysis, calling a Python library, complex validation rules. For those cases, [`CALL PYTHON`](/latest/lot-language/actions/python-integration) lets a LoT Action invoke a Python function and use the result inline.
**Like calling a specialist into a routine job.** The general contractor handles the plumbing, framing, and electrical — but when there's something only a specialist can do, they make a phone call.
### When to Stay in LoT vs. Go to Python
**Stay in LoT for** simple math, string concatenation, basic conditionals, MQTT operations, JSON field extraction, route triggering, scheduled or topic-triggered logic. Anything LoT does natively, do natively — Python adds overhead and complexity.
**Reach for Python when** you need:
* Regex matching or advanced string parsing
* Statistical or scientific calculations (averages over windows, anomaly detection, ML inference)
* Library functions: `numpy`, `pandas`, `scipy`, `scikit-learn`, custom company libraries
* Complex validation rules with many conditional branches
* JSON restructuring beyond simple field extraction
The rule of thumb: **if you can do it in LoT, do it in LoT.** Python is a tool for capabilities LoT genuinely lacks, not a fallback when LoT is unfamiliar.
### When to Reach for This
When your Action has hit the limits of LoT's expressiveness — typically around regex, statistics, ML, or library calls — and the work isn't worth wrapping in an external service.
### The Pattern
A Python script that validates a complex sensor payload using regex and a custom rule:
```python wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
# /scripts/Validator.py
import re
def validate_serial(serial):
pattern = r'^[A-Z]{2}-\d{6}-[A-Z0-9]{4}$'
if re.match(pattern, serial):
return {"valid": True, "error": None}
return {"valid": False, "error": "Serial format invalid"}
```
A LoT Action that uses it:
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
DEFINE ACTION ValidateAndStore
ON TOPIC "sensors/registration" DO
SET "serial" WITH (GET JSON "serial" IN PAYLOAD AS STRING)
CALL PYTHON "Validator.validate_serial"
WITH ({serial})
RETURN AS {result}
SET "valid" WITH (GET JSON "valid" IN {result} AS BOOL)
IF {valid} EQUALS TRUE THEN
PUBLISH TOPIC "registry/valid" WITH PAYLOAD
ELSE
SET "err" WITH (GET JSON "error" IN {result} AS STRING)
PUBLISH TOPIC "registry/rejected" WITH ("Rejected: " + {err})
```
What's happening:
1. The Action receives a sensor registration with a serial number.
2. It hands the serial to the Python function, which uses regex (something LoT can't do natively) to validate the format.
3. The function returns a dictionary, which LoT receives as a JSON object.
4. The Action extracts the `valid` and `error` fields with `GET JSON` and routes the message accordingly.
### Practical Tips
* **Keep Python functions fast** (under \~100ms). They run inline in your Action and block message processing while they execute. For heavy computation, consider an external service.
* **Always handle errors.** Wrap the Python in `try/except` and return structured results — `{"success": false, "error": "..."}` — so the LoT side can branch on it cleanly.
* **Return JSON-friendly values.** Dictionaries, lists, strings, numbers, booleans. Custom objects won't serialize.
* **Convert types explicitly.** Python receives parameters as strings — `float(value)`, `int(count)` at the top of your function.
The full reference is at [Python Integration](/latest/lot-language/actions/python-integration).
***
## Next Steps
Reusable logic, on-demand scripts, and `CALL ACTION`.
`CALL PYTHON` syntax, deployment, and best practices.
# Working with JSON
Source: https://docs.coreflux.org/latest/lot-language/usage-patterns/working-with-json
Read JSON fields with GET JSON in Actions, handle nested payloads and missing keys, and publish structured output through Models.
## Overview
Most MQTT payloads in IoT systems are JSON — from devices, HTTP webhooks, industrial gateways, and partner APIs. In Actions you **read** fields with `GET JSON` and **write** structured output with [`PUBLISH MODEL`](/latest/lot-language/models/publish-model). Use raw `PAYLOAD AS DOUBLE` only when the message is a single value with no structure. For **binary protobuf**, use [`GET PROTO` and protobuf models](/latest/lot-language/models/protobuf) instead of `GET JSON`.
For when to choose scalar topics, JSON ingress, or Models at the architecture level, see [Choosing data formats](/latest/lot-language/usage-patterns/data-layer#choosing-data-formats) on the data layer page.
**Like fishing one specific item out of a packed grocery bag.** You don't unpack the whole bag — you reach in, grab the field by name, and leave the rest where it is.
For the full `GET JSON` syntax table and edge cases in the operations reference, see [Operations — GET JSON](/latest/lot-language/actions/operations#get-json).
***
## When to Use This
Any time an incoming MQTT message is a JSON object. Typical sources:
* Sensors and PLCs publishing JSON over MQTT
* REST or webhook Routes that forward JSON bodies to topics
* Cloud or SCADA integrations bridged into the broker
***
## Reading JSON Fields
[`GET JSON`](/latest/lot-language/actions/operations#get-json) takes a field name (or a dotted path), the source (almost always `PAYLOAD`), and a target type. It works inside any expression that needs a value:
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
DEFINE ACTION ProcessReading
ON TOPIC "sensors/reading" DO
SET "device_id" WITH (GET JSON "id" IN PAYLOAD AS STRING)
SET "temperature" WITH (GET JSON "temperature" IN PAYLOAD AS DOUBLE)
SET "active" WITH (GET JSON "enabled" IN PAYLOAD AS BOOL)
PUBLISH TOPIC "processed/" + {device_id} + "/temperature" WITH {temperature}
```
Supported target types are `STRING`, `DOUBLE`, `INT`, and `BOOL`. The conversion happens at extraction time, so by the time the value is in your variable it is already typed correctly for comparisons and math.
***
## Nested Paths
For nested objects, use dot notation. For arrays, use bracket notation. Mix them freely:
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
DEFINE ACTION ProcessNested
ON TOPIC "sensors/payload" DO
SET "temp" WITH (GET JSON "data.sensors.temperature" IN PAYLOAD AS DOUBLE)
SET "first_reading" WITH (GET JSON "readings[0].value" IN PAYLOAD AS DOUBLE)
SET "second_id" WITH (GET JSON "readings[1].sensor_id" IN PAYLOAD AS STRING)
```
For a payload like:
```json wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
{
"data": { "sensors": { "temperature": 23.5 } },
"readings": [
{ "sensor_id": "S1", "value": 100 },
{ "sensor_id": "S2", "value": 200 }
]
}
```
`{temp}` becomes `23.5`, `{first_reading}` becomes `100`, `{second_id}` becomes `"S2"`.
***
## Handling Missing Fields
If a field is absent, `GET JSON` returns a type-appropriate default: empty string for `STRING`, `0` for numeric types, `FALSE` for `BOOL`. For optional fields, check explicitly with `EMPTY`:
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
SET "battery" WITH (GET JSON "battery_level" IN PAYLOAD AS DOUBLE)
IF {battery} == EMPTY THEN
PUBLISH TOPIC "alerts/missing_field" WITH "battery_level not reported"
ELSE
PUBLISH TOPIC "processed/battery" WITH {battery}
```
This pattern matters when you consume data from third parties — webhooks and partner APIs often omit fields rather than send nulls.
***
## End-to-End: Extract JSON and Publish a Model
To publish structured JSON without hand-building strings, define a **COLLAPSED** Model with typed fields and use `PUBLISH MODEL ... TO ... WITH` from an Action. The broker assembles the JSON for you.
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
DEFINE MODEL SensorDataRecord COLLAPSED
ADD STRING "sensor_id"
ADD STRING "sensor_type"
ADD DOUBLE "temperature"
ADD DOUBLE "pressure"
ADD STRING "timestamp"
ADD STRING "quality_status"
DEFINE ACTION ProcessSensorJSON
ON TOPIC "sensors/+/json_data" DO
SET "sensor_id" WITH TOPIC POSITION 2
SET "temperature_val" WITH (GET JSON "temperature" IN PAYLOAD AS DOUBLE)
SET "pressure_val" WITH (GET JSON "pressure" IN PAYLOAD AS DOUBLE)
SET "sensor_type" WITH (GET JSON "type" IN PAYLOAD AS STRING)
SET "quality" WITH (GET JSON "quality_status" IN PAYLOAD AS STRING)
PUBLISH MODEL SensorDataRecord TO "sensors/processed/" + {sensor_id} WITH
sensor_id = {sensor_id}
sensor_type = {sensor_type}
temperature = {temperature_val}
pressure = {pressure_val}
timestamp = TIMESTAMP "UTC"
quality_status = {quality}
```
| Step | What Happens |
| ---- | ---------------------------------------------------------------------------------- |
| 1 | Action triggers when any sensor publishes JSON to `sensors/+/json_data` |
| 2 | `GET JSON` extracts each field with the correct type |
| 3 | `PUBLISH MODEL` builds a clean JSON object and publishes it to the processed topic |
See [Publish Model](/latest/lot-language/models/publish-model) for more on COLLAPSED models and field mapping.
***
## Next Steps
Topic trees, scalar vs JSON payloads, and when to use Models.
Full syntax reference for GET JSON and related operations.
# High-Availability Clustering
Source: https://docs.coreflux.org/latest/mqtt-broker/clustering
Run multiple Coreflux MQTT Broker nodes as a fault-tolerant cluster with leader election, durable QoS replication, and automatic failover
## Keep messaging available when a node fails
A single broker is a single point of failure. High-availability clustering runs several Coreflux MQTT Broker nodes as one system: if one node stops, the others keep serving clients. Clients can connect to any healthy node. Important messages (QoS 1 and 2) are copied across nodes so a crash of a minority of the cluster cannot erase a message the broker already accepted.
**Like a team with a captain and shared playbook.** One node leads changes; the others stay ready. If the captain drops out, the team elects a new one and keeps playing — without rewriting every client's connection plan.
### When to use clustering
Use clustering when you need:
* continuous MQTT service if one broker stops or is restarted;
* important publishes (QoS 1/2) to survive if some of the nodes crash after the broker has accepted them; or
* a fixed or elastic set of broker nodes that should act as one cluster.
Prefer **odd** node counts (**3** or **5**) so a clear majority can decide who leads. Clustering requires a license that grants the cluster feature. If you enable clustering without that license, the broker logs a warning and runs standalone.
Clustering is **broker configuration**, not a LoT System Route. For topic sync with another MQTT broker (including non-Coreflux), use an [MQTT Bridge](/latest/lot-language/routes/system/mqtt-bridge) instead.
***
Configure each node with the same cluster identity and discovery settings, then deploy the same LoT project to every node through your normal workflow.
## Quick Start
Add a `Cluster` section to the broker configuration (or matching environment variables — env vars override the file). On **every** node, create a dedicated MQTT user for inter-node traffic and set `DataPlaneUsername` to that username so nodes can exchange topic data.
Use fixed peer addresses when automatic discovery is unavailable (cloud, most Kubernetes networks, multi-subnet LANs):
```json wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
{
"Cluster": {
"Enabled": true,
"ClusterId": "factory-floor",
"NodeName": "broker-a",
"Discovery": "seeds",
"Seeds": ["broker-a:9200", "broker-b:9200", "broker-c:9200"],
"SharedSecret": "a-strong-secret",
"DataPlaneUsername": "cluster"
}
}
```
Auto-discover peers on a shared local network (default discovery mode):
```json wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
{
"Cluster": {
"Enabled": true,
"ClusterId": "lab-cluster",
"NodeName": "broker-1",
"Discovery": "multicast",
"SharedSecret": "a-strong-secret",
"DataPlaneUsername": "cluster"
}
}
```
Discover peers via a headless Service DNS name; better for elastic pods than a static address list:
```json wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
{
"Cluster": {
"Enabled": true,
"ClusterId": "k8s-brokers",
"NodeName": "broker-0",
"Discovery": "kubernetes",
"KubernetesServiceDns": "coreflux-broker-headless.default.svc.cluster.local",
"KubernetesPeerPort": 9200,
"SharedSecret": "a-strong-secret",
"DataPlaneUsername": "cluster"
}
}
```
***
## Concepts operators need
| Plane | What it does for you |
| ----------------- | --------------------------------------------------------------------------------------- |
| **Control plane** | How nodes find each other, who leads, and which node may accept LoT **writes** |
| **Data plane** | Moves MQTT topic traffic between nodes so subscribers on any node can receive publishes |
* **One leader.** Runtime LoT **write** commands run only on the current leader. Followers reject writes and tell you which node is the leader. Read-only and node-local commands (queries, diagnostics, license/instance key) still run locally.
* **LoT is not copied between nodes.** Each node loads its own LoT from disk / GitOps. A write applied on the leader changes **only that node** until you distribute the same project files to the others. Point runtime tooling at the leader, and treat your project repository as the source of truth for all nodes.
* **Only matching nodes join.** Nodes must share the same cluster identity and join secret so unrelated brokers do not join by accident. (See `ClusterId` and `SharedSecret` in the configuration reference.)
* **Session failover needs durability.** Enable [MQTT state durability](/latest/mqtt-broker/durability) so a client that reconnects to another node can get its subscriptions back. Messages queued for offline clients stay on the node that hosted them (see [Limitations](#limitations)).
***
## Configuration reference
### Core options
| Config field | Env var | Default | Description |
| ---------------------- | -------------------------------------- | ---------------------- | ----------------------------------------------------------------- |
| `Enabled` | `COREFLUX_CLUSTER_ENABLED` | `false` | Master switch |
| `ClusterId` | `COREFLUX_CLUSTER_ID` | `coreflux-cluster` | Logical cluster identity |
| `NodeName` | `COREFLUX_CLUSTER_NODE_NAME` | hostname | This node's identity |
| `BindIP` | `COREFLUX_CLUSTER_BIND_IP` | `0.0.0.0` | Control-plane listener bind address |
| `Port` | `COREFLUX_CLUSTER_PORT` | `9200` | Control-plane TCP port |
| `AdvertisedHost` | `COREFLUX_CLUSTER_ADVERTISED_HOST` | `NodeName` | Address peers use to reach this node |
| `AdvertisedPort` | `COREFLUX_CLUSTER_ADVERTISED_PORT` | `Port` | Port peers use to reach this node |
| `Discovery` | `COREFLUX_CLUSTER_DISCOVERY` | `multicast` | `multicast`, `seeds`, or `kubernetes` |
| `Seeds` | `COREFLUX_CLUSTER_SEEDS` | `[]` | Comma/semicolon list of `host:port` |
| `MulticastGroup` | `COREFLUX_CLUSTER_MULTICAST_GROUP` | `239.63.114.71` | Multicast group |
| `MulticastPort` | `COREFLUX_CLUSTER_MULTICAST_PORT` | `9201` | Multicast announce port |
| `KubernetesServiceDns` | `COREFLUX_CLUSTER_K8S_SERVICE_DNS` | — | Headless Service DNS name |
| `KubernetesPeerPort` | `COREFLUX_CLUSTER_K8S_PEER_PORT` | — | Peer port for Kubernetes discovery |
| `SharedSecret` | `COREFLUX_CLUSTER_SHARED_SECRET` | — | Join secret (discovery filtering and data-plane integrity) |
| `DataPlaneEnabled` | `COREFLUX_CLUSTER_DATAPLANE_ENABLED` | `true` | Exchange MQTT traffic between nodes |
| `DataPlaneMqttPort` | `COREFLUX_CLUSTER_DATAPLANE_MQTT_PORT` | broker MQTT port | MQTT port used between nodes |
| `DataPlaneUsername` | `COREFLUX_CLUSTER_DATAPLANE_USERNAME` | — | MQTT username for inter-node links (password from the user store) |
| `UseTls` | `COREFLUX_CLUSTER_TLS` | `false` | TLS on inter-node MQTT links |
| `Topology` | `COREFLUX_CLUSTER_TOPOLOGY` | `mesh` | `mesh` (recommended) or `ring` (complete only for ≤ 3 nodes) |
| `DataDir` | `COREFLUX_CLUSTER_DATA_DIR` | `{CFBasePath}/cluster` | Cluster state directory |
The data plane starts only when `DataPlaneEnabled` is true **and** `DataPlaneUsername` names a user that exists in the broker MQTT user store on that node.
### Replication options
| Config field | Env var | Default | Description |
| -------------------------- | --------------------------------------------- | ---------------------- | ---------------------------------------------------------------- |
| `ReplicationEnabled` | `COREFLUX_CLUSTER_REPLICATION_ENABLED` | `true` | Require quorum durability before acknowledging QoS ≥ 1 publishes |
| `ReplicationFactor` | `COREFLUX_CLUSTER_REPLICATION_FACTOR` | `0` (= auto min(3, N)) | Replica-set size per partition |
| `PartitionCount` | `COREFLUX_CLUSTER_PARTITION_COUNT` | `32` | Logical partitions (pick once; changing re-hashes keys) |
| `ReplicationTimeoutMs` | `COREFLUX_CLUSTER_REPLICATION_TIMEOUT_MS` | `5000` | Quorum wait before rejecting a publish |
| `ReplicationReplaySeconds` | `COREFLUX_CLUSTER_REPLICATION_REPLAY_SECONDS` | `300` | Failover replay window (0 = no replay) |
***
## Ports
| Port | Protocol | Purpose |
| ---------------- | -------- | ---------------------------------------------- |
| `9200` | TCP | Control plane (leader election and membership) |
| `9201` | UDP | Multicast discovery (multicast mode only) |
| Broker MQTT port | TCP | Inter-node MQTT data plane |
Open these between cluster members in your firewall or network policies.
***
## Delivery guarantees
| QoS | What clustering adds |
| ----- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| **0** | Best-effort delivery to peers; may be lost if a target node is down |
| **1** | The broker accepts the publish only after a **majority** of the replica set has stored it; other nodes receive it reliably; after failover it may be delivered again (at-least-once) |
| **2** | Same majority check before the broker accepts the publish in normal operation; across failover, treat delivery as **at-least-once** and design consumers to ignore duplicates |
**Summary:** once the broker has accepted a QoS 1 or 2 publish, that message is stored on a majority of nodes and can be replayed if ownership moves after a failure. If the cluster is split and this side no longer has a majority, it **rejects** those publishes instead of accepting messages it cannot keep safe.
***
## Operations
### Bootstrap and failover
* A node that sees an existing leader joins as a follower.
* If no leader is visible and peers exist, the cluster elects a leader and adds members as they are discovered.
* When the leader is lost, a new one is elected quickly (typically within a fraction of a second). Survivors stop contacting confirmed-dead peers and bring returning nodes back into the mesh automatically.
* With **seeds** discovery, a stopped node can remain a configured member (marked unavailable) while majority is computed over the configured list. Prefer **kubernetes** discovery when instances come and go often.
### Observability
| Signal | What it shows |
| ---------------------- | ----------------------------------------------------------------------------------------------------------------- |
| `GET /cluster` | Full status: role, leadership, members, data-plane health, replication, and related fields |
| `GET /info` | Lightweight `cluster` summary |
| Prometheus `/metrics` | Cluster, partition, replication, and related series (`coreflux_cluster_*`, `coreflux_replication_*`, and related) |
| Retained `$SYS` topics | `$SYS/Coreflux/Cluster/{Role,IsLeader,Leader,Term,Members,Status}` (refreshed periodically) |
Use MQTT Explorer or any MQTT client to subscribe to the `$SYS/Coreflux/Cluster/#` topics, or call `GET /cluster` on each node's operations HTTP port.
***
## Limitations
**LoT is not replicated between nodes.** Distribute the same project/configuration to every node. Runtime edits on the leader do not appear on followers until you provision them.
**Retained messages are node-local.** They survive a restart on that node but are not a cluster-wide retained store. A subscriber on another node may not see a retain set elsewhere.
**Subscriptions can move; offline queues do not.** A client reconnecting with a persistent session to another node can regain its subscriptions when [durability](/latest/mqtt-broker/durability) is enabled. Queued QoS 1/2 messages for offline clients stay on the node that hosted them until that node returns.
**QoS 2 across failover is at-least-once.** Design consumers to tolerate duplicates after a node failure or replay.
***
## Next Steps
Persist retained messages, sessions, and queued QoS across restarts — required for session failover.
Ports, TLS, listeners, and other broker settings for each node.
# Broker Commands
Source: https://docs.coreflux.org/latest/mqtt-broker/commands
Publish commands to the Coreflux MQTT broker at runtime and parse the JSON replies
## Control the Broker Over MQTT
Instead of editing files and restarting, you publish a command string to a system topic and the broker applies the change immediately: users, LoT entities, Routes, projects, secrets, and more.
**Like a remote control for the broker.** You send a message to a reserved topic; the broker answers on a matching output topic. No SSH and no restart.
### When to use broker commands
* Deploy or remove Actions, Models, Routes, and Rules without downtime
* Run an Action on demand, or inspect what is loaded
* Manage users, groups, environment variables, and secrets
* Load LoT notebook projects and check their status
* Drive the same verbs from [Coreflux HUB Command Console](/hub/system/command-console) or any MQTT client
On a [cluster](/latest/mqtt-broker/clustering), send **write** commands (add/remove/load) to the **leader**. Read-only and license commands still run on the node you are connected to.
***
Subscribe first, then publish. Replies are always JSON on a single output topic.
## Quick Start
Subscribe to `$SYS/Coreflux/Command/Output`, then publish the command. Use QoS 1 on both the command and the output subscription.
```bash wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
-addAction DEFINE ACTION Heartbeat ON EVERY 10 SECONDS DO PUBLISH TOPIC "system/heartbeat" WITH TIMESTAMP "UTC"
```
The broker replies with a JSON envelope. `success` is `true` when the Action was registered:
```json wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
{
"success": true,
"command": "AddAction",
"message": "Action 'Heartbeat' added successfully.",
"entityType": "Action",
"entityName": "Heartbeat",
"data": {}
}
```
List commands put the structured result in `data`. `message` stays a short human summary.
```bash wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
-listRoutes -requestId routes-1
```
```json wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
{
"success": true,
"command": "ListRoutes",
"requestId": "routes-1",
"message": "Listed 1 route(s).",
"data": [
{
"name": "CloudBridge",
"type": "MQTT_BRIDGE",
"connection": "Connected",
"health": "Green"
}
]
}
```
Every client shares `$SYS/Coreflux/Command/Output`. Pass `-requestId` so you can match the reply to the request you sent.
```bash wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
-listProjects -requestId demo-1
```
Read `requestId` on the envelope root. Omit the flag when you only have one command in flight.
## How Commands Work
| Role | Topic | Notes |
| ------------------------- | ------------------------------ | ------------------------------------------------------- |
| **Publish commands** | `$SYS/Coreflux/Command` | UTF-8 plain string payload (not a JSON wrapper) |
| **Subscribe for replies** | `$SYS/Coreflux/Command/Output` | Always this topic — independent of flags in the command |
```bash wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
-commandName [-requestId ]
```
| Flag | Required | Purpose |
| ------------ | -------- | ------------------------------------------------------------------------------------- |
| `-requestId` | No | Client correlation id; echoed as `requestId` in the JSON reply. Alias: `--request-id` |
Authorization uses the **MQTT session username** of the publishing client. Do not send `-requestUser` in the payload.
Replies always go to `$SYS/Coreflux/Command/Output`. Do not subscribe to `$SYS/Coreflux/CommandResponse`. Custom reply routing (`-bt`) and `$SYS/Coreflux/Cloud/Command` are not supported.
## Response Envelope
Every reply on `$SYS/Coreflux/Command/Output` is one JSON object. List and query payloads live in `data`. `data` is **always present**: `{}` when there is nothing structured to return, or `[]` for an empty list.
| Field | Type | Description |
| ------------ | --------------- | ------------------------------------------------------------------------------------- |
| `success` | boolean | Whether the command itself succeeded |
| `command` | string | Command name (`AddRoute`, `ListProjects`, …). Unknown verbs keep the raw `-verb` form |
| `message` | string | Human-readable summary |
| `entityType` | string? | Entity kind for deploy, remove, and run commands |
| `entityName` | string? | Entity name when applicable |
| `requestId` | string? | Echo of `-requestId` when you sent one |
| `data` | object or array | Structured result. Always present |
| `errors` | object\[]? | Present when `success` is `false` |
| `warnings` | string\[]? | Non-fatal warnings (for example a project load that skipped one entity) |
### How to parse a reply
1. Parse the payload as JSON (always a single object).
2. Read `success`, `command`, and optional `requestId` from the root.
3. Read structured results from `data`.
4. Read human text from `message` and failures from `errors[]`.
### Error objects
| Field | Description |
| ----------------- | -------------------------------------------- |
| `message` | Error text for that entry |
| `line` / `column` | 1-based source location for LoT parse errors |
| `sourceLines` | Map of line number to source text |
| `suggestions` | Parser hints when available |
Parse failure example:
```json wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
{
"success": false,
"command": "AddAction",
"message": "Unexpected token 'WITH' at line 4",
"entityType": "Action",
"entityName": "BadAction",
"data": {},
"errors": [
{
"message": "Unexpected token 'WITH' at line 4",
"line": 4,
"column": 12,
"sourceLines": { "4": "DO PUBLISH TOPIC \"x\"" }
}
]
}
```
### Global failures
**Permission denied** (command accepted at MQTT, then blocked by RBAC):
```json wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
{
"success": false,
"command": "AddRoute",
"message": "Permission denied: You are not allowed to execute -addRoute.",
"data": {},
"errors": [
{ "message": "Permission denied: You are not allowed to execute -addRoute." }
]
}
```
**Unknown command:**
```json wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
{
"success": false,
"command": "-notARealCommand",
"message": "Unknown command: -notARealCommand",
"data": {},
"errors": [{ "message": "Unknown command: -notARealCommand" }]
}
```
If the session lacks `CommandCall`, the publish to `$SYS/Coreflux/Command` can be rejected before any Output message. Handle both MQTT-level denial and JSON `success: false`. See [RBAC with LoT Rules](/latest/lot-language/rules/rbac).
***
## Command Index
Most verbs also accept a dashed alias (`-addRoute` / `--add-route`). The table lists the MQTT form you publish.
| Area | Commands |
| ----------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **Users** | `-addUser`, `-removeUser`, `-changeUserPassword`, `-changeUserSettings`, `-addUserToGroup`, `-removeUserFromGroup`, `-listUserGroups` |
| **Rules** | `-addRule`, `-removeRule`, `-removeAllProjectRules`, `-restoreRules` |
| **Models** | `-addModel`, `-removeModel`, `-removeAllModels` |
| **Actions** | `-addAction`, `-removeAction`, `-runAction`, `-removeAllActions`, `-actionTrace` |
| **Routes** | `-addRoute`, `-removeRoute`, `-removeAllRoutes`, `-listRoutes`, `-routeCode`, `-checkRouteConnection`, `-routeCertificates`, `-listTemplates`, `-connectionStatus`, `-discoverAIProviders` |
| **Diagnostics** | `-lotDiagnostic`, `-getPendingStatus` |
| **Python** | `-addPython`, `-removePython`, `-installPythonPackage`, `-listPythonPackages`, `-removeAllPythonScripts` |
| **Panels** | `-addPanel` (`-addVisu`), `-removePanel`, `-listPanels`, `-panelCode`, `-updateVisuState`, `-addVisuFile`, `-refreshVisu`, `-addTheme`, `-removeTheme`, `-removeAllThemes`, `-listThemes` |
| **Projects** | `-addProject`, `-removeProject`, `-loadProject`, `-unloadProject`, `-listProjects`, `-listEntities`, `-getProject`, `-duplicateProject`, `-projectCheckout`, `-projectPull`, `-projectPush`, `-projectStatus`, `-projectManifest` |
| **Env & secrets** | `-setEnv`, `-removeEnv`, `-listEnv`, `-setSecret`, `-removeSecret`, `-listSecrets` |
| **License** | `-showInstanceKey`, `-loadLicense`, `-installLicense` |
| **Audit** | `-auditStatus`, `-auditQuery`, `-auditFilter`, `-decryptAudit`, `-auditExport` |
| **System** | `-addTraceLog`, `-listTraceLogs`, `-removeTraceLog`, `-removeAllTraceLogs`, `-updateData`, `-h` |
`-updateData` refreshes broker `$SYS` statistics and **does not** publish a reply on `$SYS/Coreflux/Command/Output`.
***
## User Management
Create a user, then assign groups for `USER IN GROUP` in LoT Rules. Group commands require user-management permission on the **authenticated MQTT session**. See [RBAC with LoT Rules](/latest/lot-language/rules/rbac).
```bash wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
-addUser operator SecurePass123!
-removeUser operator
-changeUserPassword operator NewSecurePass456!
-changeUserSettings alice AllowedUserManagement true
-addUserToGroup alice operators
-removeUserFromGroup alice operators
-listUserGroups alice
```
`-listUserGroups` returns `{ username, groups, count }` in `data`.
Permission flags for `-changeUserSettings` include `AllowedUserManagement`, `AllowedSystemConfiguration`, and `AllowedLogManagement`.
***
## Rules
Rules define access control. See [Rules Overview](/latest/lot-language/rules/overview), [Rules Syntax](/latest/lot-language/rules/syntax), and [RBAC](/latest/lot-language/rules/rbac). **Lower priority number wins**. `-addRule` accepts priorities from `100` to `1 000 000` only; custom ACL rules require Growth or Enterprise.
Rules are LoT code and can also be deployed from a [LoT Notebook](/latest/quick-start/vscode) by running the cell that contains the definition.
```bash wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
-addRule DEFINE RULE RestrictSensorPublish WITH PRIORITY 100 FOR Publish TO TOPIC "sensors/#"
IF USER IN GROUP "operators" THEN
ALLOW
ELSE
DENY
```
```bash wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
-removeRule RestrictSensorPublish
```
Locked built-in system rules cannot be removed. Seed catch-alls (`AllowPublishTopic` / `AllowSubscribeTopic` / `AllowConnect`) may be removed to switch that operation toward default-deny — except Connect, where removing `AllowConnect` without a replacement leaves CONNECT ungated. See [RBAC](/latest/lot-language/rules/rbac#seed-catch-alls).
`-restoreRules` puts `AllowPublishTopic`, `AllowSubscribeTopic`, and `AllowConnect` back to their shipped ALLOW definitions. Missing seeds are re-added; overrides of those names are reset; locked built-ins and your other custom rules are left alone. Requires `root` or `AllowedSystemConfiguration`. No arguments.
```bash wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
-restoreRules
```
`-removeAllProjectRules` removes permission rules **owned by the active project**. Built-in rules and operator-authored custom rules stay in place.
***
## Models
```bash wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
-addModel DEFINE MODEL TemperatureSensor
WITH TOPIC "sensors/+/temperature"
ADD FIELD temperature WITH TYPE Float
ADD FIELD unit WITH TYPE String
ADD FIELD timestamp WITH TYPE DateTime
```
```bash wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
-removeModel TemperatureSensor
-removeAllModels
```
`-removeAllModels` removes every Model on the broker.
***
## Actions
```bash wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
-addAction DEFINE ACTION Heartbeat
ON EVERY 10 SECONDS DO
PUBLISH TOPIC "system/heartbeat" WITH TIMESTAMP "UTC"
```
```bash wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
-addAction DEFINE ACTION EchoMessage
ON TOPIC "input/data" DO
PUBLISH TOPIC "output/echo" WITH PAYLOAD
```
```bash wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
-removeAction Heartbeat
-removeAllActions
```
### Run an Action on demand
`-runAction` runs an Action immediately, regardless of its trigger.
```bash wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
-runAction Heartbeat
```
Success means the command **dispatched** the Action. For this form, a failure inside the Action body is not in the command reply — watch `$SYS/Coreflux/Actions//Trace` (if tracing is on) or `$SYS/Coreflux/Actions//Error`. `data.traceId` identifies this execution.
**Callable Actions** (those that declare `INPUT`) accept a JSON object of parameters:
```bash wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
-runAction SetZoneSetpoint {"zone":"f2-north","target":21.5}
```
The broker rejects a payload on a non-callable Action, invalid JSON, or a non-object JSON value.
**Topic-triggered Actions** (`ON TOPIC` / `ON CHANGE`) accept trigger context instead of `INPUT` bindings when the JSON object contains a `topic` key. The Action runs **synchronously**; `data` includes `status`, `published`, `durationMs`, `error`, and `traceId`. You do not need to arm `-actionTrace` for this form.
```bash wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
-runAction TemperatureMonitor {"topic":"sensors/zone1/temperature","payload":{"value":72}}
```
| `data.status` | Meaning |
| ------------------- | ------------------------------------------------------------------------------------------------- |
| `success` | Finished; `published` lists messages it emitted |
| `error` | The Action threw; `error` has the message. Envelope `success` stays `true` — the *command* worked |
| `no_trace_captured` | Ran, but no trace was produced |
The `topic` value must match the Action's trigger pattern (including wildcards). This `topic` form does not apply to callables — their JSON object is always `INPUT` bindings, even if it contains a `topic` key.
See [Callable ACTIONs](/latest/lot-language/actions/callable-actions) for `INPUT` / `CALL ACTION`.
### Action tracing
```bash wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
-actionTrace Heartbeat
-actionTrace all
-actionTrace status
-actionTrace off
-actionTrace remove Heartbeat
```
Traces publish to `$SYS/Coreflux/Actions//Trace`. Arming auto-expires; `data.expiresUtc` tells you when. A `{ "topic": … }` `-runAction` arms tracing for that one call and disarms afterward, unless a live `-actionTrace` session was already on.
***
## Routes
```bash wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
-addRoute DEFINE ROUTE CloudBridge WITH TYPE MQTT_BRIDGE
ADD SOURCE_CONFIG
WITH BROKER SELF
ADD DESTINATION_CONFIG
WITH BROKER_ADDRESS "iot.cloudprovider.com"
WITH BROKER_PORT '8883'
WITH USE_TLS true
ADD MAPPING sensorData
WITH SOURCE_TOPIC "sensors/#"
WITH DESTINATION_TOPIC "factory1/sensors/#"
WITH DIRECTION "out"
```
```bash wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
-removeRoute CloudBridge
-removeAllRoutes
-listRoutes
-routeCode POSTGRESQL
-listTemplates
-checkRouteConnection CloudBridge
-connectionStatus
```
`-listRoutes` places entries in `data` (`name`, `type`, `connection`, `health`). Unmonitored routes report `"Unknown"` for connection and health.
`-checkRouteConnection` tests the named Route. Details such as `routeType` and `connectionTime` are in `data`. What “connected” means depends on the Route type — see the Route page (for example [OPC UA Client](/latest/lot-language/routes/industrial/opcua-client) or [Modbus TCP Server](/latest/lot-language/routes/industrial/modbus-tcp-server)).
`-routeCode` returns template LoT for that Route type in `data`. `-listTemplates` lists available templates.
`-routeCertificates` manages PKI for Routes that support certificate stores (OPC UA client and server). The argument is `cert64:` plus base64 of a minified JSON request (`stores`, `list`, `inspect`, `trust`, `delete`, `export`, `import`). Operation details are in `data`. See [OPC UA Client — Certificate Management](/latest/lot-language/routes/industrial/opcua-client#certificate-management).
```bash wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
-routeCertificates cert64:
```
`-discoverAIProviders` probes configured LLM providers for reachability and models. API keys and host URLs come from secrets and environment variables (`OPENAI_API_KEY`, `MISTRAL_API_KEY`, `ANTHROPIC_API_KEY`, `OLLAMA_HOST`). An optional JSON object can override those values for the probe. Results are also published retained on `$SYS/Coreflux/AI/Providers`.
```bash wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
-discoverAIProviders
```
***
## Diagnostics
```bash wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
-lotDiagnostic action Heartbeat
-getPendingStatus
```
`-lotDiagnostic` takes an entity kind (`model`, `action`, `route`, or `rule`) and the entity name.
***
## Environment Variables and Secrets
See [Environment Variables & Secrets](/latest/mqtt-broker/secrets-and-env) for storage, substitution in LoT, and security notes.
```bash wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
-setEnv DB_HOST=postgres.example.com
-removeEnv OLD_CONFIG
-listEnv
-setSecret DB_PASSWORD=my_secure_password
-removeSecret OLD_API_KEY
-listSecrets
```
`-listEnv` returns name/value pairs in `data`. `-listSecrets` returns **names only** — secret values never appear in command output.
***
## Python Integration
```bash wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
-addPython Calculator
# Script Name: Calculator
def add(a, b):
return a + b
```
Python scripts must start with `# Script Name: YourScriptName`. That name is what you pass to `CALL PYTHON` from a LoT Action.
```bash wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
-removePython Calculator
-installPythonPackage numpy
-listPythonPackages
-removeAllPythonScripts
```
***
## Panels and Themes
Deploy dashboards written as `DEFINE PANEL`. `-addPanel` and `-addVisu` are the same command. See [Defining Panels](/latest/lot-language/panels/defining-panels).
```bash wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
-addPanel DEFINE PANEL LineOverview
WITH VISIBILITY PUBLIC
WITH STATE PUBLISHED
WITH TITLE "Line Overview"
WITH LAYOUT "grid" COLUMNS 3
ADD COMPONENT "Temperature" WITH TYPE "gauge"
SET RANGE FROM 0 TO 100
BIND VALUE TO TOPIC "sensor/temperature"
```
```bash wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
-removePanel LineOverview
-listPanels
-panelCode LineOverview
-updateVisuState LineOverview PUBLISHED
-refreshVisu
```
`-updateVisuState` takes `DRAFT`, `PUBLISHED`, or `ARCHIVED`.
`-addVisuFile lot64:` deploys every `DEFINE PANEL` block from a `.lot` file encoded as base64.
Theme commands: `-addTheme` (full `DEFINE THEME` body), `-removeTheme`, `-listThemes`, `-removeAllThemes`.
`-listPanels` and `-listThemes` also publish catalog data on `$SYS/Coreflux/VoT/Panels/` and `$SYS/Coreflux/VoT/Themes/`.
***
## Projects
A project is a folder of `.lotnb` files under the broker data path. See [Project organization](/latest/lot-language/usage-patterns/project-organization).
`-loadProject` and `-unloadProject` remove only entities the broker attributes to that project. Actions, Models, Routes, Python scripts, and panels you added with `-addAction` (and similar) while no project was active stay running.
```bash wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
-listProjects
-addProject https://git.example.com/factory-iot.git main
-addProject zip64:
-loadProject factory-iot
-unloadProject
-removeProject factory-iot
-listEntities
-getProject factory-iot
-duplicateProject factory-iot factory-iot-copy
-projectCheckout factory-iot main
-projectPull factory-iot
-projectPush factory-iot
-projectStatus factory-iot
-projectManifest factory-iot
```
Names may contain spaces or hyphens (`-loadProject Traceability System` or `-loadProject "Traceability System"`). `/` is not allowed.
`-listProjects` returns `name`, `active`, git metadata, and related fields in `data`.
`-loadProject` returns `{ project, entities, counts }` in `data`. `counts.actions` lists **user-defined** Actions only (not Route-internal event Actions). Built-in permission rules are omitted. Per-entity failures appear in `warnings[]`; `success` can still be `true` when the load finished with partial errors.
The last loaded project is remembered across restarts unless you pass `--project` at startup. See [Startup Options](/latest/mqtt-broker/startup-options#loading-a-project-at-startup).
`-unloadProject` tears down owned runtime entities and returns the broker to standalone mode. The folder stays on disk. `-removeProject` also deletes the folder.
`-getProject` exports a zip (base64) on `$SYS/Coreflux/Projects//download`.
`-projectManifest` returns hashes of stored files and declared entities so clients can compare a working copy without downloading the zip. The same JSON is published retained on `$SYS/Coreflux/Projects/{name}/manifest`.
***
## License
```bash wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
-showInstanceKey
-loadLicense /path/to/file.license
-installLicense
```
`-showInstanceKey` returns the instance key in `message`. `-installLicense` takes the license file text (the same content you would read from a `.license` file), which is the usual path from HUB and other UIs.
***
## Audit
```bash wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
-auditStatus
-auditQuery 2026-08-26
-auditQuery 2026-08-26 -auditFilter operationName=UserLogin
-decryptAudit 2026-08-26
```
`-auditQuery` takes a date (`YYYY-MM-DD`). Add `-auditFilter field=value` in the same payload to narrow results. `-decryptAudit` decrypts that day's log; pair with `-auditExport ` to write JSON to a file. Decryption requires the audit key configured on the broker (`COREFLUX_AUDIT_KEY`).
***
## Trace Logging
Capture broker log lines and republish matches to an MQTT topic.
```bash wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
-addTraceLog level=Information,lifetime=3600,messageContains=sensor,topic=logs/sensors
-listTraceLogs
-removeTraceLog logs/sensors
-removeAllTraceLogs
```
| Parameter | Description |
| ----------------- | --------------------------------------------------- |
| `level` | `Debug`, `Information`, `Warning`, `Error`, `Fatal` |
| `lifetime` | Seconds until the trace point expires |
| `messageContains` | Only logs whose message contains this substring |
| `topic` | MQTT topic where matching logs are published |
***
## Command Authorization
Not every user can run every command. Management gates such as `CommandCall` and per-entity creation flags are defined in LoT Rules. See [Rules overview](/latest/lot-language/rules/overview) and [RBAC](/latest/lot-language/rules/rbac).
Configure access control before any untrusted client can publish to `$SYS/Coreflux/Command`.
***
## Next Steps
Subscribe to command output, entity code, and broker telemetry.
Run the same commands from Coreflux HUB without a separate MQTT client.
# Broker Configuration
Source: https://docs.coreflux.org/latest/mqtt-broker/configuration
Complete guide to configuring the Coreflux MQTT Broker - ports, security, TLS, and deployment options
Configure your Coreflux MQTT Broker to match your deployment requirements. This guide covers network settings, security options, TLS certificates, and deployment-specific configurations.
Configuration changes require a broker restart to take effect. Plan configuration updates during maintenance windows for production systems.
To keep retained messages, persistent sessions, and queued QoS 1/2 across restarts, configure the separate [`Durability`](/latest/mqtt-broker/durability) section (or `COREFLUX_DURABILITY_*` environment variables).
***
## Configuration Overview
The broker configuration is stored as a JSON object. The structure below shows every supported property (the file is encrypted on disk; this is an illustrative representation):
```json wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
{
"Name": "Coreflux DataHub",
"Port": 1883,
"TlsPort": 8883,
"WebsocketPort": 5000,
"WebsocketPortTls": 443,
"BindIPForMQTT": "0.0.0.0",
"BindIPForMQTTwithTls": "0.0.0.0",
"BindIpForWebSockets": "0.0.0.0",
"BindIpForWebSocketsTls": "0.0.0.0",
"AnonymousLogin": false,
"CleanSession": false,
"SessionExpiryInterval": 0,
"MaxPendingMessagesPerClient": 20000,
"FullDebugLog": false,
"CloudLogging": false,
"ResendRetainTopics": false,
"ClientCertificationValidation": false,
"CheckCertificateChainValidation": false,
"CheckCertificateRevokationList": false,
"ServerCertificatePath": "",
"ServerCertificateKeyPath": "",
"ServerCertificatePassword": "",
"RootCACertificatePath": "",
"ClientCertificateSourcePath": "",
"ClientCertificateRevocationListPath": "",
"ProjectPath": "",
"UsersFile": "",
"EnvsPath": "",
"SecretsPath": ""
}
```
### First-Run Defaults
When no config file exists, the broker creates one with these values:
| Property | Default |
| --------------------------------- | ---------------------------- |
| `Name` | `"Coreflux DataHub"` |
| `Port` | `1883` |
| `TlsPort` | `8883` |
| `WebsocketPort` | `5000` |
| `WebsocketPortTls` | `443` |
| Bind IPs (MQTT / TLS / WebSocket) | `"0.0.0.0"` (all interfaces) |
| `AnonymousLogin` | `true` |
| `FullDebugLog` | `false` |
| `ClientCertificationValidation` | `false` |
| `MaxPendingMessagesPerClient` | `20000` |
| `CleanSession` | `false` |
| `SessionExpiryInterval` | `0` |
On a brand-new install the broker creates the config with `AnonymousLogin: true`. Set it to `false` and configure user credentials before exposing the broker to any untrusted network.
***
## Network Settings
### Ports
The broker listens on multiple ports for different connection types:
Standard MQTT port for unencrypted connections. This is the default port used by most MQTT clients.
MQTT port for TLS-encrypted connections. Requires valid server certificate configuration.
Port for MQTT over WebSocket connections. Used by browser-based clients and applications that cannot use raw TCP.
Port for MQTT over WebSocket with TLS encryption. Commonly set to 443 to work through corporate firewalls.
All ports must be between 1 and 65535. Ensure chosen ports are not already in use by other services.
### IP Bindings
Control which network interfaces the broker listens on:
IP address to bind for standard MQTT connections. Use `0.0.0.0` to listen on all interfaces, or a specific IP to restrict access.
IP address to bind for TLS-encrypted MQTT connections.
IP address to bind for WebSocket connections.
IP address to bind for WebSocket connections with TLS.
#### Example: Restrict to Local Network
```json wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
{
"BindIPForMQTT": "192.168.1.100",
"BindIPForMQTTwithTls": "192.168.1.100",
"BindIpForWebSockets": "192.168.1.100",
"BindIpForWebSocketsTls": "192.168.1.100"
}
```
#### Example: Listen on All Interfaces
```json wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
{
"BindIPForMQTT": "0.0.0.0",
"BindIPForMQTTwithTls": "0.0.0.0",
"BindIpForWebSockets": "0.0.0.0",
"BindIpForWebSocketsTls": "0.0.0.0"
}
```
***
## Security Settings
### Authentication
When `true`, clients may connect **without** a valid username/password, and unknown usernames are treated as anonymous. When `false`, only registered users in the users store can connect.
The class default is `false`, but on a brand-new install the broker writes the first-run config with `AnonymousLogin: true`. Enforced at runtime in `ValidateConnectionAsync`, which rejects unauthenticated connects when `false` and the user is not found.
Never leave `AnonymousLogin` enabled in production environments. Always require authentication for broker access.
### TLS Server Certificate
Configure TLS encryption for secure client connections:
Path to the server certificate file (PEM or PFX format). Required to enable the TLS listeners — MQTTS and WSS endpoints are only started when a server certificate is loaded successfully. If set but the file is missing, a warning is logged and the broker may still start without TLS.
Path to the **private key** file when the certificate and key are in separate PEM files. Leave empty if the key is embedded in the certificate file.
Password for an encrypted private key or PFX bundle. If `ServerCertificatePath` is set but this is empty, a warning is logged at startup.
Path to the root CA certificate for certificate chain validation.
#### Example: TLS Configuration
```json wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
{
"ServerCertificatePath": "/certs/server.crt",
"ServerCertificateKeyPath": "/certs/server.key",
"ServerCertificatePassword": "your-certificate-password",
"RootCACertificatePath": "/certs/ca.crt"
}
```
### Mutual TLS (mTLS)
For environments requiring client certificate authentication:
Directory or source containing the **trusted client CA / client certificate** material used during mTLS validation. If set but the path does not exist, a warning is logged at startup.
Enable client certificate validation. When `true`, TLS connections **require** a client certificate (`ClientCertificateMode.RequireCertificate`). When `false`, client certificates are optional (`AllowCertificate`).
Path to the Certificate Revocation List (CRL) file for checking revoked certificates.
Enable CRL checking for client certificates.
Enable full certificate chain validation for client certificates.
#### Example: mTLS Configuration
```json wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
{
"ServerCertificatePath": "/certs/server.crt",
"ServerCertificateKeyPath": "/certs/server.key",
"ServerCertificatePassword": "your-password",
"ClientCertificateSourcePath": "/certs/trusted-clients/",
"ClientCertificationValidation": true,
"CheckCertificateChainValidation": true,
"CheckCertificateRevokationList": true,
"ClientCertificateRevocationListPath": "/certs/crl.pem"
}
```
mTLS is recommended for high-security environments where you need to verify client identity at the transport layer, in addition to username/password authentication.
***
## Sessions and MQTT Behavior
**Broker policy** (not the client CONNECT flag).
* `false` — allow persistent sessions; honour each client's Clean Session / Clean Start flag.
* `true` — **require** clean sessions only; reject clients that connect with a persistent session (returns `NotAuthorized`).
Wired in via `ValidateConnectionAsync` (reject policy) and `WithPersistentSessions(!CleanSession)` at startup.
**Naming note:** `CleanSession = true` on the broker means "force clean sessions from clients", not "enable clean session".
**Clean Session enforcement** — per the MQTT 5.0 spec, a broker with `CleanSession = true` rejects any client connecting with `CleanSession = false` (persistent session). Make sure your clients connect with a clean session when this policy is enabled.
**MQTT 5 only** — maximum session lifetime cap, in seconds. Defines how long the broker holds a session after a client disconnects. (Previously hardcoded to 10 minutes; now configurable.)
* `0` — no cap; the client's CONNECT value is used as-is.
* `> 0` — reject persistent connects where the client requests **more** than this many seconds.
* `4294967295` (`uint.MaxValue`) — allow "never expire" client requests.
Applied in `MqttService.ApplyConfiguredSessionExpiry` during connection validation.
**MQTT 3.1.1:** ignored — persistence is controlled by `CleanSession` + `WithPersistentSessions` only.
**Important:** Coreflux has **no** server-builder API for session expiry. Clients that need MQTT 5 persistence must set `SessionExpiryInterval > 0` in their CONNECT packet.
When `true`, the broker maintains a buffer of retained messages and re-publishes them in specific situations: on new subscriptions (copies retained messages into an internal buffer) and via periodic `LogRetainedObjects` publishing. Useful when clients need retained messages re-delivered after reconnect or subscription changes.
***
## Performance and Back-Pressure
Maximum number of **pending outbound messages** queued per connected client before Coreflux drops messages (especially QoS 0). Protects broker memory when a client is slow to read.
Wired in via `WithMaxPendingMessagesPerClient` at startup.
`MaxPendingMessagesPerClient` must be between `1` and `100000`. A value outside this range prevents the broker from starting.
Per-route wait-or-drop when a LoT route cannot keep up is configured in LoT with `ADD ROUTE_CONFIG`. These environment variables set the broker-wide defaults (a restart is required to change them):
| Variable | Default | Effect |
| ---------------------------------------- | ---------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `COREFLUX_ROUTE_OVERLOAD_POLICY` | `balanced` | Default for `OVERLOAD_POLICY`: `lossless` (wait forever, never drop), `balanced` (wait up to the timeout, then drop the arriving message), or `realtime` (drop immediately). |
| `COREFLUX_ROUTE_CHANNEL_FULL_TIMEOUT_MS` | `50` | Default wait budget in milliseconds for `balanced`. |
| `COREFLUX_ROUTE_CHANNEL_CAPACITY` | `100000` | Shared ceiling on how many route updates the broker holds at once. Values below `1024` are raised to `1024`. |
See [Route Overload Configuration](/latest/lot-language/routes/route-config) for the LoT block, per-route overrides, and what happens under load.
***
## Deployment and Data Paths
These paths control where the broker stores its state. `ProjectPath` is the "base" that the other three resolve against.
Overrides the broker **data root** (`CFBasePath`) so all state (`bin`, `log`, `stats`, config, projects, users, envs, secrets) lives under this path instead of the default install directory. Typical for Kubernetes volumes or custom install layouts. If not set, the broker uses its install directory.
**Precedence:** `COREFLUX_DATA_PATH` (if set) wins over this field. `CONFIG_PATH` sets the initial data directory (used by Docker images) but a non-empty `ProjectPath` can still replace it unless `COREFLUX_DATA_PATH` is set. The `-p` / `--path` flag is applied late — prefer env vars.
Path to the file holding the broker's user accounts (the username/password credentials used when `AnonymousLogin` is `false`). When set and valid, users load from this **plaintext JSON** array instead of the encrypted user store, and runtime user changes are written back to this file.
Directory containing the `.env` file for environment variables the broker (and LOT logic) can reference at runtime — used to externalize deployment-specific values instead of hardcoding them in config. Resolves to `{EnvsPath}/.env`. Falls back to `CFBasePath` when empty. See [Environment Variables & Secrets](/latest/mqtt-broker/secrets-and-env) for usage.
Directory containing `secrets.json` — the secrets store for sensitive values (tokens, keys, credentials) kept separate from the main config so they aren't sitting in plain config JSON. Used by routes and LOT `GET SECRET`. Resolves to `{SecretsPath}/secrets.json`. Falls back to `CFBasePath` when empty. See [Environment Variables & Secrets](/latest/mqtt-broker/secrets-and-env) for usage.
***
## Logging
Enable detailed debug logging: connection validation details, every publish/subscription intercept, TCP health noise, etc. Useful for troubleshooting but may impact performance in production.
Only enable `FullDebugLog` for troubleshooting. Debug logging generates significant log volume and may impact broker performance.
When `true`, switches logging to **stdout-only structured JSON** (no rolling file sink). Intended for Docker/Kubernetes where log aggregators scrape container output. The `COREFLUX_CLOUD_LOGGING=true` environment variable also enables cloud logging.
***
## Additional Settings
A human-readable name for this DataHub / broker instance. Used for identification in logs and UI; does not affect MQTT protocol behaviour.
***
## Applying Configuration
### Via MQTT Topic
Publish a configuration JSON to the system topic using any MQTT client (such as MQTT Explorer). You can send a **partial update** — only the fields you include are changed; everything else is left as-is:
| Field | Value |
| --------- | ---------------------------------------------------- |
| **Topic** | `$SYS/Coreflux/Config/New` |
| **Auth** | Requires a user with system configuration permission |
**Payload:** Full or partial configuration JSON object
```json wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
{"Name":"ProductionBroker","Port":1883,"TlsPort":8883,"AnonymousLogin":false}
```
The runtime handler currently persists only a subset of fields (`AnonymousLogin`, bind IPs, ports, `FullDebugLog`). After publishing, you must restart the broker for changes to take effect — a restart is required for listener, session, and certificate changes to take full effect.
### Configuration Validation
The broker validates configuration on load. The following checks are **fatal** — an invalid value prevents the broker from starting:
| Check | Requirement |
| ----------------------------- | ---------------------------- |
| Port values | Must be between 1 and 65535 |
| IP addresses | Must be valid IPv4 addresses |
| `MaxPendingMessagesPerClient` | Must be between 1 and 100000 |
Certificate-related issues are **non-fatal**: they log a warning at startup and the broker continues without TLS rather than refusing to start.
| Condition | Behaviour |
| ----------------------------------------------------------------- | ----------------------------------------- |
| `ServerCertificatePath` set but file missing | Warning logged; TLS listeners not started |
| `ServerCertificatePath` set but `ServerCertificatePassword` empty | Warning logged |
| `ClientCertificateSourcePath` set but path does not exist | Warning logged |
### Related Environment Variables
These process environment variables override or complement the JSON file. Restart the broker after changing them. Details and examples: [Environment Variables & Secrets](/latest/mqtt-broker/secrets-and-env).
| Variable | Effect |
| ---------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------ |
| `COREFLUX_DATA_PATH` | Data directory, used as-is; takes precedence over `CONFIG_PATH` and `ProjectPath` |
| `CONFIG_PATH` | Data directory if `COREFLUX_DATA_PATH` is unset (Docker images set `/etc/project`) |
| `COREFLUX_CLOUD_LOGGING` | Enables stdout JSON logging (same intent as `CloudLogging`) |
| `COREFLUX_LOG_LEVEL` | Log verbosity (`Verbose` … `Fatal`) |
| `COREFLUX_SECRET_KEY` | Base64 32-byte key for encrypted secrets |
| `COREFLUX_AUDIT_KEY` | Base64 32-byte key for audit log encryption |
| `COREFLUX_OPS_ENABLED` | Enable ops HTTP on `COREFLUX_OPS_PORT` (default `9100`) |
| `COREFLUX_OPS_PORT` | Ops listener port |
| `COREFLUX_OPS_BIND_IP` | Ops bind address |
| `COREFLUX_ROUTE_OVERLOAD_POLICY` | Broker-wide default for route `OVERLOAD_POLICY`. See [Route Overload Configuration](/latest/lot-language/routes/route-config). |
| `COREFLUX_ROUTE_CHANNEL_FULL_TIMEOUT_MS` | Broker-wide default wait budget (ms) for `balanced` routes |
| `COREFLUX_ROUTE_CHANNEL_CAPACITY` | Shared ceiling on queued route updates (default `100000`) |
| `COREFLUX_DURABILITY_*` | See [MQTT State Durability](/latest/mqtt-broker/durability) |
| `COREFLUX_CLUSTER_*` | See [Clustering](/latest/mqtt-broker/clustering) |
| `COREFLUX_DISABLE_PYTHON` | Skip in-process Python when `true` |
***
## Docker Configuration
When running Coreflux in Docker, configure the broker using environment variables or mounted configuration files.
### Using Environment Variables
```bash wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
docker run --pull=always -d \
--name coreflux_broker \
-p 1883:1883 \
-p 5000:5000 \
-p 9100:9100 \
-v project-volume:/etc/project \
-v /path/to/certs:/certs \
coreflux/coreflux-mqtt-broker:latest
```
```yaml wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
version: '3.8'
services:
coreflux:
image: coreflux/coreflux-mqtt-broker:latest
container_name: coreflux
ports:
- "1883:1883"
- "8883:8883"
- "5000:5000"
- "443:443"
volumes:
- ./config:/config
- ./certs:/certs
restart: unless-stopped
```
### Mounting Configuration
Mount your configuration file to the container:
```bash wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
docker run -d \
-v /path/to/config.json:/app/config.json \
-p 1883:1883 \
coreflux/coreflux-mqtt-broker:latest
```
### TLS with Docker
When using TLS in Docker, ensure certificate paths in the configuration match the mounted paths inside the container:
```yaml wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
version: '3.8'
services:
coreflux:
image: coreflux/coreflux-mqtt-broker:latest
ports:
- "1883:1883"
- "8883:8883"
volumes:
- ./certs/server.crt:/certs/server.crt:ro
- ./certs/server.key:/certs/server.key:ro
- ./certs/ca.crt:/certs/ca.crt:ro
```
Configuration JSON should reference the container paths:
```json wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
{
"ServerCertificatePath": "/certs/server.crt",
"ServerCertificateKeyPath": "/certs/server.key",
"RootCACertificatePath": "/certs/ca.crt"
}
```
***
## Configuration Examples
### Development Environment
Minimal configuration for local development:
```json wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
{
"Name": "DevBroker",
"Port": 1883,
"AnonymousLogin": true,
"FullDebugLog": true,
"BindIPForMQTT": "127.0.0.1"
}
```
### Production Environment
Secure configuration for production deployment:
```json wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
{
"Name": "ProductionBroker",
"Port": 1883,
"TlsPort": 8883,
"WebsocketPort": 5000,
"WebsocketPortTls": 443,
"AnonymousLogin": false,
"FullDebugLog": false,
"BindIPForMQTT": "0.0.0.0",
"BindIPForMQTTwithTls": "0.0.0.0",
"BindIpForWebSockets": "0.0.0.0",
"BindIpForWebSocketsTls": "0.0.0.0",
"ServerCertificatePath": "/certs/server.crt",
"ServerCertificateKeyPath": "/certs/server.key",
"ServerCertificatePassword": "secure-password",
"RootCACertificatePath": "/certs/ca.crt"
}
```
### High-Security Environment
Configuration with mTLS for maximum security:
```json wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
{
"Name": "SecureBroker",
"Port": 1883,
"TlsPort": 8883,
"AnonymousLogin": false,
"FullDebugLog": false,
"BindIPForMQTT": "0.0.0.0",
"BindIPForMQTTwithTls": "0.0.0.0",
"ServerCertificatePath": "/certs/server.crt",
"ServerCertificateKeyPath": "/certs/server.key",
"ServerCertificatePassword": "secure-password",
"RootCACertificatePath": "/certs/ca.crt",
"ClientCertificateSourcePath": "/certs/trusted-clients/",
"ClientCertificationValidation": true,
"CheckCertificateChainValidation": true,
"CheckCertificateRevokationList": true,
"ClientCertificateRevocationListPath": "/certs/crl.pem"
}
```
***
## Next Steps
Learn how to manage the broker via MQTT commands.
Monitor broker status through system topics.
# MQTT State Durability
Source: https://docs.coreflux.org/latest/mqtt-broker/durability
Persist retained messages, sessions, and queued QoS 1/2 across broker restarts with the durability write-ahead log
## Keep MQTT state across restarts
By default, retained messages, persistent sessions, and queued QoS 1/2 traffic live in memory. A broker restart clears them. **Durability** writes that state to a local write-ahead log (WAL) so after a restart, clients can reconnect and pick up where they left off — their subscriptions, retained values, and any messages that were still waiting to be delivered.
**Like a notebook next to a whiteboard.** The whiteboard is fast working memory; the notebook is what you still have after the room goes dark. Durability is the notebook for MQTT state that must survive a restart.
### When to enable durability
Enable durability when you need:
* retained values to survive a broker restart;
* persistent sessions and their subscriptions restored after reboot; or
* queued QoS 1/2 messages re-offered to clients that reconnect with a persistent session.
Leave it off on constrained edge devices (slow flash, tight write budgets) or when restart persistence is not required.
Durability is a **broker-level** setting. It works on a standalone broker and on every cluster node. [High-availability clustering](/latest/mqtt-broker/clustering) builds on it for session failover — enable durability when you need subscriptions to follow a client that reconnects to another node.
***
Configure the `Durability` section (or `COREFLUX_DURABILITY_*` environment variables), restart the broker, and verify retained or session state after a controlled restart.
## Quick Start
Add a `Durability` block to the broker configuration, or set environment variables (env vars override the file). Restart the broker for changes to take effect.
Opt in with the default flush policy (group commit every 50 ms):
```json wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
{
"Durability": {
"Enabled": true,
"FsyncPolicy": "interval",
"FsyncIntervalMs": 50,
"Codec": "json"
}
}
```
Flush every append when you need the smallest power-loss window on a single node:
```json wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
{
"Durability": {
"Enabled": true,
"FsyncPolicy": "always",
"Codec": "json"
}
}
```
Tune without editing the config file (useful in containers):
```bash wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
export COREFLUX_DURABILITY_ENABLED=true
export COREFLUX_DURABILITY_FSYNC=interval
export COREFLUX_DURABILITY_FSYNC_INTERVAL_MS=50
export COREFLUX_DURABILITY_CODEC=json
# optional: export COREFLUX_DURABILITY_DATA_DIR=/var/lib/coreflux/mqttwal
```
***
## What is persisted
| State | Survives restart? | Notes |
| --------------------------------------- | ----------------- | ---------------------------------------------------------------------------------------------------- |
| **Retained messages** | Yes | Reloaded on boot on that node |
| **Persistent sessions + subscriptions** | Yes | A client that reconnects with a persistent session gets its subscriptions back |
| **Queued QoS 1/2 (unacked)** | Yes | Re-offered on session restore (**at-least-once** — a message acked at crash time may be redelivered) |
QoS 0 traffic is not made durable by this layer — MQTT treats it as best-effort.
***
## Configuration reference
| Config field | Env var | Default | Description |
| ------------------------ | --------------------------------------- | --------------------- | ---------------------------------------------------------- |
| `Enabled` | `COREFLUX_DURABILITY_ENABLED` | `false` | Master switch — opt in for restart durability |
| `FsyncPolicy` | `COREFLUX_DURABILITY_FSYNC` | `interval` | `interval` (group commit) or `always` (flush every append) |
| `FsyncIntervalMs` | `COREFLUX_DURABILITY_FSYNC_INTERVAL_MS` | `50` | Flush interval in ms when policy is `interval` |
| `Codec` | `COREFLUX_DURABILITY_CODEC` | `json` | Log encoding: `json` (readable) or `binary` (compact) |
| `DataDir` | `COREFLUX_DURABILITY_DATA_DIR` | `{data path}/mqttwal` | Root directory for durable MQTT state |
| `MaxSegmentBytes` | — | `8388608` (8 MiB) | Segment size that triggers rotation |
| `CheckpointEveryAppends` | — | `4096` | Appends between checkpoints that truncate older segments |
Accept `1` / `true` (case-insensitive) for `COREFLUX_DURABILITY_ENABLED`.
### Fsync policy
| Policy | Behavior | Trade-off |
| -------------------- | -------------------------------------------- | ------------------------------------------------------------------------------------------------- |
| `interval` (default) | Appends buffer and flush together on a timer | Bounded loss window on **power failure** only; process crashes still keep what the OS already had |
| `always` | Flush to disk on every append | Strongest single-node guarantee; highest I/O cost (costly on eMMC/SD edge devices) |
### Codec
* **`json` (default)** — human-readable records; easier to inspect during debugging.
* **`binary`** — compact encoding; smaller on disk and typically faster to encode/decode on busy brokers. Existing JSON segments still recover; new records append in binary. Switching back from binary to `json` for inspection of already-written binary segments is not supported — stay on `json` if you need text tooling on the log.
In a cluster, the same codec setting also applies to the replication logs used for quorum durability.
***
## Durability and clustering
Durability and clustering solve different loss modes:
| Concern | What protects you |
| ------------------------------------------------------------- | ------------------------------------------------------------------------------------------------- |
| **Single-node restart** | Durability WAL on that broker |
| **Minority node crash after an acknowledged QoS ≥ 1 publish** | Cluster [quorum replication](/latest/mqtt-broker/clustering#delivery-guarantees) |
| **Client reconnects to another node** | Session takeover — requires **`Durability.Enabled`** so subscriptions can hydrate on the new node |
**Offline queues stay on the node that hosted them.** Subscriptions can follow a client after failover (with durability enabled), but queued QoS 1/2 messages for offline clients do not migrate. See [clustering limitations](/latest/mqtt-broker/clustering#limitations).
***
## Observability
Prometheus `/metrics` exposes durability and WAL series, including:
| Metric family | What it shows |
| ----------------------------- | ------------------------------------------------------------------------------- |
| `coreflux_durability_enabled` | Whether durability is on |
| `coreflux_wal_*` | Appends, bytes written, fsyncs, last fsync latency |
| `coreflux_durable_*` | Counts for retained messages, sessions, in-flight messages, and in-flight drops |
Use these alongside your usual broker health checks after enabling durability or changing fsync policy.
***
## Limitations
**Durability is opt-in.** The default is `Enabled: false`. Without it, retained messages, sessions, and queued QoS 1/2 are lost on restart, and cluster session failover cannot restore subscriptions on another node.
**Enabling durability reduces QoS 1 throughput by about 20%.** The write-ahead log adds per-message work on the publish path. QoS 0 is unaffected. Leave durability off when you need maximum QoS 1 throughput and do not need restart persistence or cluster session failover.
**At-least-once after crash.** A QoS 1/2 message acknowledged at the exact moment of failure may be redelivered after restore. Design consumers to tolerate duplicates.
**Retained messages remain node-local.** Durability restores retains on the same node after restart; it does not create a cluster-wide retained store. See [clustering limitations](/latest/mqtt-broker/clustering#limitations).
***
## Next Steps
Run multiple brokers with quorum replication and session failover on top of durability.
Ports, TLS, listeners, and other broker settings.
# MQTT Broker
Source: https://docs.coreflux.org/latest/mqtt-broker/overview
Introduction to the Coreflux MQTT Broker - the core component of your IoT platform.
## Why Use the Coreflux MQTT Broker?
Your IoT devices need to communicate with each other and with your applications. Without a central hub, you'd need to manage complex point-to-point connections between every device—impossible to scale and maintain.
**Think of the broker as a smart post office.** Devices "mail" messages to topics (like addresses), and the broker delivers them to everyone who "subscribed" to that address. No device needs to know who else is listening.
The Coreflux MQTT Broker solves this by providing:
* **Reliable message delivery** between any number of devices
* **Built-in data processing** with LoT (Language of Things)
* **Security and access control** without external services
* **Edge computing** that reduces cloud dependency
***
## How Does an MQTT Broker Work?
An MQTT broker sits at the center of all communication. Instead of devices talking directly to each other, they all talk to the broker—and the broker handles delivery.
```mermaid theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
flowchart TB
subgraph Publishers
A[Temperature Sensor]
B[Pressure Sensor]
C[Motion Detector]
end
subgraph Broker
D[Coreflux MQTT Broker]
end
subgraph Subscribers
E[Dashboard App]
F[Alert System]
G[Database Logger]
end
A -->|"PUBLISH sensors/temp"| D
B -->|"PUBLISH sensors/pressure"| D
C -->|"PUBLISH sensors/motion"| D
D -->|"DELIVER"| E
D -->|"DELIVER"| F
D -->|"DELIVER"| G
```
### The Publish-Subscribe Pattern
MQTT uses a **publish-subscribe** (pub/sub) model:
1. **Publishers** send messages to **topics** (like `sensors/temperature`)
2. **Subscribers** register interest in topics they care about
3. The **broker** receives all messages and delivers them to matching subscribers
This decouples senders from receivers—a sensor doesn't need to know (or care) who's listening to its data.
### What the Broker Handles
| Function | What It Does |
| ------------------------- | -------------------------------------------------------------------------- |
| **Message Routing** | Receives messages from publishers, matches topics, delivers to subscribers |
| **Connection Management** | Maintains client connections, handles reconnects, manages session state |
| **Quality of Service** | Ensures delivery guarantees (at-most-once, at-least-once, exactly-once) |
| **Message Retention** | Optionally stores the last message on a topic for new subscribers |
| **Security** | Authenticates clients and enforces access permissions |
***
## The Coreflux Advantage
Standard MQTT brokers route messages—that's it. The Coreflux MQTT Broker goes further with **built-in intelligence** using the LoT (Language of Things) runtime:
### Beyond Message Routing
* **[LoT Actions](/latest/lot-language/actions/overview)** — React to messages automatically. When a temperature exceeds a threshold, trigger an alert without external code.
* **[LoT Models](/latest/lot-language/models/overview)** — Transform and structure data in real-time. Convert raw sensor values into formatted JSON with timestamps and metadata.
* **[LoT Routes](/latest/lot-language/routes/overview)** — Connect to external systems. Bridge to other brokers, store data in databases, call REST APIs—all configured in LoT.
* **[LoT Rules](/latest/lot-language/rules/overview)** — Control access with fine-grained permissions. Define who can publish, subscribe, or manage the broker.
### Processing at the Edge
Instead of sending all data to the cloud for processing, the Coreflux broker processes data **where it's generated**:
* **Lower latency** — Local actions execute in milliseconds, no round-trip to the cloud
* **Reduced bandwidth** — Only filtered or aggregated data leaves the edge
* **Offline resilience** — Critical logic keeps running even if the internet drops
* **Stronger privacy** — Sensitive data stays on-premises by default
***
## Key Benefits
MQTT's lightweight protocol is designed for constrained devices and limited bandwidth. The Coreflux broker optimizes message delivery for challenging network conditions—perfect for industrial environments, remote sites, or battery-powered devices.
**Coreflux enhancement:** Built-in message compression and intelligent batching reduce bandwidth by up to 70% compared to raw MQTT.
IoT security is non-negotiable. The Coreflux broker includes TLS encryption, certificate-based authentication, and username/password auth out of the box.
**Coreflux enhancement:** LoT Rules provide fine-grained access control—define exactly who can publish, subscribe, or manage each topic pattern.
Raw sensor data is rarely useful as-is. It needs filtering, transformation, and enrichment before applications can use it.
**Coreflux enhancement:** LoT Models and Actions process data in real-time at the edge. No external ETL pipelines or stream processors required.
IoT systems don't exist in isolation. They connect to databases, APIs, dashboards, and other brokers.
**Coreflux enhancement:** LoT Routes provide native integration with SQL databases, REST APIs, industrial protocols (Modbus, OPC-UA, S7), and other MQTT brokers.
***
## Coreflux MQTT Broker as a Platform
The Coreflux MQTT Broker is more than a message router — it is a complete IoT platform running at the edge:
| Layer | What You Get |
| ------------------ | ---------------------------------------------------------------------------------------------------------------- |
| **MQTT Messaging** | Reliable pub/sub communication with QoS guarantees, session management, and retained messages |
| **Security** | TLS encryption, client authentication, and fine-grained topic-level access control via LoT Rules |
| **LoT Runtime** | Built-in automation, data transformation, external integrations, and access policies — no external code required |
| **Administration** | Configuration, logging, metrics, and maintenance through MQTT-native management topics |
***
## Next Steps
Learn the fundamentals of MQTT messaging.
Install and configure your Coreflux broker.
# MQTT Protocol
Source: https://docs.coreflux.org/latest/mqtt-broker/protocol
Understanding the MQTT protocol - the foundation of IoT communication.
## The Language of IoT
MQTT (Message Queuing Telemetry Transport) is a lightweight, publish-subscribe messaging protocol designed for constrained devices and low-bandwidth, high-latency networks. It has become the de facto standard for IoT communication.
Before diving into Coreflux features like LoT Actions and Models, you need to understand how MQTT works. Every Coreflux capability builds on these fundamentals—topics, subscriptions, QoS, and message patterns.
**Think of MQTT like a radio station.** Publishers broadcast on a frequency (topic), and anyone who tunes in (subscribes) hears the message. The broker is the transmission tower that makes it all work.
MQTT is designed to be simple, lightweight, and easy to implement. A minimal MQTT control message can be as small as 2 bytes, making it ideal for resource-constrained devices.
***
## The Publish-Subscribe Pattern
Unlike traditional request-response protocols (like HTTP), MQTT uses a publish-subscribe (pub/sub) pattern. This decouples the sender (publisher) from the receiver (subscriber), allowing for more flexible and scalable architectures.
```mermaid theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
flowchart LR
subgraph Publishers
A[Temperature Sensor]
B[Humidity Sensor]
end
subgraph Broker
C[Coreflux MQTT Broker]
end
subgraph Subscribers
D[Mobile App]
E[Dashboard]
F[Database]
end
A -->|"PUBLISH sensors/temp"| C
B -->|"PUBLISH sensors/humidity"| C
C -->|"DELIVER"| D
C -->|"DELIVER"| E
C -->|"DELIVER"| F
```
### How It Works
A client publishes a message to a specific topic on the broker.
The broker receives the message and checks which clients are subscribed to that topic.
The broker forwards the message to all subscribers of that topic.
***
## Topics
Topics are the routing mechanism in MQTT. They are UTF-8 strings that the broker uses to filter messages for each connected client.
### Topic Structure
Topics are hierarchical, using forward slashes (`/`) as level separators. This hierarchy is powerful for organizing data—especially in industrial settings using patterns like the Unified Namespace (UNS).
```
building/floor1/room101/temperature
building/floor1/room101/humidity
building/floor2/room201/temperature
```
### Topic Tree Example
A well-designed topic tree organizes your entire system logically:
```mermaid theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
flowchart LR
A[enterprise] --> B[site] --> C[area] --> D[line] --> E[cell] --> F[device] --> G[measurement]
```
Here's how this maps to real topics:
```
enterprise/
├── factory-munich/
│ ├── production/
│ │ ├── line-1/
│ │ │ ├── cnc-machine-01/
│ │ │ │ ├── temperature
│ │ │ │ ├── vibration
│ │ │ │ └── status
│ │ │ └── robot-arm-02/
│ │ │ ├── position
│ │ │ └── status
│ │ └── line-2/
│ │ └── ...
│ └── quality/
│ └── ...
└── factory-berlin/
└── ...
```
Design your topic hierarchy carefully. A well-structured topic tree—often called a Unified Namespace (UNS)—makes it easier to subscribe to related data, manage access control, and scale your system.
### Topic Best Practices
| Practice | Example | Description |
| -------------------------- | ---------------------------------- | --------------------------------- |
| Use hierarchical structure | `factory/line1/machine1/status` | Organize topics logically |
| Be specific | `sensors/temp/celsius` | Avoid overly generic topics |
| Use lowercase | `home/living-room/light` | Consistent naming convention |
| Avoid leading slashes | `home/kitchen` not `/home/kitchen` | Leading slash creates empty level |
***
## Wildcards
MQTT supports two wildcard characters for subscribing to multiple topics at once.
The `+` wildcard matches **exactly one** topic level.
**Subscription:** `building/floor1/+/temperature`
**Matches:** `building/floor1/room101/temperature`, `building/floor1/room102/temperature`, `building/floor1/lobby/temperature`
**Does NOT match:** `building/floor2/room201/temperature` (different floor), `building/floor1/room101/humidity` (different measurement), `building/floor1/west/room101/temperature` (extra level)
The `#` wildcard matches **any number of levels** and must be at the end.
**Subscription:** `building/floor1/#`
**Matches:** `building/floor1/room101/temperature`, `building/floor1/room101/humidity`, `building/floor1/room102/status`, `building/floor1/lobby/motion/detected`
**Does NOT match:** `building/floor2/room201/temperature` (different floor), `building/basement/storage/humidity` (different path)
Use wildcards carefully. Subscribing to `#` alone would receive ALL messages on the broker, which could overwhelm your client and create security risks.
***
## Quality of Service (QoS)
MQTT defines three levels of Quality of Service for message delivery. Choose based on your reliability requirements and network conditions.
### QoS 0 — At Most Once
**Fire and forget.** The message is sent once with no acknowledgment.
```mermaid theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
sequenceDiagram
participant P as Publisher
participant B as Broker
participant S as Subscriber
P->>B: PUBLISH
B->>S: PUBLISH
```
| Aspect | Detail |
| ------------------ | ------------------------------------------------------------ |
| **Delivery** | At most once (may be lost) |
| **Acknowledgment** | None |
| **Performance** | Fastest |
| **Use when** | Frequent sensor readings where occasional loss is acceptable |
**Best for:** High-frequency telemetry data like temperature readings every second. Missing one reading doesn't matter when the next arrives immediately.
### QoS 1 — At Least Once
**Acknowledged delivery.** The sender stores the message until the broker confirms receipt.
```mermaid theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
sequenceDiagram
participant P as Publisher
participant B as Broker
participant S as Subscriber
P->>B: PUBLISH
B->>S: PUBLISH
B->>P: PUBACK
```
| Aspect | Detail |
| ------------------ | ----------------------------------------------------------- |
| **Delivery** | At least once (may duplicate) |
| **Acknowledgment** | PUBACK from broker |
| **Performance** | Moderate |
| **Use when** | Important data where your application can handle duplicates |
**Best for:** Event logs, alerts, or status updates. If the same alert arrives twice, your system should deduplicate it.
### QoS 2 — Exactly Once
**Assured delivery.** A four-way handshake ensures the message arrives exactly once.
```mermaid theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
sequenceDiagram
participant P as Publisher
participant B as Broker
P->>B: PUBLISH
B->>P: PUBREC
P->>B: PUBREL
B->>P: PUBCOMP
```
| Aspect | Detail |
| ------------------ | --------------------------------------------- |
| **Delivery** | Exactly once (guaranteed, no duplicates) |
| **Acknowledgment** | PUBREC → PUBREL → PUBCOMP handshake |
| **Performance** | Slowest |
| **Use when** | Critical data where duplicates cause problems |
**Best for:** Financial transactions, billing events, or commands where duplicate execution would be harmful (e.g., "dispense medication").
***
## Retained Messages
When a message is published with the retain flag set, the broker stores the last message for that topic. Any new subscriber immediately receives this retained message upon subscribing.
Retained messages are useful for status topics. A new client connecting can immediately know the current state without waiting for the next update.
**Example use cases:**
* Device status (online/offline)
* Current configuration values
* Last known sensor readings
***
## Last Will and Testament (LWT)
The Last Will and Testament feature allows a client to specify a message that the broker should publish if the client disconnects unexpectedly. This is essential for detecting device failures.
### How LWT Works
```mermaid theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
sequenceDiagram
participant D as Device
participant B as Broker
participant M as Monitoring System
D->>B: CONNECT with LWT
Note right of B: LWT stored: "devices/sensor-01/status" = "offline"
D->>B: PUBLISH status = "online"
B->>M: status = "online"
Note over D: Device crashes or loses network
B->>M: LWT triggered: status = "offline"
```
When connecting, the client specifies its LWT message, topic, QoS, and retain flag.
The broker holds the LWT message as long as the client stays connected.
If the client disconnects without sending a proper DISCONNECT packet (crash, network loss), the broker publishes the LWT message.
### LWT Configuration Example
A typical pattern for device status monitoring:
| Setting | Value | Purpose |
| --------------- | -------------------------- | -------------------------------------- |
| **LWT Topic** | `devices/sensor-01/status` | Where to publish if device disconnects |
| **LWT Message** | `offline` | What to publish |
| **LWT Retain** | `true` | Keep message for new subscribers |
| **LWT QoS** | `1` | Ensure delivery to monitoring systems |
When the device connects, it publishes `online` to the same topic (retained). If it crashes, the broker publishes `offline`. Monitoring systems always know the current state.
***
## Clean Sessions
MQTT supports two session types:
| Session Type | Clean Session Flag | Behavior |
| ---------------------- | ------------------ | ---------------------------------------------------------------------------------- |
| **Clean Session** | `true` | No state is stored between connections. Subscriptions are discarded on disconnect. |
| **Persistent Session** | `false` | Broker stores subscriptions and queued messages (QoS 1/2) for the client. |
Persistent sessions are useful for mobile devices or intermittent connections where you don't want to miss messages during brief disconnections.
***
## Keep Alive
The keep-alive mechanism ensures the connection remains active and detects dead connections quickly.
```mermaid theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
sequenceDiagram
participant C as Client
participant B as Broker
C->>B: CONNECT (keep-alive: 60s)
Note over C,B: Normal data exchange
C->>B: PUBLISH
B->>C: PUBACK
Note over C,B: No data to send... approaching keep-alive timeout
C->>B: PINGREQ
B->>C: PINGRESP
Note over C,B: Connection confirmed alive
C->>B: PINGREQ
Note over B: No PINGRESP for 1.5x keep-alive
Note over B: Connection declared dead, LWT published
```
### How It Works
| Step | Description |
| --------------- | ----------------------------------------------------------------------------------------------- |
| **1. Connect** | Client specifies a keep-alive interval (in seconds) when connecting |
| **2. Activity** | Client must send a control packet within each keep-alive period |
| **3. Ping** | If no data is being sent, the client sends PINGREQ |
| **4. Response** | The broker responds with PINGRESP |
| **5. Timeout** | If no communication occurs within 1.5x the keep-alive period, the connection is considered dead |
Choose keep-alive values based on your network. Shorter intervals (15-30s) detect failures faster but use more bandwidth. Longer intervals (60-120s) are better for mobile networks with variable latency.
***
## Next Steps
Learn how to control the Coreflux broker via MQTT commands.
Explore system topics for monitoring and control.
# Environment Variables & Secrets
Source: https://docs.coreflux.org/latest/mqtt-broker/secrets-and-env
Manage configuration, credentials, and sensitive data securely with environment variables and encrypted secrets
## Stop Hardcoding Credentials
Every IoT deployment needs connection strings, API keys, and passwords -- but embedding them directly in your LoT code or route definitions creates security risks and makes your configuration impossible to reuse across environments. The Coreflux Broker solves this with built-in environment variable and secrets management.
* **Environment variables** store non-sensitive configuration (hostnames, ports, URLs) in a plain-text `.env` file
* **Secrets** store sensitive credentials (passwords, API keys, tokens) encrypted at rest with **AES-256-GCM**
Both are accessible from LoT Actions, Models, Routes, and broker CLI commands using a unified syntax -- no external tools or services required.
**Think of it like a vault and a notebook.** Environment variables are the notebook -- quick to read, easy to update, visible if someone opens the file. Secrets are the vault -- locked with a key, encrypted on disk, only decrypted when the broker needs them at runtime.
Environment variables and secrets management is available from **Coreflux Broker v1.9.3** and above.
## When to Use Each
| Scenario | Use | Why |
| ------------------------- | ------------------------ | -------------------------------------------------- |
| Database host, port, name | **Environment variable** | Non-sensitive, changes per environment |
| API base URLs | **Environment variable** | Configuration that varies between dev/staging/prod |
| Feature flags | **Environment variable** | Simple on/off toggles |
| Database passwords | **Secret** | Must not be readable on disk |
| API keys and tokens | **Secret** | Sensitive credentials |
| TLS passphrases | **Secret** | Security-critical values |
***
## Config Root
The broker picks its **data directory** (config root) in this order — **first match wins**:
1. `COREFLUX_DATA_PATH` (used as-is)
2. `CONFIG_PATH` (used as-is; this is what official Docker images set)
3. `-p` / `--path` (used as-is, applied later in startup — prefer the env vars)
4. Default: `{install directory}/Coreflux`
When an env var is set, **`/Coreflux` is not appended**. With `CONFIG_PATH=/etc/project`, files live at `/etc/project/.env`, not `/etc/project/Coreflux/.env`. The `/Coreflux` suffix is only added in the default case (no env var and no `-p`).
| Install | Typical data directory |
| ------------------------- | --------------------------------------------------------------------------- |
| Docker / Compose | `/etc/project` (`CONFIG_PATH` in the image; mount your volume here) |
| Linux APT/DNF | `/var/lib/coreflux` (`COREFLUX_DATA_PATH` in `coreflux-broker.service`) |
| Windows / portable binary | `{folder with the executable}/Coreflux` unless you set `COREFLUX_DATA_PATH` |
### File Layout
Environment and secrets files live in the data directory:
```
{data directory}/
.env # Plain-text environment variables (key=value)
secrets.json # AES-256-GCM encrypted secrets
secret.key # Optional: encryption key (base64, 32 bytes)
bin/ # Internal broker data (managed automatically)
log/ # Log files
stats/ # Statistics
```
***
## Broker process environment variables
These names are read by the **broker process** (Docker `environment:`, systemd `Environment=`, Kubernetes `env:`). They are not the same as values you store in `.env` for `GET ENV` — though a name set in the process environment is still visible to `GET ENV` through the fallback chain.
Restart the broker after changing them.
### Data path and logging
| Variable | Default | Description |
| ------------------------ | -------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `COREFLUX_DATA_PATH` | `{install}/Coreflux` | Data directory, used as-is. Wins over `CONFIG_PATH` and over the `ProjectPath` config field. |
| `CONFIG_PATH` | — | Same role as `COREFLUX_DATA_PATH` if that variable is unset. Official images set this to `/etc/project`. A non-empty `ProjectPath` in broker JSON can still override it unless `COREFLUX_DATA_PATH` is set. |
| `COREFLUX_CLOUD_LOGGING` | auto in containers | `true` / `1` forces stdout JSON logs. Also on when the process runs in a container. |
| `COREFLUX_LOG_LEVEL` | `Information` | Serilog level (`Verbose`, `Debug`, `Information`, `Warning`, `Error`, `Fatal`). |
### Secrets and audit
| Variable | Default | Description |
| --------------------- | ----------- | ----------------------------------------------------------------------------------------------- |
| `COREFLUX_SECRET_KEY` | derived key | Base64-encoded 32-byte key for `secrets.json`. Preferred in Docker/Kubernetes. |
| `COREFLUX_AUDIT_KEY` | derived key | Base64-encoded 32-byte key for encrypted audit logs (or use `audit.key` in the data directory). |
### Operational HTTP (`/health`, `/ready`, `/info`, `/metrics`)
| Variable | Default | Description |
| ---------------------- | --------- | -------------------------------------------------------------- |
| `COREFLUX_OPS_ENABLED` | `true` | Set `false` / `0` to disable the ops listener. |
| `COREFLUX_OPS_PORT` | `9100` | Ops TCP port. Do not publish this port to the public internet. |
| `COREFLUX_OPS_BIND_IP` | `0.0.0.0` | Ops bind address. Use `127.0.0.1` to keep probes on localhost. |
Env vars override the matching fields in broker JSON (`OpsEnabled`, `OpsPort`, `OpsBindIP`).
### Routes, durability, clustering, Python
| Variable | Description |
| ---------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------- |
| `COREFLUX_ROUTE_OVERLOAD_POLICY` | Broker-wide default `lossless`, `balanced`, or `realtime`. See [Route Overload Configuration](/latest/lot-language/routes/route-config). |
| `COREFLUX_ROUTE_CHANNEL_FULL_TIMEOUT_MS` | Wait budget (ms) for `balanced` (default `50`). |
| `COREFLUX_ROUTE_CHANNEL_CAPACITY` | Shared queue ceiling (default `100000`). |
| `COREFLUX_DURABILITY_*` | Restart persistence for retained messages and sessions. See [MQTT State Durability](/latest/mqtt-broker/durability). |
| `COREFLUX_CLUSTER_*` | High-availability clustering. See [Clustering](/latest/mqtt-broker/clustering). |
| `COREFLUX_DISABLE_PYTHON` | `true` skips in-process Python (`CALL PYTHON`). |
***
## Environment Variables
### Storage
Environment variables are stored in plain text in `{data directory}/.env` using standard key=value format:
```env wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
DB_HOST=postgres.example.com
DB_PORT=5432
API_BASE_URL=https://api.example.com
MQTT_BRIDGE_HOST=remote-broker.company.com
```
### Resolution Order
When the broker resolves an environment variable (via `GET ENV`, route config, or CLI), it checks multiple sources in this order:
| Priority | Source | Description |
| -------- | --------------------- | ----------------------------------------------------------- |
| 1 | `.env` file (managed) | Set via `-setEnv` or `KEEP ENV`. Persisted to disk. |
| 2 | Process environment | Docker `environment:`, Kubernetes `env:`, or shell exports. |
| 3 | User environment | OS user-level environment variables (non-Docker). |
| 4 | Machine environment | OS machine-level environment variables (non-Docker). |
If not found in any source, an empty string is returned.
This fallback chain means Docker `environment:` variables work even without a `.env` file -- the broker picks them up from the process environment automatically.
***
## Secrets
### How Secrets Work
Secrets are encrypted at rest using **AES-256-GCM** and stored in `{data directory}/secrets.json`. Each secret entry contains the initialization vector, the encrypted value with authentication tag, and a creation timestamp:
```json wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
{
"DB_PASSWORD": {
"IV": "base64...",
"Value": "base64(ciphertext):base64(tag)",
"Created": "2026-02-08T12:00:00Z"
}
}
```
Secret values are only decrypted at runtime when accessed via `GET SECRET` or route configuration. They are never logged or displayed -- even the `-listSecrets` command only shows secret names, never values.
### Encryption Key
The broker resolves the encryption key in this order:
| Priority | Source | Notes |
| -------- | ---------------------------------- | ------------------------------------------------------------------------------------------------------- |
| 1 | `COREFLUX_SECRET_KEY` env var | Best for Docker/Kubernetes. Base64-encoded, 32 bytes. |
| 2 | `{data directory}/secret.key` file | Good for mounted volumes. Base64-encoded, 32 bytes. |
| 3 | Derived from machine GUID | Auto-generated fallback. Machine-specific -- secrets become unreadable if moved to a different machine. |
#### Generating a Key
Use OpenSSL to generate a cryptographically secure 32-byte key:
```bash wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
openssl rand -base64 32
# Example output: K7x2mN8pQ1rT5uW3yB6dF9hJ0kL4nP7sV2xZ8cG1eA=
```
If you use the derived key fallback (no explicit key), your secrets are tied to the specific machine. Moving the `secrets.json` file to another machine or container will make the secrets unreadable. Always use an explicit key for portable deployments.
***
## Using in LoT
Environment variables and secrets can be read, written, and deleted from within LoT Actions, Models, and route configurations.
### Reading Values
Use `GET ENV` to read an environment variable and `GET SECRET` to read an encrypted secret (decrypted at runtime):
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
-- Read configuration from environment
SET "myHost" WITH (GET ENV "DB_HOST")
SET "myPort" WITH (GET ENV "DB_PORT")
-- Read credentials from encrypted secrets
SET "dbPass" WITH (GET SECRET "DB_PASSWORD")
SET "apiKey" WITH (GET SECRET "API_KEY")
```
### Persisting Values
Use `KEEP ENV` to save a new environment variable to the `.env` file, and `KEEP SECRET` to encrypt and store a new secret:
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
-- Save an environment variable (persisted to .env)
KEEP ENV "LAST_RUN" WITH TIMESTAMP "UTC"
-- Save an encrypted secret (persisted to secrets.json)
KEEP SECRET "API_KEY" WITH "sk-abc123"
```
### Deleting Values
Use `DELETE ENV` and `DELETE SECRET` to remove values:
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
-- Remove an environment variable
DELETE ENV "OLD_CONFIG"
-- Remove a secret
DELETE SECRET "OLD_API_KEY"
```
### Complete Example
This action reads database configuration from environment variables and credentials from secrets on broker startup, then reports the status:
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
DEFINE ACTION ConnectToDatabase
ON START DO
-- Read connection settings from environment
SET "host" WITH (GET ENV "DB_HOST")
SET "port" WITH (GET ENV "DB_PORT")
SET "dbName" WITH (GET ENV "DB_NAME")
-- Read credentials from encrypted secrets
SET "user" WITH (GET SECRET "DB_USER")
SET "pass" WITH (GET SECRET "DB_PASSWORD")
IF {host} == EMPTY THEN
PUBLISH TOPIC "system/errors" WITH "DB_HOST not configured"
ELSE
PUBLISH TOPIC "system/status" WITH "Database configured: " + {host} + ":" + {port}
-- Track when we last initialized
KEEP ENV "LAST_DB_INIT" WITH TIMESTAMP "UTC"
```
***
## Using in Routes
Environment variables and secrets can be referenced directly in route definitions, keeping connection details out of your LoT code.
### Standard Syntax
Use `GET ENV` and `GET SECRET` in route configuration blocks to inject values at runtime:
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
DEFINE ROUTE MyDB WITH TYPE POSTGRESQL
ADD SQL_CONFIG
WITH SERVER GET ENV "DB_HOST"
WITH PORT 5432
WITH DATABASE GET ENV "DB_NAME"
WITH USERNAME GET ENV "DB_USER"
WITH PASSWORD GET SECRET "DB_PASSWORD"
```
### Legacy Prefix Syntax
The `ENV:` and `SECRET:` prefix syntax is also supported for backward compatibility in route `ADD CONFIG` sections:
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
WITH SERVER ENV:DB_HOST
WITH PASSWORD SECRET:DB_PASSWORD
```
### Event Query Placeholders
In route event queries (e.g. `WITH QUERY`), use curly-brace placeholders that are resolved at execution time:
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
ADD EVENT StoreReading
WITH SOURCE_TOPIC "sensors/#"
WITH QUERY "INSERT INTO readings (host, value) VALUES ('{env.DB_HOST}', '{payload}')"
```
| Placeholder | Resolves To |
| --------------- | -------------------------- |
| `{env.NAME}` | Environment variable value |
| `{secret.NAME}` | Decrypted secret value |
***
## CLI Commands
Manage environment variables and secrets at runtime by publishing commands to `$SYS/Coreflux/Command` using any MQTT client (such as MQTT Explorer).
### Environment Variable Commands
| Command | Description |
| -------------------- | ------------------------------------------------------ |
| `-setEnv NAME=value` | Set an environment variable (persisted to `.env` file) |
| `-removeEnv NAME` | Remove an environment variable |
| `-listEnv` | List all managed environment variables |
Publish these messages to `$SYS/Coreflux/Command` to manage environment variables:
```bash wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
# Set an environment variable
-setEnv DB_HOST=postgres.example.com
# List all environment variables
-listEnv
# Remove an environment variable
-removeEnv OLD_CONFIG
```
### Secret Commands
| Command | Description |
| ----------------------- | -------------------------------------------------------- |
| `-setSecret NAME=value` | Set a secret (encrypted and persisted to `secrets.json`) |
| `-removeSecret NAME` | Remove a secret |
| `-listSecrets` | List secret names (values are never shown) |
Publish these messages to `$SYS/Coreflux/Command` to manage secrets:
```bash wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
# Set a secret
-setSecret DB_PASSWORD=my_secure_password
# List secret names
-listSecrets
# Remove a secret
-removeSecret OLD_API_KEY
```
Subscribe to `$SYS/Coreflux/Command/Output` to receive confirmation responses from commands.
***
## Deployment
Set up environment variables and secrets across different deployment targets.
On Windows, the data directory defaults to the broker's executable folder + `\Coreflux` unless `COREFLUX_DATA_PATH` is set (used as-is).
If the broker is at `C:\Coreflux\CorefluxMQTTBroker.exe`, the default data directory is `C:\Coreflux\Coreflux\`.
To pin a custom path, set a machine environment variable (no extra `\Coreflux` is appended):
```powershell wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
[Environment]::SetEnvironmentVariable("COREFLUX_DATA_PATH", "C:\CorefluxData", "Machine")
```
Files then live directly under `C:\CorefluxData\`.
Create a `.env` file in the data directory:
```powershell wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
@"
DB_HOST=postgres.example.com
DB_PORT=5432
API_BASE_URL=https://api.example.com
"@ | Out-File -FilePath "C:\CorefluxData\.env" -Encoding UTF8
```
Generate and save a `secret.key` file for encrypting secrets:
```powershell wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
openssl rand -base64 32 > "C:\CorefluxData\secret.key"
```
Start the broker, then set secrets using any MQTT client (such as MQTT Explorer). Publish to `$SYS/Coreflux/Command`:
```
-setSecret DB_PASSWORD=my_secure_password
```
Subscribe to `$SYS/Coreflux/Command/Output` to confirm the secret was stored.
APT/DNF installs already set `COREFLUX_DATA_PATH=/var/lib/coreflux` on `coreflux-broker.service`. Put `.env` and `secret.key` there.
Write your environment variables and restrict permissions:
```bash wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
sudo tee /var/lib/coreflux/.env << 'EOF'
DB_HOST=postgres.example.com
DB_PORT=5432
API_BASE_URL=https://api.example.com
EOF
sudo chown coreflux:coreflux /var/lib/coreflux/.env
sudo chmod 600 /var/lib/coreflux/.env
```
```bash wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
openssl rand -base64 32 | sudo tee /var/lib/coreflux/secret.key >/dev/null
sudo chown coreflux:coreflux /var/lib/coreflux/secret.key
sudo chmod 600 /var/lib/coreflux/secret.key
```
```bash wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
sudo systemctl restart coreflux-broker
```
Using any MQTT client, publish to `$SYS/Coreflux/Command`:
```
-setSecret DB_PASSWORD=my_secure_password
```
Verify with `-listSecrets` -- the secret name should appear in the output.
### Option 1: Mount a Config Volume (Recommended)
Pre-create the config files on the host and mount them into the container.
**1. Prepare the host directory:**
Create the config directory, generate an encryption key, and write your environment variables:
```bash wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
mkdir -p ./coreflux-data
# Generate encryption key
openssl rand -base64 32 > ./coreflux-data/secret.key
# Create .env file
cat > ./coreflux-data/.env << 'EOF'
DB_HOST=postgres.example.com
DB_PORT=5432
API_BASE_URL=https://api.example.com
EOF
```
**2. Docker Compose file:**
Mount the host directory at `/etc/project` (the image's `CONFIG_PATH`):
```yaml wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
services:
coreflux-broker:
image: coreflux/coreflux-mqtt-broker:latest
container_name: coreflux-broker
ports:
- "1883:1883"
- "8883:8883"
- "5000:5000"
- "9100:9100"
volumes:
- ./coreflux-data:/etc/project
restart: unless-stopped
```
The broker resolves:
* `CONFIG_PATH=/etc/project` (baked into the image) — data directory is `/etc/project`
* `.env` from `/etc/project/.env` (your `./coreflux-data/.env`)
* `secret.key` from `/etc/project/secret.key`
* All broker state persists in the mounted directory
**3. Set secrets after the broker starts:**
Using any MQTT client, publish to `$SYS/Coreflux/Command`:
```
-setSecret DB_PASSWORD=my_secure_password
```
### Option 2: Pass Env Vars via Docker (No .env File)
If you prefer not to manage a `.env` file, pass variables directly through Docker's `environment:`. The resolution fallback to process environment picks them up:
```yaml wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
services:
coreflux-broker:
image: coreflux/coreflux-mqtt-broker:latest
ports:
- "1883:1883"
- "9100:9100"
environment:
- DB_HOST=postgres.example.com
- DB_PORT=5432
- API_BASE_URL=https://api.example.com
- COREFLUX_SECRET_KEY=K7x2mN8pQ1rT5uW3yB6dF9hJ0kL4nP7sV2xZ8cG1eA=
volumes:
- coreflux-data:/etc/project
volumes:
coreflux-data:
```
Variables set this way:
* Are available to LoT via `GET ENV "DB_HOST"`
* Are available to routes via `GET ENV "DB_HOST"` and legacy `ENV:DB_HOST`
* Are **not** persisted to `.env` (they exist only in the process environment)
* Will be lost if the container restarts without the same `environment:` block
Secrets still need a volume for `secrets.json` persistence, even when passing the encryption key via `COREFLUX_SECRET_KEY`.
Use Kubernetes ConfigMaps for environment variables and Kubernetes Secrets for the encryption key.
**1. ConfigMap for environment variables:**
Store non-sensitive configuration in a ConfigMap:
```yaml wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
apiVersion: v1
kind: ConfigMap
metadata:
name: coreflux-env
data:
DB_HOST: "postgres.cluster.local"
DB_PORT: "5432"
API_BASE_URL: "https://api.example.com"
```
**2. Kubernetes Secret for the encryption key:**
Store the encryption key in a Kubernetes Secret (base64-encoded):
```yaml wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
apiVersion: v1
kind: Secret
metadata:
name: coreflux-secrets
type: Opaque
data:
COREFLUX_SECRET_KEY: SzdsMm1OOHBRMXJUNXVXM3lCNmRGOWhKMGtMNG5QN3NWMnhaOGNHMWVBPQ==
```
**3. Deployment:**
Inject the ConfigMap as environment variables via `envFrom`, and the encryption key from the Kubernetes Secret:
```yaml wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
apiVersion: apps/v1
kind: Deployment
metadata:
name: coreflux-broker
spec:
replicas: 1
selector:
matchLabels:
app: coreflux-broker
template:
metadata:
labels:
app: coreflux-broker
spec:
containers:
- name: broker
image: coreflux/coreflux-mqtt-broker:latest
ports:
- containerPort: 1883
- containerPort: 5000
- containerPort: 9100
env:
- name: CONFIG_PATH
value: "/etc/project"
- name: COREFLUX_SECRET_KEY
valueFrom:
secretKeyRef:
name: coreflux-secrets
key: COREFLUX_SECRET_KEY
envFrom:
- configMapRef:
name: coreflux-env
volumeMounts:
- name: data
mountPath: /etc/project
volumes:
- name: data
persistentVolumeClaim:
claimName: coreflux-pvc
```
**4. PersistentVolumeClaim:**
Provide persistent storage for broker data, secrets, and configuration:
```yaml wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: coreflux-pvc
spec:
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 1Gi
```
Apply all resources:
```bash wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
kubectl apply -f coreflux-configmap.yaml
kubectl apply -f coreflux-secret.yaml
kubectl apply -f coreflux-pvc.yaml
kubectl apply -f coreflux-deployment.yaml
```
ConfigMap values are injected as process environment variables via `envFrom`, so `GET ENV` picks them up through the fallback chain. The `COREFLUX_SECRET_KEY` from the Kubernetes Secret enables portable secret encryption.
***
## Best Practices
Never rely on the derived key fallback for production deployments. Always provide a `COREFLUX_SECRET_KEY` environment variable or a `secret.key` file. The derived key is machine-specific -- if you migrate, scale, or recreate a container, your secrets become unreadable.
On Linux, restrict access to sensitive files to the broker's service user only:
```bash wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
chmod 600 /var/lib/coreflux/.env
chmod 600 /var/lib/coreflux/secret.key
chmod 600 /var/lib/coreflux/secrets.json
```
Even for values that seem static (like a database port), use `GET ENV` so you can change configuration without redeploying LoT code. This makes your Actions and Routes portable across dev, staging, and production.
Always mount a persistent volume at the data directory (`/etc/project` in Docker, `/var/lib/coreflux` on Linux packages). Without a volume, `.env`, `secrets.json`, and broker state are lost when the container is recreated.
Keep `secret.key`, `secrets.json`, and `.env` files out of Git. Add them to `.gitignore` and use your deployment pipeline to inject them at runtime.
***
## Resolution Reference
### Where Each Syntax Reads From
| Syntax | Where It Works | Resolved By |
| ------------------- | -------------------------------- | -------------------------------------- |
| `GET ENV "NAME"` | Actions, Models, Route configs | `.env` file, then process env fallback |
| `GET SECRET "NAME"` | Actions, Models, Route configs | Decrypts from `secrets.json` |
| `ENV:NAME` | Route `ADD CONFIG` sections only | Same as `GET ENV` |
| `SECRET:NAME` | Route `ADD CONFIG` sections only | Same as `GET SECRET` |
| `{env.NAME}` | Event queries (`WITH QUERY`) | Replaced at event execution time |
| `{secret.NAME}` | Event queries (`WITH QUERY`) | Replaced at event execution time |
### Encryption Key Resolution Order
| Priority | Source | Notes |
| -------- | ---------------------------------- | ------------------------------------------------------------------------------------------ |
| 1 | `COREFLUX_SECRET_KEY` env var | Best for Docker/Kubernetes. Base64-encoded, 32 bytes. |
| 2 | `{data directory}/secret.key` file | Good for mounted volumes. Base64-encoded, 32 bytes. |
| 3 | Derived from machine GUID | Auto-generated fallback. Machine-specific -- secrets won't decrypt on a different machine. |
### GetEnv Resolution Order
| Priority | Source | Notes |
| -------- | --------------------- | ----------------------------------------------------------- |
| 1 | `.env` file (managed) | Set via `-setEnv` or `KEEP ENV`. Persisted to disk. |
| 2 | Process environment | Docker `environment:`, Kubernetes `env:`, or shell exports. |
| 3 | User environment | OS user-level env vars (non-Docker). |
| 4 | Machine environment | OS machine-level env vars (non-Docker). |
***
## Next Steps
Full reference for all broker CLI commands including env and secret management.
See GET ENV and GET SECRET in the complete LoT operations reference.
# Startup Options & Deployment
Source: https://docs.coreflux.org/latest/mqtt-broker/startup-options
Start the broker with CLI flags, projects, and packages — Docker, Linux systemd, and Windows services
## Configure the broker before it starts
Pass configuration at process start so containers, packages, and CI pipelines come up the same way every time. Typical jobs:
* Point the data directory at a persistent volume
* Import broker or user JSON on first boot
* Load a LoT notebook project from git or disk
* Install a license file before MQTT accepts connections
**Think of startup options as a recipe card.** You tell the broker where its data lives and which project to load; it applies that recipe and then stays up as a service or container.
For install commands (Docker, APT, DNF, Windows), start with [Installation](/latest/quick-start/installation). For `COREFLUX_*` variables, secrets, and `.env` files, see [Environment Variables & Secrets](/latest/mqtt-broker/secrets-and-env).
***
## Command-line options
### Configuration
| Option | Long form | Description |
| ------ | ---------------- | ---------------------------------------------------------------------------------------------------------------------------------------- |
| `-c` | `--config` | Path to broker configuration JSON. Applied only when no broker config exists yet, unless you also pass `--config-force`. |
| `-cf` | `--config-force` | Overwrite existing broker configuration with `--config`. |
| `-u` | `--users` | Path to users JSON. Applied only when no users file exists yet, unless you also pass `--users-force`. |
| `-uf` | `--users-force` | Overwrite existing users with `--users`. |
| `-p` | `--path` | Data directory (same role as `COREFLUX_DATA_PATH`). Prefer the environment variable so the path is set **before** `--config` is applied. |
Set `COREFLUX_DATA_PATH` or `CONFIG_PATH` in the process environment, then start the binary. `-p` / `--path` is applied late in startup, so `--config` can write into the default data directory instead of the path you intended.
### Projects and licenses
Load LoT (Language of Things) from a **project** (git URL, local folder, or a project already stored on this broker). That is the supported way to ship Actions, Models, Routes, and Rules at boot.
| Option | Long form | Description |
| ------------------ | --------------------- | --------------------------------------------------------------------------------------------- |
| | `--project` | Load a project at startup (git URL, local path, or existing project name). |
| | `--branch` | Git branch or tag to check out with `--project`. |
| `-loadLicense` | `--load-license` | Copy a license file into the data directory so the broker can validate it on start. |
| `-showInstanceKey` | `--show-instance-key` | Print the instance key to stdout and exit (needed to request a license). Does not start MQTT. |
A project loaded with `--project` (or `-loadProject` at runtime) is remembered across restarts. On the next start, if you do **not** pass `--project`, the broker re-activates that project. `-unloadProject` clears it. `--project` always wins over the remembered project.
### Other options
| Option | Long form | Description |
| ------ | ----------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `-I` | `--install` | Register a Windows service or a Linux systemd unit for a **portable** (zip/tarball) binary. Skip this when you installed from APT/DNF — the package already ships `coreflux-broker.service`. |
| `-h` | `--help` | Display help. |
***
## Basic usage
### Starting with configuration files
```bash wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
# Start with broker configuration
./CorefluxMQTTBroker --config /path/to/broker-config.json
# Start with users configuration
./CorefluxMQTTBroker --users /path/to/users.json
# Start with both
./CorefluxMQTTBroker \
--config /path/to/broker-config.json \
--users /path/to/users.json
```
```powershell wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
# Start with broker configuration
.\CorefluxMQTTBroker.exe --config C:\config\broker-config.json
# Start with users configuration
.\CorefluxMQTTBroker.exe --users C:\config\users.json
# Start with both
.\CorefluxMQTTBroker.exe `
--config C:\config\broker-config.json `
--users C:\config\users.json
```
### Force overwrite existing configuration
By default, if configuration already exists in the data directory, startup JSON is ignored. Use force flags to overwrite:
```bash wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
# Force overwrite broker config
./CorefluxMQTTBroker --config /path/to/broker-config.json --config-force
# Force overwrite users config
./CorefluxMQTTBroker --users /path/to/users.json --users-force
# Force overwrite both (short form)
./CorefluxMQTTBroker -c /path/to/broker-config.json -cf -u /path/to/users.json -uf
```
Force flags overwrite existing configurations. Back up your current configuration before using them.
### Loading a project at startup
```bash wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
# From git URL
./CorefluxMQTTBroker --project https://github.com/org/my-iot-project.git --branch production
# From local path
./CorefluxMQTTBroker --project /path/to/my-project
# By project name (already stored on this broker)
./CorefluxMQTTBroker --project my-iot-project
```
Package Actions, Models, Routes, and Rules in a LoT notebook project and load it with `--project`. That is the supported bootstrap path for LoT at startup.
***
## Configuration files
### Broker configuration (JSON)
The broker configuration file defines MQTT service settings:
```json wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
{
"Name": "MyCorefluxBroker",
"Port": 1883,
"TlsPort": 8883,
"WebsocketPort": 5000,
"WebsocketPortTls": 5443,
"BindIPForMQTT": "0.0.0.0",
"BindIPForMQTTwithTls": "0.0.0.0",
"BindIpForWebSockets": "0.0.0.0",
"BindIpForWebSocketsTls": "0.0.0.0",
"AnonymousLogin": false,
"FullDebugLog": false,
"ServerCertificatePath": "/path/to/certificate.pfx",
"ServerCertificatePassword": "your-certificate-password",
"ResendRetainTopics": false
}
```
For every supported field (ports, TLS/mTLS, sessions, performance, data paths, ops, durability), see [Broker Configuration](/latest/mqtt-broker/configuration).
### Users configuration (JSON)
The users configuration file defines MQTT users and their permissions:
```json wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
[
{
"UserName": "root",
"Password": "your-secure-password",
"AllowedBaseTopic": "#",
"AllowedSystemConfiguration": true,
"AllowedUserManagement": true,
"AllowedLogManagement": true
},
{
"UserName": "device-user",
"Password": "device-password",
"AllowedBaseTopic": "devices/",
"AllowedSystemConfiguration": false,
"AllowedUserManagement": false,
"AllowedLogManagement": false
}
]
```
| Property | Type | Description |
| ---------------------------- | ------ | --------------------------------------------- |
| `UserName` | string | MQTT username |
| `Password` | string | MQTT password |
| `AllowedBaseTopic` | string | Topic access restriction (`#` for all topics) |
| `AllowedSystemConfiguration` | bool | Allow system configuration changes |
| `AllowedUserManagement` | bool | Allow user management operations |
| `AllowedLogManagement` | bool | Allow log management operations |
***
## Container deployment
Official images use `CONFIG_PATH=/etc/project`. Persist that directory so configuration, secrets, and projects survive image updates. The published image is `coreflux/coreflux-mqtt-broker`.
Same pattern as [Installation](/latest/quick-start/installation): named volume at `/etc/project`, MQTT on `1883`, WebSocket on `5000`, ops on `9100`.
```bash wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
docker run --pull=always -d --name coreflux_broker \
-p 1883:1883 -p 5000:5000 -p 9100:9100 \
-v project-volume:/etc/project \
coreflux/coreflux-mqtt-broker:latest \
--project https://github.com/org/my-iot-project.git --branch main
```
Import JSON on first boot by mounting files and passing `--config` / `--users` (add `--config-force` / `--users-force` only when you intend to replace existing state):
```bash wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
docker run --pull=always -d --name coreflux_broker \
-p 1883:1883 -p 5000:5000 -p 9100:9100 \
-v project-volume:/etc/project \
-v /host/config:/config:ro \
coreflux/coreflux-mqtt-broker:latest \
--config /config/broker-config.json \
--users /config/users.json
```
Probe liveness with `curl http://localhost:9100/health`. The image has no shell, so do not use `exec` healthchecks.
Use the Compose file from [Installation](/latest/quick-start/installation). To load a project, add `command` on the broker service:
```yaml wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
services:
broker:
image: coreflux/coreflux-mqtt-broker:latest
ports:
- "1883:1883"
- "5000:5000"
- "9100:9100"
volumes:
- broker-data:/etc/project
command:
- "--project"
- "https://github.com/org/my-iot-project.git"
- "--branch"
- "main"
restart: unless-stopped
volumes:
broker-data:
```
The image already sets `CONFIG_PATH=/etc/project`. Override `COREFLUX_DATA_PATH` only if you change the mount path.
Mount a PVC at `/etc/project`. Use HTTP probes on port `9100` (the image has no shell).
```yaml wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
apiVersion: apps/v1
kind: Deployment
metadata:
name: coreflux-broker
spec:
replicas: 1
selector:
matchLabels:
app: coreflux-broker
template:
metadata:
labels:
app: coreflux-broker
spec:
containers:
- name: broker
image: coreflux/coreflux-mqtt-broker:latest
args:
- "--project"
- "https://github.com/org/my-iot-project.git"
- "--branch"
- "production"
ports:
- containerPort: 1883
name: mqtt
- containerPort: 5000
name: websocket
- containerPort: 9100
name: ops
env:
- name: CONFIG_PATH
value: "/etc/project"
volumeMounts:
- name: data
mountPath: /etc/project
livenessProbe:
httpGet:
path: /health
port: 9100
initialDelaySeconds: 5
periodSeconds: 15
readinessProbe:
httpGet:
path: /ready
port: 9100
initialDelaySeconds: 5
periodSeconds: 10
volumes:
- name: data
persistentVolumeClaim:
claimName: coreflux-data
```
***
## Service installation
Prefer the install method from [Installation](/latest/quick-start/installation). Use `--install` only for portable binaries.
APT and DNF packages already register **`coreflux-broker.service`**, install under `/opt/coreflux-broker`, and set `COREFLUX_DATA_PATH=/var/lib/coreflux`.
```bash wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
sudo systemctl status coreflux-broker
sudo systemctl restart coreflux-broker
sudo journalctl -u coreflux-broker -f
```
To add startup flags (for example `--project`), override the unit:
```bash wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
sudo systemctl edit coreflux-broker
```
```ini wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
[Service]
ExecStart=
ExecStart=/opt/coreflux-broker/CorefluxMQTTBroker --project https://github.com/org/my-iot-project.git --branch main
```
Then `sudo systemctl daemon-reload && sudo systemctl restart coreflux-broker`.
[Installation](/latest/quick-start/installation) registers the service with `sc create`. For a portable zip, you can instead run as Administrator:
```powershell wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
.\CorefluxMQTTBroker.exe --install
net start CorefluxMQTTBroker
Get-Service CorefluxMQTTBroker
```
To apply JSON once before the service owns the process, run the broker in the foreground with `--config` / `--users`, stop it, then install or start the service.
For a tarball (not APT/DNF), run as root:
```bash wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
sudo ./CorefluxMQTTBroker --install
```
That writes a systemd unit. For production, still prefer [packages.coreflux.org](https://packages.coreflux.org) so upgrades and `COREFLUX_DATA_PATH` stay consistent.
***
## Best practices
Use `COREFLUX_DATA_PATH` or `CONFIG_PATH` so `--config` and `--users` land in the same directory the broker uses at runtime. Docker images already set `CONFIG_PATH=/etc/project`.
`-cf` and `-uf` overwrite existing configuration. Back up first.
Load Actions, Models, Routes, and Rules with `--project`, not with ad-hoc `.lot` file flags at process start.
Always mount a volume at `/etc/project` in Docker and Kubernetes. Without it, configuration, secrets, and projects disappear when the container is recreated.
***
## Troubleshooting
1. Confirm `COREFLUX_DATA_PATH` / `CONFIG_PATH` matches the volume you mounted.
2. If files already exist in that directory, add `--config-force` / `--users-force` only if you intend to replace them.
3. Check logs for validation errors.
```bash wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
sudo lsof -i :1883
```
On Windows: `netstat -ano | findstr :1883`
Stop the other listener or change the broker port in configuration.
The package service user is `coreflux`, with state under `/var/lib/coreflux`. The service user needs read access to certificates and write access to the data directory.
***
## Next Steps
Ports, TLS, sessions, ops, and every JSON field.
Data path, COREFLUX\_\* variables, .env, and encrypted secrets.
# System ($SYS) Topics
Source: https://docs.coreflux.org/latest/mqtt-broker/sys-topics
Monitor the broker, read configuration, and control Coreflux over MQTT system topics
## See the Broker From MQTT
You do not need a separate monitoring stack to know what the broker is doing. Subscribe to `$SYS` topics and you get version, license, connected clients, LoT source, route health, and project state as MQTT messages — the same way you already consume sensor data.
**Think of `$SYS` as the broker's dashboard in topic form.** Subscribe like any other MQTT topic and watch live status, without opening a separate UI.
### When to use this
| Goal | Start here |
| --------------------------------------------------- | ----------------------------------------------------------------------- |
| Confirm the broker is up and which build is running | `$SYS/Coreflux/Version` |
| Send management commands | `$SYS/Coreflux/Command` and `Command/Output` |
| Inspect or back up LoT source | `$SYS/Coreflux/Actions/#`, `Models/#`, `Routes/#`, `Rules/#`, `Visus/#` |
| Watch route health | `$SYS/Coreflux/Routes/+/status` |
| See which project is loaded | `$SYS/Coreflux/Projects/active` |
Topics that start with `$` are not included when you subscribe to `#` at the root. Subscribe to `$SYS/#` (or a more specific `$SYS/…` filter) explicitly.
***
The tables below are the map; later sections explain payloads and who may publish or subscribe.
## Topic Hierarchy
```
$SYS/
├── Coreflux/
│ ├── Command # Publish commands
│ ├── Command/Output # JSON command replies
│ ├── Config # Live configuration (includes Python status)
│ ├── Config/New # Publish a config update
│ ├── Config/New/Result # Result of a config update
│ ├── Config/Users # User list (passwords masked)
│ ├── Version # Broker version
│ ├── License # License and usage
│ ├── Resources/ # Memory totals
│ ├── Comms/
│ │ ├── Sessions # MQTT sessions
│ │ ├── Clients # Connected clients
│ │ ├── Logins # Login events
│ │ └── PacketTrace/… # Optional packet traces
│ ├── Actions/ # Action LoT
│ ├── Models/ # Model LoT
│ ├── Routes/ # Route LoT
│ ├── Rules/ # Rule LoT
│ ├── Visus/ # Panel LoT
│ ├── Projects/ # Project list, active, manifest, download
│ ├── Entities/ownership # Project vs ad-hoc origin
│ ├── Cluster/ # Role, leader, members (when clustering)
│ ├── Python/Scripts # Registered Python scripts
│ └── AI/Providers # Discovered LLM providers
└── / # e.g. Linux-x64, Windows-x64
├── Python/Info
└── Comms/TCP|UDP/…
```
### Read-only (broker → client)
| Topic | Content |
| ------------------------------------ | -------------------------------------------------------- |
| `$SYS/Coreflux/Version` | Version JSON (**retained**) |
| `$SYS/Coreflux/License` | License, tier, limits, current usage (**retained**) |
| `$SYS/Coreflux/Config` | Live configuration JSON, including Python backend status |
| `$SYS/Coreflux/Config/Users` | Users with passwords masked; includes `Groups` |
| `$SYS/Coreflux/Config/New/Result` | Outcome of the last publish to `Config/New` |
| `$SYS/Coreflux/Comms/Sessions` | Active sessions |
| `$SYS/Coreflux/Comms/Clients` | Connected clients |
| `$SYS/Coreflux/Comms/Logins` | Login event lines |
| `$SYS/Coreflux/Actions/{name}` | Action LoT (**retained**) |
| `$SYS/Coreflux/Models/{name}` | Model LoT (**retained**) |
| `$SYS/Coreflux/Routes/{name}` | Route LoT (**retained**) |
| `$SYS/Coreflux/Routes/{name}/status` | Route health JSON |
| `$SYS/Coreflux/Rules/{name}` | Rule LoT |
| `$SYS/Coreflux/Visus/{name}` | Panel source (**retained**) |
| `$SYS/Coreflux/Projects/list` | Stored projects (**retained**) |
| `$SYS/Coreflux/Projects/active` | Loaded project (**retained**) |
| `$SYS/Coreflux/Entities/ownership` | Running entities → project origin (**retained**) |
### Writable (client → broker)
| Topic | Purpose |
| ---------------------------------------------------- | ------------------------------------------------------------------- |
| `$SYS/Coreflux/Command` | Run [broker commands](/latest/mqtt-broker/commands) |
| `$SYS/Coreflux/Config/New` | Apply a configuration JSON (restart still required for most fields) |
| `$SYS/Coreflux/Actions/{name}/Run` | Run a callable Action (panels use this) |
| `$SYS/Coreflux/Actions/{name}/Invoke` | Same as `/Run` (legacy alias) |
| `$SYS/Coreflux/Routes/{name}/Events/{event}/Trigger` | Fire a named route event from a panel |
***
## Command Topics
Send a command and read the reply over MQTT. Subscribe to the output topic **before** you publish, or you can miss a fast reply.
Using any MQTT client (such as MQTT Explorer), subscribe to `$SYS/Coreflux/Command/Output`.
Publish the command string to `$SYS/Coreflux/Command`. Add `-requestId ` when several commands may be in flight.
Every reply is one JSON object on `Command/Output`. Match `requestId` when you sent one.
### `$SYS/Coreflux/Command`
**Direction:** Client → Broker
| Field | Value |
| ----------- | ----------------------------------------------------------------------------------------------------------------- |
| **Topic** | `$SYS/Coreflux/Command` |
| **Payload** | `-addAction DEFINE ACTION Heartbeat ON EVERY 10 SECONDS DO PUBLISH TOPIC "system/heartbeat" WITH TIMESTAMP "UTC"` |
| **Auth** | Session allowed for `CommandCall` (`root` or the matching RBAC permission flags) |
### `$SYS/Coreflux/Command/Output`
**Direction:** Broker → Client
Replies always use this JSON envelope. Structured list/query results are in `data`. `data` is always present (`{}` when there is nothing to return, or `[]` for an empty list).
```json wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
{
"success": true,
"command": "AddAction",
"message": "Action 'Heartbeat' added successfully.",
"entityType": "Action",
"entityName": "Heartbeat",
"data": {}
}
```
| Field | Description |
| ----------- | ----------------------------------------------- |
| `success` | Whether the command succeeded |
| `command` | Internal command name (for example `AddAction`) |
| `message` | Human-readable summary |
| `requestId` | Echo of `-requestId` when you sent one |
| `data` | Structured payload for list/query commands |
| `errors` | Present when `success` is `false` |
The broker does **not** publish to `$SYS/Coreflux/CommandResponse`. The full command list is on [Broker Commands](/latest/mqtt-broker/commands).
***
## LoT Entity Topics
Each running Action, Model, Route, Rule, and panel publishes its LoT (or panel source) on a topic named after that entity. Subscribe to retrieve what is actually deployed — useful for backup, debugging, and Coreflux HUB.
When you remove an entity, the broker publishes an **empty retained** payload on that topic (MQTT tombstone) so subscribers do not keep a stale definition.
### `$SYS/Coreflux/Actions/{ActionName}`
**Payload:** the complete Action definition (**retained**).
Subscribe with MQTT Explorer or any MQTT client:
| Field | Value |
| --------- | --------------------------------- |
| **Topic** | `$SYS/Coreflux/Actions/Heartbeat` |
**Payload received:**
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
DEFINE ACTION Heartbeat
ON EVERY 10 SECONDS DO
PUBLISH TOPIC "system/heartbeat" WITH TIMESTAMP "UTC"
```
Related Action topics:
| Topic | Content |
| ------------------------------------------ | ----------------------------------------------------------------------------------- |
| `$SYS/Coreflux/Actions/{name}/Description` | Optional description (**retained**) |
| `$SYS/Coreflux/Actions/{name}/Error` | Last Action error (**retained** when set) |
| `$SYS/Coreflux/Actions/{name}/Trace` | Execution traces when tracing is enabled (not retained; expire after a few minutes) |
| `$SYS/Coreflux/Actions/{name}/Run` | **Publish** here to run a callable Action |
| `$SYS/Coreflux/Actions/{name}/Invoke` | Same as `/Run` |
### `$SYS/Coreflux/Models/{ModelName}`
Model LoT (**retained**).
### `$SYS/Coreflux/Routes/{RouteName}`
Route LoT (**retained**). Extra subtopics appear depending on the route type:
| Topic | Content |
| ---------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------- |
| `$SYS/Coreflux/Routes/{name}/status` | Health and counters. See [Route Status Monitoring](/latest/lot-language/routes/overview#route-status-monitoring) |
| `$SYS/Coreflux/Routes/{name}/Certificates` | Certificate-store summary for cert-managed routes (**retained**) |
| `$SYS/Coreflux/Routes/{name}/Tools` | MCP tool summary for MCP routes |
| `$SYS/Coreflux/Routes/{name}/Tools/{tool}` | One MCP tool (schema and capability stats) |
| `$SYS/Coreflux/Routes/{name}/Events/{event}/Trigger` | **Publish** to fire that route event |
### `$SYS/Coreflux/Rules/{RuleName}`
Rule LoT source.
### `$SYS/Coreflux/Visus/{PanelName}`
Panel source text (**retained**).
#### Subscribe to all entities
| Entity type | Topic pattern |
| ----------- | ------------------------- |
| All Actions | `$SYS/Coreflux/Actions/#` |
| All Models | `$SYS/Coreflux/Models/#` |
| All Routes | `$SYS/Coreflux/Routes/#` |
| All Rules | `$SYS/Coreflux/Rules/#` |
| All panels | `$SYS/Coreflux/Visus/#` |
***
## Broker Information
### `$SYS/Coreflux/Version`
Broker version and build (**retained**).
```json wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
{
"version": {
"major": 2,
"minor": 14,
"patch": 0,
"branch": "release",
"os": "Linux-x64",
"stage": "stable",
"timestamp": "2026-08-26T10:00:00Z",
"notes": ""
}
}
```
The `os` field is the same prefix used under `$SYS/{OS}/…`.
### `$SYS/Coreflux/License`
License status, tier, feature flags, limits, and current artifact usage (**retained**). Unlicensed or trial brokers still publish this topic so you can see `Tier`, `IsTrial`, and `CurrentUsage`.
```json wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
{
"LicensedTo": "N/A",
"ExpiresUtc": null,
"IsActive": false,
"IsTrial": true,
"TrialDaysRemaining": 14.0,
"Tier": "Community",
"InstanceKey": "",
"Features": {
"EnableAdvancedRoutes": false,
"EnableRBAC": false
},
"CurrentUsage": {
"ArtifactCount": 3,
"Actions": { "Regular": 2, "RouteEvents": 0, "PythonScripts": 0 },
"Models": 0,
"Rules": 12,
"Routes": 1,
"Panels": 0
}
}
```
### `$SYS/Coreflux/Resources`
| Topic | Description |
| ----------------------------------------- | ------------------------------ |
| `$SYS/Coreflux/Resources/MemoryInfoTotal` | Total available memory (bytes) |
| `$SYS/Coreflux/Resources/MemoryInfoUsage` | Current memory usage (bytes) |
***
## Configuration Topics
### `$SYS/Coreflux/Config`
The live broker configuration as JSON. The broker adds a `Python` object so you can see whether scripts run locally or on the runner sidecar without also subscribing to `$SYS/{OS}/Python/Info`.
```json wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
{
"Name": "Coreflux DataHub",
"Port": 1883,
"TlsPort": 8883,
"WebsocketPort": 5000,
"WebsocketPortTls": 443,
"BindIPForMQTT": "0.0.0.0",
"AnonymousLogin": false,
"Python": {
"Available": true,
"UsingEmbeddedRuntime": false,
"UsingRunner": false,
"Version": "3.11.2",
"Backend": "python.net",
"IsolationLevel": "none"
}
}
```
To change configuration, publish JSON to `$SYS/Coreflux/Config/New`. Most fields still need a restart. See [Broker Configuration](/latest/mqtt-broker/configuration).
### `$SYS/Coreflux/Config/New` and `…/Result`
Publish a **partial** configuration object to `$SYS/Coreflux/Config/New`. The broker writes `$SYS/Coreflux/Config/New/Result` with success or validation failure text.
### `$SYS/Coreflux/Config/Users`
Configured users with passwords masked. `Groups` is the list used by `USER IN GROUP` in Rules.
```json wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
[
{
"UserName": "root",
"Password": "********",
"AllowedBaseTopic": "",
"AllowedSystemConfiguration": true,
"AllowedUserManagement": false,
"AllowedLogManagement": false,
"Groups": []
}
]
```
| Field | Description |
| ---------------------------- | --------------------------------------------------------- |
| `AllowedBaseTopic` | Optional topic prefix restriction |
| `AllowedSystemConfiguration` | Can change broker configuration and most management gates |
| `AllowedUserManagement` | Can manage users |
| `AllowedLogManagement` | Can manage log traces |
| `Groups` | Group names for [RBAC](/latest/lot-language/rules/rbac) |
***
## Connection Monitoring
### `$SYS/Coreflux/Comms/Sessions`
Active MQTT sessions.
```json wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
[
{
"CreatedTimestamp": "2026-08-26T09:01:36.5008146Z",
"DisconnectedTimestamp": null,
"ExpiryInterval": 0,
"Id": "mqtt-explorer-274f0040",
"Items": {},
"PendingApplicationMessagesCount": 0
}
]
```
### `$SYS/Coreflux/Comms/Clients`
Connected clients, including bytes and packet counts.
```json wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
[
{
"BytesReceived": 173,
"BytesSent": 3943557,
"ConnectedTimestamp": "2026-08-26T09:01:36.5008189Z",
"Endpoint": "172.17.0.1:52256",
"Id": "mqtt-explorer-274f0040",
"ProtocolVersion": 4,
"ReceivedApplicationMessagesCount": 16070,
"SentApplicationMessagesCount": 0,
"Session": {
"Id": "mqtt-explorer-274f0040",
"PendingApplicationMessagesCount": 0
}
}
]
```
| Field | Description |
| ----------------------------- | -------------------------------------- |
| `BytesReceived` / `BytesSent` | Totals for this connection |
| `Endpoint` | Client IP and port |
| `Id` | MQTT client ID |
| `ProtocolVersion` | `3` = MQTT 3.1, `4` = 3.1.1, `5` = 5.0 |
### `$SYS/Coreflux/Comms/Logins`
One line per login attempt (password always masked):
```
ClientId = lotvstudio-a6kjtbkwp, Endpoint = 172.17.0.1:53454, Username = root, Password = ******, CleanSession = True, ReasonCode = Success
```
Lifecycle Actions (`ON CONNECT` / `ON DISCONNECT`) are a better place for automation; this topic is for observability. See [Action triggers](/latest/lot-language/actions/events).
### `$SYS/Coreflux/Comms/PacketTrace/{clientId}/In` and `/Out`
Optional raw packet traces started with `-mqttPacketTrace`. Use only while debugging a specific client.
***
## Project Topics
Retained so Coreflux HUB and other clients get the last project state on subscribe, without sending `-listProjects` first. See [Project Commands](/latest/mqtt-broker/commands#project-commands).
| Topic | Content |
| ---------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------- |
| `$SYS/Coreflux/Projects/list` | Stored projects and which one is active (**retained**) |
| `$SYS/Coreflux/Projects/active` | Currently loaded project (**retained**; empty in standalone mode) |
| `$SYS/Coreflux/Projects/{name}/status` | Git status for that project (**retained**) |
| `$SYS/Coreflux/Projects/{name}/manifest` | SHA-256 of stored file bytes, plus entities declared in `.lotnb` cells (**retained**). Same JSON as `-projectManifest`. Cleared on `-removeProject` |
| `$SYS/Coreflux/Entities/ownership` | Map of `type:name` → `{ project, file, cell }` for project-owned entities (**retained**). Absence of a key means the entity is ad-hoc |
Export payloads on `$SYS/Coreflux/Projects/{name}/download` are **not** retained (one-shot zip transfer).
Under default rules, `$SYS/Coreflux/Projects/#` is allowed for the same users who may use `-listProjects` (`AllowedUserManagement`, `AllowedSystemConfiguration`, `root`, `admin`) — not every MQTT user.
## Platform Topics
These topics are prefixed with the operating system identifier (for example `Linux-x64`, `Windows-x64`). Read `os` from `$SYS/Coreflux/Version`.
### `$SYS/{OS}/Python/Info`
Python availability and version on that host.
```json wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
{
"Available": true,
"Version": "3.11.2 (main, Apr 28 2025, 14:11:48) [GCC 12.2.0]",
"Scripts": []
}
```
Individual scripts also appear at `$SYS/{OS}/Python/Scripts/{scriptName}`.
### TCP and UDP listeners
| Topic | Content |
| --------------------------------------- | ----------------------------------------------- |
| `$SYS/{OS}/Comms/TCP/Listeners` | Addresses and ports the process is listening on |
| `$SYS/{OS}/Comms/TCP/ActiveConnections` | Established TCP connections |
| `$SYS/{OS}/Comms/UDP/Listeners` | UDP listeners |
| `$SYS/{OS}/Comms/UDP/ActiveConnections` | Active UDP associations |
Example TCP listeners payload:
```json wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
[
{"Address": "0.0.0.0", "Port": "1883"},
{"Address": "0.0.0.0", "Port": "80"}
]
```
***
## Access Control
`$SYS` uses the `SubscribeSys` and `PublishSys` rule scopes. **Locked built-in rules** already decide who may subscribe to `$SYS/#`, publish commands, and update config. A custom `SubscribeSys` rule on `$SYS/#` does not override those gates.
| Who | Typical access |
| --------------------------------------------- | ------------------------------------------------------------------------------------- |
| `root`, `admin`, `AllowedSystemConfiguration` | Subscribe to `$SYS/#` |
| `CommandCall` gates | Publish to `$SYS/Coreflux/Command` and subscribe to `Command/Output` |
| Same roles as `-listProjects` | `$SYS/Coreflux/Projects/#` |
| Any connected client | Publish to `$SYS/Coreflux/Actions/+/Run` and `/Invoke` (panels call Actions this way) |
See [Rules syntax](/latest/lot-language/rules/syntax#system-topics-sys) and [RBAC](/latest/lot-language/rules/rbac) for flags and locked rules.
Publish access to `$SYS/Coreflux/Command` lets a client run administrative commands. Keep that topic on management users only. `/Run` and `/Invoke` are allowed more widely so dashboards can call Actions — restrict those Actions themselves if they are sensitive.
***
## Next Steps
Change ports, TLS, and other broker settings
Full command list published to `$SYS/Coreflux/Command`
# Best Practices & Patterns
Source: https://docs.coreflux.org/latest/quick-start/best-practices
Overview and table of contents for best practices—topic design, reusable logic, alarms, integrations, production architecture, and notebooks.
## After Hello World, What's Next?
Once you've followed [Getting Started](/latest/quick-start/getting-started) and have Coreflux, VS Code, and your first Action running, the question becomes *how to grow*: naming topics, handling JSON, scaling Actions, wiring integrations, and shipping systems that stay maintainable. Experienced teams reuse the same patterns across factories, buildings, energy sites, and fleets—only the topics and payloads change.
**Like learning chords before writing songs.** Your first Action proves the broker runs your logic; patterns teach you how dozens of Actions, Models, and Routes fit together without turning into spaghetti.
## How Are Best Practices & Patterns Organized
Chapters below live under **Best Practices and Usage Patterns** in the sidebar. Pick one based on what you're building — each page focus on a set of tips for a given topic. Start with **Naming conventions** and finish with **Anti-patterns** when reviewing code.
PascalCase entities, snake\_case variables, and topic naming basics.
Topic trees (UNS), scalar vs JSON payloads, Models, and inheritance — how data is shaped and addressed.
GET JSON in Actions, nested payloads, and publishing structured output through Models.
Callable actions, wildcards, counting over time, and Python — techniques for the Action you are writing now; logic that scales and stays maintainable.
Threshold crossings and device or route offline detection; optional email routes alongside alarms.
REST server and client, live KPIs from a database, and ENV / Secrets — Routes that bridge to non-MQTT systems.
End-to-end device-to-database ingestion and edge–cloud bridges — synthesis patterns that build on everything before.
Keeping notebooks clean and readable — single-file vs multi-file layouts and shared discipline at every stage.
Common LoT mistakes and what to do instead.
***
## Next Steps
Building blocks and how Actions, Models, Routes, and Rules fit together.
Deep dive on `FROM` and base models.
New to the language? **[Introduction to LoT](/latest/lot-language/introduction)** links Actions, Models, Routes, and Rules from one page.
# Build Your First Dashboard
Source: https://docs.coreflux.org/latest/quick-start/building-dashboards
From a topic in the Data Viewer to a live, shareable panel — in about ten minutes.
By the end of this guide you'll have a live monitoring panel with a gauge, a live trend chart, a control, and an action wired to it — open and shareable on a second screen. Everything runs on data you generate yourself, so you can follow along even on a fresh broker.
**Like assembling a control room one instrument at a time.** You'll bolt on one widget per step, watch it come alive immediately, and ship the finished console at the end.
So your widgets have something to show, deploy a small **LoT Action** that publishes a changing temperature and machine status every few seconds. Open the **LoT Editor** in the HUB, paste this, and deploy:
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
DEFINE ACTION SimulateSensorData
ON EVERY 3 SECONDS DO
IF GET TOPIC "sim/counter" == EMPTY THEN
KEEP TOPIC "sim/counter" WITH 0
SET "n" WITH (GET TOPIC "sim/counter" AS INT)
SET "n" WITH ({n} + 10)
IF {n} > 140 THEN
SET "n" WITH 0
KEEP TOPIC "sim/counter" WITH {n}
PUBLISH TOPIC "sensor/temperature" WITH {n}
IF {n} > 100 THEN
PUBLISH TOPIC "machine/status" WITH "stopped"
ELSE
PUBLISH TOPIC "machine/status" WITH "running"
```
This ramps `sensor/temperature` from 0 to 140 and flips `machine/status` between `running` and `stopped` — enough to move a gauge past its threshold and draw a live trend line.
**Prefer to drive it by hand?** Skip the Action and instead publish manually in the [Data Viewer](/hub/mqtt/data-viewer): send `sensor/temperature` a number like `82`, and `machine/status` the text `running` or `stopped`.
Open the **Data Viewer** and confirm `sensor/temperature` is updating with a numeric payload. Note the payload shape: a plain number binds directly; a JSON object would need `JSON_PATH` later.
Open **Dashboards**, click **Create new panel**, and start from this skeleton. The Dashboard Manager opens the panel definition next to its live result:
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
DEFINE PANEL Line3ShiftLead
WITH VISIBILITY PUBLIC
WITH STATE PUBLISHED
WITH TITLE "Line 3 — Shift Lead"
WITH LAYOUT "grid" COLUMNS 2
```
Add a **gauge** bound to `sensor/temperature`, with a range, a unit, and a warning threshold. The panel so far — the new gauge component is highlighted:
```lot wrap focus={7-11} theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
DEFINE PANEL Line3ShiftLead
WITH VISIBILITY PUBLIC
WITH STATE PUBLISHED
WITH TITLE "Line 3 — Shift Lead"
WITH LAYOUT "grid" COLUMNS 2
ADD COMPONENT "TempGauge" WITH TYPE "gauge"
SET RANGE FROM 0 TO 150
SET UNIT "°C"
BIND VALUE TO TOPIC "sensor/temperature"
ADD THRESHOLD AT 100 WITH COLOR "warning"
```
Save — the needle starts moving immediately. That's the live binding at work.
The gauge shows *now*; a chart shows *the last few minutes*. Open the **AI Assistant** and ask:
> "Add a live chart to my Line 3 panel that trends `sensor/temperature` over time."
Review what it added in the Dashboard Manager — the panel now reads:
```lot wrap focus={13-16} theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
DEFINE PANEL Line3ShiftLead
WITH VISIBILITY PUBLIC
WITH STATE PUBLISHED
WITH TITLE "Line 3 — Shift Lead"
WITH LAYOUT "grid" COLUMNS 2
ADD COMPONENT "TempGauge" WITH TYPE "gauge"
SET RANGE FROM 0 TO 150
SET UNIT "°C"
BIND VALUE TO TOPIC "sensor/temperature"
ADD THRESHOLD AT 100 WITH COLOR "warning"
ADD COMPONENT "TempTrend" WITH TYPE "realtime-chart"
SET LABEL "Temperature Trend"
SET UNIT "°C"
BIND VALUE TO TOPIC "sensor/temperature"
```
The `realtime-chart` accumulates every published value into a trend — with the simulator from step 1 running, the line starts drawing itself within seconds.
Add a **toggle** that tracks the live pump state — it flips the moment the bound topic changes. Publish `true` or `false` to `zone1/pump/state` in the Data Viewer to see it move:
```lot wrap focus={18-20} theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
DEFINE PANEL Line3ShiftLead
WITH VISIBILITY PUBLIC
WITH STATE PUBLISHED
WITH TITLE "Line 3 — Shift Lead"
WITH LAYOUT "grid" COLUMNS 2
ADD COMPONENT "TempGauge" WITH TYPE "gauge"
SET RANGE FROM 0 TO 150
SET UNIT "°C"
BIND VALUE TO TOPIC "sensor/temperature"
ADD THRESHOLD AT 100 WITH COLOR "warning"
ADD COMPONENT "TempTrend" WITH TYPE "realtime-chart"
SET LABEL "Temperature Trend"
SET UNIT "°C"
BIND VALUE TO TOPIC "sensor/temperature"
ADD COMPONENT "PumpToggle" WITH TYPE "toggle"
SET LABEL "Pump"
BIND STATE TO TOPIC "zone1/pump/state"
```
Events and actions are what make a panel *do* things, not just show them. Add a **button** that publishes a start command when clicked:
```lot wrap focus={22-25} theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
DEFINE PANEL Line3ShiftLead
WITH VISIBILITY PUBLIC
WITH STATE PUBLISHED
WITH TITLE "Line 3 — Shift Lead"
WITH LAYOUT "grid" COLUMNS 2
ADD COMPONENT "TempGauge" WITH TYPE "gauge"
SET RANGE FROM 0 TO 150
SET UNIT "°C"
BIND VALUE TO TOPIC "sensor/temperature"
ADD THRESHOLD AT 100 WITH COLOR "warning"
ADD COMPONENT "TempTrend" WITH TYPE "realtime-chart"
SET LABEL "Temperature Trend"
SET UNIT "°C"
BIND VALUE TO TOPIC "sensor/temperature"
ADD COMPONENT "PumpToggle" WITH TYPE "toggle"
SET LABEL "Pump"
BIND STATE TO TOPIC "zone1/pump/state"
ADD COMPONENT "StartButton" WITH TYPE "button"
SET LABEL "Start Line"
ON CLICK DO
PUBLISH TOPIC "line3/commands/start" WITH 1
```
Watch it work: subscribe to `line3/commands/start` in the [Data Viewer](/hub/mqtt/data-viewer), click the button, and the `1` arrives instantly.
The full set of events (clicks, changes, thresholds) and actions (publish, navigate, invoke, trigger) is in [Actions & Events](/latest/lot-language/panels/actions-events).
Save, and the console is complete — verify the labels and ranges; operators will rely on this screen.
In the Dashboard Manager, hover the panel row and click **Broadcast in new tab**. Copy the URL into any browser — a wall TV, a tablet, a kiosk. Same live data, no HUB access required.
## The complete panel
Everything from the steps above in one copy-paste definition:
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
DEFINE PANEL Line3ShiftLead
WITH VISIBILITY PUBLIC
WITH STATE PUBLISHED
WITH TITLE "Line 3 — Shift Lead"
WITH LAYOUT "grid" COLUMNS 2
ADD COMPONENT "TempGauge" WITH TYPE "gauge"
SET RANGE FROM 0 TO 150
SET UNIT "°C"
BIND VALUE TO TOPIC "sensor/temperature"
ADD THRESHOLD AT 100 WITH COLOR "warning"
ADD COMPONENT "TempTrend" WITH TYPE "realtime-chart"
SET LABEL "Temperature Trend"
SET UNIT "°C"
BIND VALUE TO TOPIC "sensor/temperature"
ADD COMPONENT "PumpToggle" WITH TYPE "toggle"
SET LABEL "Pump"
BIND STATE TO TOPIC "zone1/pump/state"
ADD COMPONENT "StartButton" WITH TYPE "button"
SET LABEL "Start Line"
ON CLICK DO
PUBLISH TOPIC "line3/commands/start" WITH 1
```
## Where to go next
Every widget, property, and bind target.
Ready-to-adapt recipes: KPI walls, control panels, and model-backed dashboards.
# Creating Agents in Coreflux
Source: https://docs.coreflux.org/latest/quick-start/creating-agents
Add AI to Coreflux in minutes — talk to live data and put intelligence inside LoT automations, bringing AI to your solutions.
## Turn Your Broker Into a Teammate — and Your Automations Into Thinkers
Your devices, integrations, and databases already run on Coreflux — whether you monitor a factory, a solar site, a building portfolio, or a mobile fleet. **AI agents** live in LoT on that same broker: no separate AI platform, no copy-paste into another chat app.
**Talk with live data and automate with judgment.** Teams ask in plain language and get answers from what the broker sees *right now*. Your **Actions** already react to MQTT and timers; with agents they can also **reason** on a schedule or when an event fires — a status digest, a plain-language take on an alarm, a handover brief — all through `CALL AGENT` in LoT.
| What you gain | Example (any vertical) |
| ------------------------------------------- | ------------------------------------------------------------------------------------------------- |
| **Ask the system, don't only chart it** | *"Which zones are offline?"* · *"What's output at site B?"* · *"Any inverters under target?"* |
| **Smarter automations, not more scripts** | A timer or topic wakes an agent; LoT publishes the result — no babysitting a workflow |
| **Reports while you sleep** | Hourly or daily digest on MQTT for supervisors, facility managers, or NOC staff |
| **Specialists, not one overloaded chatbot** | A guide for people, a reporter on a schedule, an analyst when alarms fire — each with its own job |
You define agents once in LoT (alongside your Actions and Routes). Below are **ready-to-use use cases** you can paste and deploy — pick one to prove value in minutes, or combine several on the same broker. Full options — multi-agent teams, MCP tools, safety rails — are in [AI Routes](/latest/lot-language/routes/ai-routes).
**Like a supervisor on the floor *and* a analyst on the line.** People get answers when they ask; your automations get judgment when something changes — all on the broker you already run.
***
## Ready-to-Use Use Cases (Copy & Deploy)
Each tab is a full recipe: agent Route, LoT Action where needed, and MQTT topics to try. Adapt names to your vertical — examples in the table.
| Use case | Who benefits | How it runs |
| ----------------------- | ----------------------- | -------------------------------------------------- |
| **Talk with your data** | Operators, support, NOC | Ask on `agents/site/ask`, read `agents/site/reply` |
| **Automatic report** | Supervisors, managers | Scheduled summary on `reports/operations/hourly` |
| **Alert triage** | On-call, maintenance | Each alert gets a short AI explanation |
| **Shift handover** | Shift leads, crews | Plain-language brief before the next rotation |
Some examples of use-cases for your industry:
| Vertical | Typical data topics | Automatic report might cover |
| --------------------- | ----------------------------------- | ----------------------------------------- |
| **Manufacturing** | `production/`, `lines/`, `quality/` | Throughput, downtime, batch issues |
| **Solar & energy** | `solar/`, `inverters/`, `grid/` | Generation, underperformance, curtailment |
| **Smart buildings** | `buildings/`, `hvac/`, `occupancy/` | Comfort, energy, fault patterns |
| **Fleet & logistics** | `fleet/`, `vehicles/`, `routes/` | Utilisation, delays, asset health |
### Before you start
* A **running broker** — [Installation](/latest/quick-start/installation) and [Getting Started](/latest/quick-start/getting-started) if you are new
* A **model provider** — OpenAI, Anthropic, Mistral, or **local Ollama** on your network. Every recipe in the tabs below uses **OpenAI**; swap `PROVIDER`, `MODEL`, and the secret name if you prefer another cloud provider or edge inference (see [Picking a Provider](/latest/lot-language/routes/ai-routes#picking-a-provider)).
For these examples, store an [OpenAI API key](https://platform.openai.com/api-keys) once on the broker:
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
KEEP SECRET "OPENAI_KEY" WITH "sk-..."
```
The **[HUB AI Assistant](/hub/ai-assistant)** (Coreflux icon in the dock) is a separate built-in helper for drafting LoT and dashboards. This guide is about **your** agents — the ones you own in LoT.
**Outcome:** Anyone with MQTT access can ask questions; answers use **live broker data** (topics, Routes, recent payloads).
Paste into the LoT Editor (Routes, then Actions) or your notebook:
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
DEFINE ROUTE SiteGuide WITH TYPE AGENT
ADD AGENT_CONFIG
WITH PROVIDER "openai"
WITH MODEL "gpt-5.5"
WITH API_KEY GET SECRET "OPENAI_KEY"
WITH BROKER_TOOLS "true"
WITH INTERACTION_MODE "interactive"
WITH INTERACTION_TOPIC "agents/site/interact"
WITH SYSTEM_PROMPT "You are a site operations guide. Use broker tools to answer from live MQTT data for this deployment. Be concise."
DEFINE ACTION AskSiteGuide
ON TOPIC "agents/site/ask" DO
CALL AGENT "SiteGuide.execute"
WITH (task = PAYLOAD)
RETURN AS {reply}
PUBLISH TOPIC "agents/site/reply" WITH {reply}
```
In HUB **Routes**, check **SiteGuide** (AGENT). In **Actions**, check **AskSiteGuide**. Both should show healthy.
In MQTT Explorer or HUB [Data Viewer](/hub/mqtt/data-viewer), publish to `agents/site/ask` and subscribe to `agents/site/reply`. Try a question that fits your data, for example:
```text theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
Which sensors have been updated in the last few minutes?
```
```text theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
What is the latest data on telemetry/zone1/status?
```
(Rename `telemetry/` to `production/`, `solar/`, `buildings/`, or your own tree.)
The reply topic carries the agent's answer — grounded in what your broker actually sees.
You just **talked with your data**. No extra AI server — the agent runs where your MQTT already lives.
**Outcome:** **LoT automation with AI** — on a schedule, an Action calls the agent and publishes an **operations summary** to MQTT. No chat window.
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
DEFINE ROUTE OperationsReporter WITH TYPE AGENT
ADD AGENT_CONFIG
WITH PROVIDER "openai"
WITH MODEL "gpt-5.4-mini"
WITH API_KEY GET SECRET "OPENAI_KEY"
WITH BROKER_TOOLS "true"
WITH INTERACTION_MODE "autonomous"
WITH SYSTEM_PROMPT "You write operations reports from live MQTT. Use broker tools on telemetry/ and related topics. Bullet points: current status, anomalies, items needing follow-up. Plain language for managers. Adapt wording to what the data represents (production, energy, facilities, fleet, etc.)."
DEFINE ACTION HourlyOperationsReport
ON EVERY 1 HOUR DO
CALL AGENT "OperationsReporter.execute"
WITH (task = "Hourly operations report: summarise key signals from telemetry/ topics, flag anything off-norm or stale, note follow-ups before the next review period.", use_history = "false")
RETURN AS {report}
PUBLISH TOPIC "reports/operations/hourly" WITH {report}
```
Add **OperationsReporter** and **HourlyOperationsReport** in the LoT Editor. No public chat line — only the schedule triggers this agent.
Subscribe to `reports/operations/hourly` in MQTT Explorer, a dashboard, or the HUB Data Viewer.
Publish sample payloads under your data tree (`telemetry/`, `solar/site1/power`, `buildings/tower/hvac`, etc.), then wait for the next run — or temporarily use `ON EVERY 30 SECONDS` and change it back when done.
**Scheduled insight on autopilot** — the report speaks your operation's language, not broker diagnostics.
**Outcome:** When something hits your alerts topic, the broker adds a **plain-language triage note** — what it might mean and what to check first.
Works for any alert source: equipment faults, threshold breaches, connectivity, security events.
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
DEFINE ROUTE AlertAnalyst WITH TYPE AGENT
ADD AGENT_CONFIG
WITH PROVIDER "openai"
WITH MODEL "gpt-5.4-mini"
WITH API_KEY GET SECRET "OPENAI_KEY"
WITH BROKER_TOOLS "true"
WITH INTERACTION_MODE "autonomous"
WITH SYSTEM_PROMPT "You triage operational alerts from MQTT. In 3 short sentences: what happened, likely cause, recommended first check. Be direct."
DEFINE ACTION TriageAlert
ON TOPIC "alerts/#" DO
CALL AGENT "AlertAnalyst.execute"
WITH (task = "Alert on topic " + TOPIC + ": " + PAYLOAD, use_history = "false")
RETURN AS {triage}
PUBLISH TOPIC "agents/site/alerts/analysis" WITH {triage}
```
Ensure your existing logic still publishes under `alerts/` — for example `alerts/inverter/fault`, `alerts/building/leak`, or `alerts/line2/quality`.
Add **AlertAnalyst** and **TriageAlert**. Subscribe to `agents/site/alerts/analysis`.
Publish a test alert to `alerts/test/manual` with any message. Read the analysis on the triage topic.
On-call gets **context with the alarm**, not only a raw payload.
**Outcome:** Before each rotation, a **handover brief** lands on MQTT — what changed, what looks odd, what the incoming team should watch. Common in manufacturing, utilities, and 24/7 operations centers.
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
DEFINE ROUTE HandoverBrief WITH TYPE AGENT
ADD AGENT_CONFIG
WITH PROVIDER "openai"
WITH MODEL "gpt-5.5"
WITH API_KEY GET SECRET "OPENAI_KEY"
WITH BROKER_TOOLS "true"
WITH INTERACTION_MODE "autonomous"
WITH SYSTEM_PROMPT "You write handover notes for operations teams. Use broker tools. Bullet points: status, anomalies, open items. Max 8 bullets."
DEFINE ACTION ShiftHandover
ON EVERY 8 HOURS DO
CALL AGENT "HandoverBrief.execute"
WITH (task = "Prepare handover for the next shift: current operational status, active alarms, and anything unusual in the last 8 hours.", use_history = "false")
RETURN AS {brief}
PUBLISH TOPIC "agents/site/shift/handover" WITH {brief}
```
Add **HandoverBrief** and **ShiftHandover**. Subscribe to `agents/site/shift/handover`.
Temporarily use `ON EVERY 2 MINUTES`, read one brief, then restore `ON EVERY 8 HOURS` for production.
**Crew change becomes a story**, not a scramble through dashboards.
**Starter pack:** **SiteGuide** + **HourlyOperationsReport** + **AlertAnalyst** — ask your deployment, get scheduled insight, enrich every alarm. Rename topics to `production/`, `solar/`, or `buildings/` when you copy the prompts.
***
## When You're Ready to Grow
You already have the building blocks: **chat** (ask/reply topics), **schedules** (`ON EVERY`), and **event-driven** (`ON TOPIC`) automations with `CALL AGENT`. Next level is tuning models per job, adding [MCP Routes](/latest/lot-language/routes/mcp-routes) for Slack or databases, and naming agents by role so LoT stays readable.
For multi-agent patterns (operator, specialist, watchdog, reader), see [Multi-Agent Deployments](/latest/lot-language/routes/ai-routes#multi-agent-deployments).
***
## Stay Safe While You Experiment
Add `WITH AGENT_MODE "insight"` on a Route so it can explore but not publish or trigger downstream equipment. Switch to full mode when you trust the prompt.
Always use `GET SECRET "OPENAI_KEY"` — never paste keys into Route definitions that get saved or shared.
Chat for people; timed Actions for machines. Do not put a public chat line on agents that should only run in the background.
***
## What to Explore Next
Providers, MCP tools, multi-agent teams, and deployment settings when you outgrow this tutorial.
Use MCP in your editor to write LoT faster — separate from agents running on the broker.
# Getting Started
Source: https://docs.coreflux.org/latest/quick-start/getting-started
Go from zero to your first working LoT Action in under 15 minutes.
## Why This Guide?
This is the fastest way to validate your Coreflux setup: in under 15 minutes you'll have a live LoT (Language of Things) Action running inside your broker—proof that your installation works and logic executes at the edge. New to Coreflux? Start with the [Overview](../index).
**Like installing an app on your router instead of your phone.** LoT Actions run directly inside the broker -- your logic executes at the edge without an external script or server.
## Prerequisites
Before you begin, ensure you have:
* **A running Coreflux Broker** — See the [Installation Guide](/latest/quick-start/installation) if you haven't set one up yet
* **VS Code** with the LoT Notebooks extension *or* access to the [Coreflux HUB](/hub/overview) in your browser — see the [VS Code Setup Guide](/latest/quick-start/vscode) for the extension
* **Basic MQTT knowledge** — Understanding of topics, publish/subscribe patterns
***
Ready? Here's what you'll build and deploy.
## Your First LoT Action in 15 Minutes
> **Concept Refresher**: You are about to write an action that lives and runs **inside** the broker, not on your laptop.
Let's create a **simulated temperature sensor** that publishes a temperature reading every 5 seconds. You'll see data on a topic as soon as you deploy—no external input required. This keeps the whole guide in a single "temperature monitoring" flow.
### Understanding the Action
LoT Actions have three parts:
1. **Definition** — `DEFINE ACTION `
2. **Trigger** — When to run (`ON EVERY `, `ON TOPIC ""`, or `ON CHANGE ""` for value-change-only)
3. **Logic** — What to do (publish data, conditional logic, etc.)
Here's what we're building:
```lot Temperature Simulator wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
DEFINE ACTION TemperatureSimulator
ON EVERY 5 SECONDS DO
PUBLISH TOPIC "sensors/zone1/temperature" WITH "{\"temperature\": 72}"
```
| Line | What it does |
| ------------------------------------------------ | -------------------------------------------------------------- |
| `DEFINE ACTION TemperatureSimulator` | Creates an action named "TemperatureSimulator" |
| `ON EVERY 5 SECONDS DO` | Sets a time-based trigger (runs every 5 seconds) |
| `PUBLISH TOPIC ... WITH "{\"temperature\": 72}"` | Publishes a JSON payload with temperature in Fahrenheit (72°F) |
Quotes inside strings must be escaped with backslashes (e.g. `"{\"temperature\": 72}"`).
For a complete reference on triggers, operations, and advanced patterns, see the [Actions documentation](/latest/lot-language/actions/overview).
### Deploy the Action
If you have the Coreflux HUB running—via Docker Compose or the standalone desktop app for Windows or Linux—you can deploy actions directly from the HUB. See [Installation](/latest/quick-start/installation) and the [Coreflux HUB Overview](/hub/overview).
In your browser, open `http://localhost:3000` for Docker Compose. Click **New Connection** and set **Host** to `broker` (**Protocol** `mqtt://`, **Port** `1883`). For the standalone desktop app, run the executable and create a connection to your broker.
Select **LoT Editor** in the HUB dock. In the **WORKSPACE** section of the sidebar, click **+ New** to create a project, then add a **`+ Code`** cell to its notebook. See [LoT Editor](/hub/lot-editor) for the full editor reference.
Paste the following code into the cell (no `-addAction` prefix needed—the HUB handles registration):
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
DEFINE ACTION TemperatureSimulator
ON EVERY 5 SECONDS DO
PUBLISH TOPIC "sensors/zone1/temperature" WITH "{\"temperature\": 72}"
```
Click the cell's **run** button (or press `Shift`+`Enter`) to deploy it. The cell's badge changes to **`SYNCED`** and the Action appears under **Broker → Entities → Actions** — it is now running on the broker.
The VS Code extension provides syntax highlighting, auto-completion, and real-time validation for LoT actions. See the [LoT Notebooks Setup Guide](/latest/quick-start/vscode) for full installation and configuration details.
Open the extension panel, search for 'LoT Notebooks', and install.
Click on File > New File..., give it it to a descriptive name (e.g., temperature-monitor).
Enter your broker credentials (host, port, username, password) and click Connect.
If you haven't changed your broker credentials, use these defaults:
| Setting | Value |
| -------- | ----------------------------------- |
| Host | `localhost` or your server IP |
| Port | `1883` (unencrypted) / `8883` (TLS) |
| Username | `root` |
| Password | `coreflux` (change immediately!) |
Create a new code block and add your code.
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
DEFINE ACTION TemperatureSimulator
ON EVERY 5 SECONDS DO
PUBLISH TOPIC "sensors/zone1/temperature" WITH "{\"temperature\": 72}"
```
Click the **Run Button** on that specific cell to publish the action to your broker.
If you're using any external MQTT client, publish to the system command topic:
Connect to your broker with your credentials.
Publish the following to `$SYS/Coreflux/Command`:
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
-addAction DEFINE ACTION TemperatureSimulator
ON EVERY 5 SECONDS DO
PUBLISH TOPIC "sensors/zone1/temperature" WITH "{\"temperature\": 72}"
```
Subscribe to `$SYS/Coreflux/Command/Output`. A successful deploy looks like:
```json wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
{
"success": true,
"command": "AddAction",
"message": "Action 'TemperatureSimulator' added successfully.",
"entityType": "Action",
"entityName": "TemperatureSimulator",
"data": {}
}
```
Note the `-addAction` prefix — this tells Coreflux to register the action definition that follows.
### Verify It's Running
If you deployed via the HUB, verify using the built-in Data Viewer. In the top navigation, select **MQTT**, then **Data Viewer**. In the Topic Tree, navigate to **sensors** → **zone1** → **temperature**. You should see `{"temperature": 72}` arriving every 5 seconds in the payload viewer.
The LoT extension automatically shows debug values for MQTT topics, so you can verify in two ways:
If the action appears in your Actions list under the Coreflux Entities tab, it should be running.
After deploying, the extension automatically subscribes to the action's topics and displays live MQTT messages. You should see temperature readings (e.g. `{"temperature": 72}`) on `sensors/zone1/temperature` arriving every 5 seconds.
Subscribe to `sensors/zone1/temperature`. You should see temperature readings arriving every 5 seconds:
```json wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
{"temperature": 72}
```
**Congratulations!** Your first LoT Action is live.
## Example Real Scenario: Temperature Alarm
Now let's create a **reactive action** that transforms sensor data and checks thresholds. This demonstrates data transformation and conditional logic—common patterns in industrial monitoring.
This action runs **only when a message arrives** on `sensors/+/temperature`. After you deploy it, you will see no output until something publishes to that topic—for example, the Temperature Simulator from the previous section (if both are running), or a manual publish from the HUB Data Viewer or any MQTT client.
Here's what the action does each time a new temperature reading arrives:
1. **Trigger** — The action fires whenever a message is published to any topic matching `sensors/+/temperature` (e.g. `sensors/zone1/temperature`).
2. **Extract** — It reads the `temperature` value (in Fahrenheit) from the incoming JSON payload.
3. **Convert** — It transforms Fahrenheit to Celsius using the standard formula: `(F − 32) × 5 / 9`.
4. **Decide** — If the Celsius value exceeds 35 °C, it publishes an alert to `alerts/high_temp`. Otherwise, it publishes the normalized Celsius reading to `sensors/normalized`.
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
DEFINE ACTION TemperatureAlarm
ON TOPIC "sensors/+/temperature" DO
SET "temp_f" WITH (GET JSON "temperature" IN PAYLOAD AS DOUBLE)
SET "temp_c" WITH (({temp_f} - 32) * 5 / 9)
IF {temp_c} > 35 THEN
PUBLISH TOPIC "alerts/high_temp" WITH ("Alert: " + {temp_c} + "°C exceeds threshold")
ELSE
PUBLISH TOPIC "sensors/normalized" WITH {temp_c}
```
The `{}` denotes variable substitution, and expressions must be wrapped in parentheses. The `+` in the topic pattern is a single-level MQTT wildcard -- it matches any value in that position (e.g. `sensors/zone1/temperature`, `sensors/zone2/temperature`).
### Deploy and Test
Deploy this action using the same method as above (Coreflux HUB, VS Code extension, or MQTT client). Once deployed, use the steps below to verify reactive logic.
### How to Test the Logic
The action subscribes to any topic matching `sensors/+/temperature` (e.g. `sensors/zone1/temperature`). It expects a JSON payload with a `temperature` key (Fahrenheit). It then converts to Celsius and either publishes an alert (if > 35°C) or the normalized value.
| Role | Topic | Description |
| ------------------- | --------------------------- | -------------------------------------------------------------------- |
| **Input** | `sensors/zone1/temperature` | Publish test payloads here (must match `sensors/+/temperature`) |
| **Output (normal)** | `sensors/normalized` | Receives the Celsius value when temperature is at or below 35°C |
| **Output (alert)** | `alerts/high_temp` | Receives the alert message when temperature exceeds 35°C (e.g. 95°F) |
**Sample payloads (copy-paste):**
* **Below threshold** — publish to `sensors/zone1/temperature`:
```json wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
{"temperature": 77}
```
Expect: a message on `sensors/normalized` with the Celsius value (about 25).
* **Above threshold** — publish to `sensors/zone1/temperature`:
```json wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
{"temperature": 95}
```
Expect: a message on `alerts/high_temp` with text like "Alert: 35°C exceeds threshold".
In **LoT** → **LoT Editor** → **Actions**, create a new action or open an existing one, paste the Temperature Alarm code, and save. Confirm it appears in the Actions list.
Select **MQTT** → **Data Viewer**. In the Topic Tree, watch `sensors/normalized` and `alerts/high_temp` (add them to your tracked view if needed).
In the **Publish** panel, set the topic to `sensors/zone1/temperature` and paste one of the sample JSON payloads above. Click publish. If the Temperature Simulator is still running, it already publishes every 5 seconds—you will see results on `sensors/normalized` (72°F is below 35°C).
Select the output topic in the tree and verify the payload in the viewer (normalized value or alert text).
Paste the action code in a LoT cell and run it. Confirm success in the Actions list.
After deploying, the extension automatically subscribes to the action's output topics and displays their values. Look for messages on `sensors/normalized` and `alerts/high_temp` once test data is published.
Use the HUB Data Viewer, the VS Code extension publish tools, or any MQTT client to publish one of the sample JSON payloads above to `sensors/zone1/temperature`. If the Temperature Simulator from the previous section is still running, it already publishes every 5 seconds and you will see results on `sensors/normalized` (72°F is below 35°C). Check the topic tree displayed below the code cell to see incoming values update in real time.
Check that the corresponding output topic received the expected message (normalized value or alert).
Publish the action to `$SYS/Coreflux/Command` with the `-addAction` prefix. On `$SYS/Coreflux/Command/Output`, confirm `"success": true` and that `message` reports Action `TemperatureAlarm` added.
Subscribe to `sensors/normalized` and `alerts/high_temp` so you can see responses.
Publish to topic `sensors/zone1/temperature` with payload `{"temperature": 77}` or `{"temperature": 95}` (use one of the sample payloads above).
You should see a message on `sensors/normalized` (for 77°F) or on `alerts/high_temp` (for 95°F).
This confirms your broker transforms data and applies conditional logic in real time.
### Clean Up (Optional)
To remove the test action:
In the **LoT Editor**, expand **Broker → Entities → Actions** in the sidebar, right-click the action, and choose **Remove from broker…**. Confirm—the action is removed from the broker immediately. See [Removal](/hub/lot-editor#removal-four-different-operations) for the other flows.
On the Coreflux Entities tab, right-click the desired action in your Actions list and select **"Remove"**.
Publish to `$SYS/Coreflux/Command`:
```text wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
-removeAction TemperatureAlarm
```
## Quick Examples
Looking for inspiration? Check out the [**Language of Things Training Repository**](https://github.com/CorefluxCommunity/Language-Of-Things-Training) for common patterns like:
* **Topic-Based Triggers**: Echo messages between topics
* **State Machines**: Toggle switches and conditional logic
* **Event Counters**: Track and aggregate system events
Explore a collection of ready-to-use LoT Action examples and templates.
## Troubleshooting
* Verify the broker is running and you're connected
* Check `$SYS/Coreflux/Command/Output` for error messages
* Ensure proper indentation in your LoT action (use consistent tabs or spaces)
* Confirm the action appears in `$SYS/Coreflux/Actions` or in the HUB LoT Editor Actions list
* Verify the broker hostname/IP and port are correct
* Check that port 1883 (or 8883 for TLS) is open
* Confirm your username and password are correct
* If using TLS, ensure certificates are properly configured
* Verify you're subscribed to the exact topic (including case sensitivity)
* Check if any Rules are blocking access to the topic
* Use wildcards (`+` or `#`) to debug topic hierarchies
## Next Steps
Now that you have a working setup, explore the core LoT components:
React to events and publish data. Create time-based automations or respond to topic changes.
Understand the core concepts and architecture of the Language of Things.
# Installation
Source: https://docs.coreflux.org/latest/quick-start/installation
Get a Coreflux broker and HUB running in minutes with Docker, the HUB desktop app, a free Cloud Trial, or native installs for Windows and Linux.
## Install Coreflux
Most people should use **Docker** or **Docker Compose** for the broker and the [Coreflux HUB](/hub/overview). Just evaluating with nothing to install? Use the **Cloud Trial** tab. The **HUB desktop** tab is the standalone app. Native broker binaries and cloud droplets are in the remaining tabs.
**Coreflux runs as a single install.** No separate databases or message queues—pick a method below and run it.
Every local install method uses the same defaults:
| Setting | Value |
| -------- | ---------- |
| Username | `root` |
| Password | `coreflux` |
For a **Cloud Trial**, use the credentials from your email instead.
***
**\~2 min** if Docker is already installed — broker, HUB, or both.
The broker and [Coreflux HUB](/hub/overview) are **separate images**. Pick the command that matches what you want to run. The HUB desktop app is in the **HUB desktop** tab.
New to Docker? Install it from the [official Docker install page](https://docs.docker.com/get-started/get-docker/), then confirm with `docker --version` before you continue. Images are on [Docker Hub](https://hub.docker.com/r/coreflux/coreflux-mqtt-broker).
Start the broker. Docker fetches the latest image automatically:
```bash wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
docker run --pull=always -d --name coreflux_broker -p 1883:1883 -p 5000:5000 -p 9100:9100 -v project-volume:/etc/project coreflux/coreflux-mqtt-broker:latest
```
MQTT is on `localhost:1883`. There is no HUB in this command — use **HUB only** or **Broker + HUB**, or the standalone HUB app.
Start the HUB and connect it to a broker that is already running:
```bash wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
docker run --pull=always -d --name coreflux_hub -p 3000:3000 -v hub-data:/opt/coreflux-hub/data coreflux/coreflux-hub:latest
```
Open `http://localhost:3000`. Click **New Connection** (or **Enter** on a saved one). If the broker is on the same machine, set **Protocol** `mqtt://`, **Host** `host.docker.internal` (Docker Desktop) or the host's LAN IP, and **Port** `1883` — not `localhost`, because the HUB proxies MQTT from inside its container.
Create a shared network and start both containers in one command:
```bash wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
docker network create coreflux && docker run --pull=always -d --name coreflux_broker --network coreflux --network-alias broker -p 1883:1883 -p 5000:5000 -p 9100:9100 -v project-volume:/etc/project coreflux/coreflux-mqtt-broker:latest && docker run --pull=always -d --name coreflux_hub --network coreflux -p 3000:3000 -v hub-data:/opt/coreflux-hub/data coreflux/coreflux-hub:latest
```
Open the HUB at `http://localhost:3000`, click **New Connection**, and connect with **Protocol** `mqtt://`, **Host** `broker`, **Port** `1883` (or **Protocol** `ws://`, **Host** `broker`, **Port** `5000`, **Path** `mqtt`). Skip `docker network create coreflux &&` if the `coreflux` network already exists.
| Port | Service | Description |
| ------ | ------- | ---------------------------------------- |
| `1883` | Broker | MQTT (unencrypted) |
| `5000` | Broker | WebSocket / HTTP API |
| `9100` | Broker | `/health`, `/ready`, `/info`, `/metrics` |
| `3000` | HUB | [Coreflux HUB](/hub/overview) |
| Command | Broker fields on **New Connection** |
| ------------ | ------------------------------------------------------------------------------------------------------------------ |
| Broker + HUB | Protocol `mqtt://`, Host `broker`, Port `1883` (or `ws://`, Host `broker`, Port `5000`, Path `mqtt`) |
| HUB only | Protocol `mqtt://`, Host `host.docker.internal` if the broker is on the same machine, or the broker's host address |
The HUB proxies MQTT from inside Docker, so **Host** `localhost` does not reach a broker container.
That's it—Coreflux is now running locally. Confirm it works under [Verify Installation](#verify-installation) below.
Breaking the broker `docker run` down piece by piece:
* `--pull=always` — always fetch the latest image before starting, so you never run a stale version.
* `-d` — runs the container **detached** (in the background), so your terminal stays free.
* `--name coreflux_broker` — names the container so you can reference it later (for example `docker logs coreflux_broker`).
* `-p 1883:1883 -p 5000:5000 -p 9100:9100` — each `-p host:container` maps a port on your machine to the same port inside the container. See the port table above for what each one is for.
* `-v project-volume:/etc/project` — mounts a named volume so your project (LoT, routes, configuration) persists across restarts and image updates.
* `coreflux/coreflux-mqtt-broker:latest` — the broker image, pulled from Docker Hub. The HUB image is `coreflux/coreflux-hub:latest`.
If you configure TLS certificates, expose these additional broker ports (`8883` and `5443`):
```bash wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
docker run --pull=always -d --name coreflux_broker -p 1883:1883 -p 5000:5000 -p 9100:9100 -p 8883:8883 -p 5443:5443 -v project-volume:/etc/project coreflux/coreflux-mqtt-broker:latest
```
| Port | Protocol | Description |
| ------ | -------- | ----------------------------- |
| `8883` | TCP/TLS | MQTT over TLS |
| `5443` | WSS | WebSocket / HTTP API over TLS |
See [Broker Configuration](/latest/mqtt-broker/configuration) for TLS setup.
**\~3 min** if Docker is already installed — broker and HUB on one network.
Docker Compose starts the broker and the [Coreflux HUB](/hub/overview) as two services on a shared network. Use the `full` profile to run both, or start either service alone. The HUB desktop app is in the **HUB desktop** tab.
Create a directory, then add `docker-compose.yml`:
```yaml wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
# Coreflux — Broker + Hub deployment
#
# Profiles:
# full -> broker + hub, on the same network
# broker -> broker only
# hub -> hub only
#
# Usage:
# docker compose --profile full up # broker + hub
# docker compose --profile broker up # broker only
# docker compose --profile hub up # hub only
services:
broker:
image: coreflux/coreflux-mqtt-broker:latest
profiles: [full, broker]
container_name: coreflux-broker
restart: unless-stopped
ports:
- "1883:1883" # MQTT (plain TCP)
- "8883:8883" # MQTT over TLS
- "5000:5000" # WebSocket transport / HTTP API
- "5443:5443" # WebSocket / HTTP API over TLS
- "9100:9100" # Operational endpoints: /health /ready /info /metrics
volumes:
# Persists everything under /etc/project/Coreflux (encrypted config,
# routes, actions, models, rules, logs, stats, projects).
- broker-data:/etc/project
# The published image has no shell, so a shell-based
# healthcheck cannot run inside the container. Check health externally
# via HTTP GET http://localhost:9100/health.
healthcheck:
disable: true
hub:
image: coreflux/coreflux-hub:latest
profiles: [full, hub]
container_name: coreflux-hub
restart: unless-stopped
ports:
- "3000:3000" # Web UI + API + MQTT WebSocket proxy (plain HTTP)
volumes:
# Persists writable Hub state (e.g. theme-media uploads).
- hub-data:/opt/coreflux-hub/data
# environment:
# - GROQ_API_KEY=... # optional, only for the Hub AI assistant feature
#
# NOTE: the Hub has no broker-connection env var. You choose the broker
# from the Hub UI. Because the Hub proxies the MQTT connection
# server-side, enter the broker's service name (not "localhost"):
# mqtt://broker:1883 (plain MQTT)
# ws://broker:5000/mqtt (MQTT over WebSocket)
volumes:
broker-data:
hub-data:
# Both services attach to this network and resolve each other by service
# name ("broker", "hub").
networks:
default:
name: coreflux
```
Then start the stack:
From the directory that contains `docker-compose.yml`:
```bash wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
docker compose --profile full up -d
```
Open the HUB at `http://localhost:3000`. Click **New Connection** and set **Protocol** `mqtt://`, **Host** `broker`, **Port** `1883` — the HUB proxies MQTT from inside Docker, so **Host** `localhost` does not reach the broker container.
Start just the broker:
```bash wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
docker compose --profile broker up -d
```
MQTT is on `localhost:1883`. There is no HUB in this profile.
Start just the HUB (the broker must already be reachable on the `coreflux` network, or you will connect to a remote broker from the UI):
```bash wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
docker compose --profile hub up -d
```
Open the HUB at `http://localhost:3000`, click **New Connection**, and enter the broker **Host**, **Protocol**, and **Port** on the login screen.
| Command | Starts |
| ------------------------------------ | ------------ |
| `docker compose --profile full up` | broker + HUB |
| `docker compose --profile broker up` | broker only |
| `docker compose --profile hub up` | HUB only |
| Port | Service | Description |
| ------ | ------- | ---------------------------------------- |
| `1883` | Broker | MQTT (unencrypted) |
| `8883` | Broker | MQTT over TLS |
| `5000` | Broker | WebSocket / HTTP API |
| `5443` | Broker | WebSocket / HTTP API over TLS |
| `9100` | Broker | `/health`, `/ready`, `/info`, `/metrics` |
| `3000` | HUB | [Coreflux HUB](/hub/overview) |
The HUB talks to the broker **from inside Docker**. On **New Connection**, set **Host** to the Compose service name (`broker`) — Protocol `mqtt://` and Port `1883`, or Protocol `ws://`, Port `5000`, and Path `mqtt`. Do not use **Host** `localhost`.
That's it—Coreflux is running locally. Confirm it works under [Verify Installation](#verify-installation) below.
The broker image has no shell, so Compose cannot run a healthcheck inside the container. Probe it from the host:
```bash wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
curl http://localhost:9100/health
```
`broker-data` persists broker configuration, routes, actions, models, rules, logs, and projects under `/etc/project`. `hub-data` persists HUB state such as theme-media uploads. Stopping or recreating containers does **not** delete these volumes.
```bash wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
docker compose down
```
Add `-v` only if you also want to delete the named volumes (this wipes broker and HUB data).
**\~2 min** — no Docker required for the HUB.
The standalone HUB is a desktop application for **Windows x64**, **Linux x64**, and **Linux ARM64**. It does not require a broker on the same machine.
Download the HUB executable for your platform from [coreflux.org > Start Free](https://www.coreflux.org).
Run the executable — the HUB window opens immediately.
Click **New Connection**. If the broker is on this machine, set **Protocol** `mqtt://`, **Host** `localhost`, **Port** `1883`, then enter the [default credentials](#default-login-credentials) and click **Connect**.
**\~5 min**, nothing to install — hosted broker and HUB in your browser.
Get a hosted Coreflux instance without installing anything. Request a trial on the [Coreflux website](https://www.coreflux.org/), and you will receive an email with a **URL** (used for both the [Coreflux HUB](/hub/overview) and the MQTT broker) and **login credentials**.
Go to [coreflux.org](https://www.coreflux.org/) and submit a trial request using the form on the site.
When your environment is ready, Coreflux sends an email with:
| Item | What you use it for |
| --------------- | ------------------------------------------------------------------------------------------------------------- |
| **URL** | Open the HUB in your browser and connect MQTT clients to the **same host** (broker and HUB share one address) |
| **Credentials** | Username and password for the HUB login screen and for MQTT clients |
The broker may take **up to 60 seconds** to be fully available after you receive the email. If the HUB or MQTT connection fails immediately, wait briefly and try again.
Paste the URL from the email into your browser. Click **New Connection** (or **Enter** on a saved connection), enter your username and password, and click **Connect**. Use the broker host from the email if it is pre-filled differently than your trial URL.
From the HUB you can deploy LoT Actions, browse MQTT topics in the Data Viewer, and manage Routes—same experience as a local Docker install.
Use the **same host** as the URL in your email (not `localhost`). For MQTT Explorer and other desktop clients:
| Setting | Value |
| ------------------- | ------------------------- |
| Protocol | **MQTTS** (MQTT over TLS) |
| Port | **8883** |
| Username / Password | From your trial email |
In **MQTT Explorer** specifically: enable **Encryption**, and **disable Validate certificate** (the trial broker uses a certificate that desktop tools may not trust by default).
That's it—your trial is live and ready to use, with the broker and HUB already running on the URL from your email.
**HUB remote access:** If your browser blocks the HUB page, allow **unsecure access** (or proceed past the certificate warning) for the trial URL. Managed trial environments often use certificates that browsers flag on first visit.
After the HUB loads, continue with [Getting Started](/latest/quick-start/getting-started) using the **Coreflux HUB** tabs—your trial already includes the HUB and broker.
**\~10 min** (plus a DigitalOcean account) — production-ready broker running in the cloud.
Deploy Coreflux in seconds with our **1-Click Droplet** on DigitalOcean Marketplace. This is the fastest way to get a production-ready broker running in the cloud.
Click the button above or visit the [Coreflux Marketplace page](https://marketplace.digitalocean.com/apps/coreflux-mqtt-broker?refcode=b993040d0243\&action=deploy).
Select a plan based on your needs:
| Plan | RAM | vCPUs | Best For |
| --------- | -------- | ----- | -------------------- |
| Basic | 1 GB | 1 | Development, testing |
| Basic | 2 GB | 1 | Small deployments |
| **Basic** | **4 GB** | **2** | **Recommended** ✓ |
We recommend the **4 GB / 2 vCPU** droplet for most use cases. It provides headroom for LoT Actions, multiple routes, and moderate message throughput.
Choose a datacenter close to your devices for lower latency.
Click **Create Droplet** and wait \~60 seconds for deployment.
Once the droplet is ready, use the public IP address to connect:
| Setting | Value |
| -------- | ------------------- |
| Host | `` |
| Port | `1883` |
| Username | `root` |
| Password | `coreflux` |
That's it—your droplet is now running Coreflux and ready to use.
Change the default password immediately after first login. SSH into your droplet and update the broker configuration.
The Coreflux droplet exposes the following ports by default:
| Port | Protocol | Description |
| ------ | --------- | --------------------- |
| `1883` | TCP | MQTT (unencrypted) |
| `8883` | TCP/TLS | MQTT over TLS |
| `5000` | WebSocket | WebSocket connections |
| `443` | WSS | WebSocket over TLS |
Configure your DigitalOcean firewall to restrict access as needed.
To manage your Coreflux installation via SSH:
```bash wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
ssh root@
```
The Coreflux broker runs as a systemd service. Common commands:
```bash wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
# Check status
sudo systemctl status coreflux
# Restart broker
sudo systemctl restart coreflux
# View logs
sudo journalctl -u coreflux -f
```
**\~10 min** — broker running as a Windows service.
Download and install Coreflux, then register it as a Windows service using the native `sc` tool.
1. Download the Windows zip from [coreflux.org > Start Free](https://www.coreflux.org)
2. Extract the zip file to your preferred location (e.g., `C:\Coreflux`)
3. Open Command Prompt **as Administrator** and run:
```cmd wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
sc create CorefluxBroker binPath= "C:\Coreflux\CorefluxMQTTBroker.exe" start= auto DisplayName= "Coreflux MQTT Broker"
sc description CorefluxBroker "Coreflux MQTT Broker Service"
sc start CorefluxBroker
```
That's it—the service is now registered, set to start automatically on boot, and running, so Coreflux is ready to use. For the HUB, use the **HUB desktop** tab or run the HUB container from the **Docker** tab.
Use these native Windows commands to control the service:
```cmd wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
sc stop CorefluxBroker
sc start CorefluxBroker
sc query CorefluxBroker
```
You can also manage it from **Services** (`services.msc`) in Windows.
Stop and remove the service with:
```cmd wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
sc stop CorefluxBroker
sc delete CorefluxBroker
```
**\~10 min** — broker installed from native packages and running as a systemd service.
Install the MQTT broker with your distribution's package manager. Packages are published at [packages.coreflux.org](https://packages.coreflux.org) for Debian-based systems (APT) and RHEL-compatible systems (DNF).
APT is the package manager on Debian, Ubuntu, and Debian-based distributions—including **Raspberry Pi OS**. The package manager picks the right architecture (x64 or ARM64) for your machine.
| OS | Codename |
| ------------ | ---------- |
| Ubuntu 22.04 | `jammy` |
| Ubuntu 24.04 | `noble` |
| Debian 12 | `bookworm` |
| Debian 13 | `trixie` |
On Raspberry Pi OS, use the codename that matches your release (for example **bookworm** on Pi OS based on Debian 12, **noble** on releases based on Ubuntu 24.04).
Run `lsb_release -a` and note the **Codename** line—you will use it in the next step.
```bash wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
lsb_release -a
```
Example output on Ubuntu 24.04:
```text wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
Codename: noble
```
Download Coreflux's archive signing key into `/usr/share/keyrings`:
```bash wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
sudo mkdir -p /usr/share/keyrings
curl -fsSL https://packages.coreflux.org/keys/coreflux-archive-keyring.gpg \
| sudo tee /usr/share/keyrings/coreflux-archive-keyring.gpg >/dev/null
sudo chmod 0644 /usr/share/keyrings/coreflux-archive-keyring.gpg
```
Run **only** the command that matches your codename. Each command writes `/etc/apt/sources.list.d/coreflux.list`.
```bash wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
echo "deb [signed-by=/usr/share/keyrings/coreflux-archive-keyring.gpg] https://packages.coreflux.org/deb/ubuntu jammy main" \
| sudo tee /etc/apt/sources.list.d/coreflux.list
```
```bash wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
echo "deb [signed-by=/usr/share/keyrings/coreflux-archive-keyring.gpg] https://packages.coreflux.org/deb/ubuntu noble main" \
| sudo tee /etc/apt/sources.list.d/coreflux.list
```
```bash wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
echo "deb [signed-by=/usr/share/keyrings/coreflux-archive-keyring.gpg] https://packages.coreflux.org/deb/debian bookworm main" \
| sudo tee /etc/apt/sources.list.d/coreflux.list
```
```bash wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
echo "deb [signed-by=/usr/share/keyrings/coreflux-archive-keyring.gpg] https://packages.coreflux.org/deb/debian trixie main" \
| sudo tee /etc/apt/sources.list.d/coreflux.list
```
Refresh package lists and install the broker package:
```bash wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
sudo apt update
sudo apt install coreflux-broker
```
Optional — list available versions before installing:
```bash wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
apt-cache policy coreflux-broker
```
The package registers and starts `coreflux-broker` as a systemd service:
```bash wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
systemctl status coreflux-broker.service
```
**active (running)** means Coreflux is up. MQTT listens on port `1883` by default.
That's it—the broker is installed under `/opt/coreflux-broker` and managed by systemd. Confirm it works under [Verify Installation](#verify-installation) below. For the HUB, use the **HUB desktop** tab or run the HUB container from the **Docker** tab.
The key UID should show **Coreflux Portugal S.A. Package Signing** with email `packages@coreflux.org`:
```bash wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
gpg --no-default-keyring \
--keyring /usr/share/keyrings/coreflux-archive-keyring.gpg \
--list-keys --with-fingerprint
```
```bash wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
sudo systemctl stop coreflux-broker
sudo systemctl start coreflux-broker
sudo systemctl restart coreflux-broker
sudo journalctl -u coreflux-broker -f
```
Use `dnf` on RHEL-compatible distributions—RHEL, Rocky Linux, AlmaLinux, and CentOS Stream. The repository uses `$releasever` and `$basearch`, so one repo file covers your EL version and CPU architecture (`x86_64` or `aarch64`).
| OS family | `$releasever` |
| ---------------------------------------------- | ------------- |
| EL 8 (RHEL / Rocky / Alma / CentOS Stream 8) | `8` |
| EL 9 (RHEL / Rocky / Alma / CentOS Stream 9) | `9` |
| EL 10 (RHEL / Rocky / Alma / CentOS Stream 10) | `10` |
```bash wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
sudo rpm --import https://packages.coreflux.org/keys/coreflux.asc
```
Create `/etc/yum.repos.d/coreflux.repo`:
```bash wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
printf '%s\n' \
'[coreflux]' \
'name=Coreflux stable (GA)' \
'baseurl=https://packages.coreflux.org/rpm/el/$releasever/$basearch' \
'enabled=1' \
'gpgcheck=1' \
'repo_gpgcheck=1' \
'gpgkey=https://packages.coreflux.org/keys/coreflux.asc' \
| sudo tee /etc/yum.repos.d/coreflux.repo
```
Refresh the package cache and install the broker:
```bash wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
sudo dnf makecache --refresh
sudo dnf install coreflux-broker
```
Optional — list available versions before installing:
```bash wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
dnf list --showduplicates coreflux-broker
```
The package registers and starts `coreflux-broker` as a systemd service:
```bash wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
systemctl status coreflux-broker.service
```
**active (running)** means Coreflux is up. MQTT listens on port `1883` by default.
That's it—the broker is installed under `/opt/coreflux-broker` and managed by systemd. Confirm it works under [Verify Installation](#verify-installation) below. For the HUB, use the **HUB desktop** tab or run the HUB container from the **Docker** tab.
```bash wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
sudo systemctl stop coreflux-broker
sudo systemctl start coreflux-broker
sudo systemctl restart coreflux-broker
sudo journalctl -u coreflux-broker -f
```
***
## Verify Installation
The quickest visual check is the [Coreflux HUB](/hub/overview) in your browser. You can also confirm directly by connecting an MQTT client and seeing Coreflux's system topics—that's how the broker is actually used. Use the [default credentials](#default-login-credentials) from the top of the page (or your trial email).
Open your environment URL in the browser—for a **Cloud Trial**, use the address from your email; for **Docker** or **Docker Compose**, use `http://localhost:3000`. For the **HUB desktop** app, the window opens on its own.
The login screen lists **stored connections**. Click **Enter** on a saved one, or click **New Connection** to add a broker.
Fill in the broker fields, then enter the [default credentials](#default-login-credentials) (or your trial email) and click **Connect**. Optionally click **Save connection** so the HUB remembers this broker next time.
| Install | Protocol | Host | Port | Path |
| -------------------------------------- | --------- | ---------------------- | ------ | ------ |
| Docker / Compose (broker + HUB) | `mqtt://` | `broker` | `1883` | — |
| Docker / Compose (WebSocket) | `ws://` | `broker` | `5000` | `mqtt` |
| HUB desktop, broker on this machine | `mqtt://` | `localhost` | `1883` | — |
| HUB-only container, broker on the host | `mqtt://` | `host.docker.internal` | `1883` | — |
After a successful login, you should see the HUB home screen with the bottom navigation bar.
The status bar shows route count and license info—the broker is reachable from the HUB.
Select **MQTT** → **Data Viewer**. Expand **\$SYS/Coreflux** in the Topic Tree—you should see broker topics such as **Config**, **Version**, and **Resources** updating live.
Download [MQTT Explorer](https://mqtt-explorer.com/) or use any MQTT client of your choice.
Create a new connection with these settings:
| Setting | Value |
| ------------------- | --------------------------------------------------------------------- |
| Host | `localhost` (or your trial URL host for Cloud Trial) |
| Port | `1883` (local Docker or Compose) or **8883** with MQTTS (Cloud Trial) |
| Username / Password | The default credentials above |
For **Cloud Trial**, use **MQTTS** on port **8883**, enable **Encryption**, and **disable Validate certificate** in MQTT Explorer.
Subscribe to `$SYS/#` to see broker status messages. If you see topics such as `$SYS/Coreflux/Version`, the broker is running correctly.
You should see Coreflux system topics under `$SYS/Coreflux/`.
### If verification fails
Pick the symptom that matches your setup:
If you can't connect, confirm the broker is actually up using the tooling for your install method.
For a **Docker** install, inspect the container:
```bash wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
# Is the container running?
docker ps
# View broker logs (live)
docker logs -f coreflux_broker
# View HUB logs (if you started the HUB container)
docker logs -f coreflux_hub
# Restart the container
docker restart coreflux_broker
```
For **Docker Compose**, use `docker compose ps` and `docker compose logs -f` from the directory that contains `docker-compose.yml`. The broker container is named `coreflux-broker`.
For a **service install**, check the service status: `sudo systemctl status coreflux-broker` (Linux package install), `sudo systemctl status coreflux` (DigitalOcean droplet), or `sc query CorefluxBroker` (Windows).
**Symptom:** the HUB loads in some environments but not in a Docker-in-VM or remote setup.
**Fix:** use the host's actual address instead of `localhost`.
**Symptom:** the command errors with "cannot connect to the Docker daemon".
**Fix:** start Docker Desktop (or the Docker service) and run the command again.
**Symptom:** an error binding a port, e.g. `3000` or `1883` is already allocated.
**Fix:** stop the process using that port, or remap the host side of the port (for example `-p 1884:1883` on Docker, or change the left-hand port in `docker-compose.yml`).
**Symptom:** `docker ps` shows nothing after you start the broker.
**Fix:** run `docker logs coreflux_broker` to see why it exited. For Compose, run `docker compose logs broker` from the directory that contains `docker-compose.yml`.
**Symptom:** the HUB at `http://localhost:3000` fails to connect when **Host** is `localhost`, or shows **Broker isn't reachable**.
**Fix:** the HUB proxies MQTT from inside Docker. On **New Connection**, set **Host** to `broker`, **Protocol** `mqtt://`, **Port** `1883` (or **Protocol** `ws://`, **Port** `5000`, **Path** `mqtt`).
**Symptom:** the HUB or an MQTT client fails to connect right after you receive your trial email.
**Fix:** wait up to **60 seconds** for the broker to become available, then retry. In the browser, allow **unsecure access** (or proceed past the certificate warning) for the trial URL. In MQTT Explorer, use **MQTTS** on port **8883**, enable **Encryption**, and **disable Validate certificate**.
***
## Next Steps
Create your first LoT Action in under 15 minutes.
Set up the LoT language extension for syntax highlighting.
# Using AI with Coreflux
Source: https://docs.coreflux.org/latest/quick-start/using-ai
Go from installation to AI-generated LoT code in minutes — build Actions, Routes, and documentation with natural language prompts.
## Accelerate Your IIoT Development with AI
You've installed Coreflux and deployed your first Action. Now what if you could describe your next feature in plain English and have an AI assistant write the LoT (Language of Things) code for you — correctly, using verified syntax, following best practices?
That's exactly what Coreflux's AI integration enables. By connecting your AI assistant to the Coreflux MCP (Model Context Protocol), you give it real-time access to the official documentation. The result: you describe your goal, and the AI produces production-ready LoT code — Actions, Routes, Models, and more — without you memorizing a single keyword.
**Like hiring a LoT specialist who never sleeps.** You explain what your factory floor needs — "alert me when temperature spikes" — and the specialist drafts the code, wires the database, and writes the documentation.
### When to Use This
* You want to **build IIoT features faster** by describing them in natural language
* You're **new to LoT syntax** and want correct code without a learning curve
* You need to **connect PLCs, databases, and alerts** and want AI to handle the wiring
* You want AI to **document your system** as you build it
***
## Step 1: Installation & Prerequisites
Before using AI with Coreflux, ensure the core environment is running.
| Requirement | Details |
| ------------------- | ----------------------------------------------------------------- |
| **Coreflux Broker** | Installed and running on your machine or server |
| **AI Assistant** | Cursor, Claude Desktop, Claude.ai, or VS Code with GitHub Copilot |
| **MQTT Client** | MQTT Explorer or any MQTT client for verifying results |
Follow the [Installation Guide](/latest/quick-start/installation) for your platform (Docker, Docker Compose, Windows, or Linux). Verify the broker is running by connecting with MQTT Explorer.
Connect to the broker and subscribe to `$SYS/#`. If you see system topics, the broker is ready.
| Setting | Default Value |
| -------- | ------------- |
| Host | `localhost` |
| Port | `1883` |
| Username | `root` |
| Password | `coreflux` |
If you haven't completed the [Getting Started](/latest/quick-start/getting-started) guide yet, do that first. It takes under 15 minutes and confirms your environment is working.
***
## Step 2: Setup AI (MCP & Agents)
Coreflux uses the **Model Context Protocol (MCP)** to give your AI assistant real-time access to the official documentation. This means the AI doesn't guess at LoT syntax — it looks it up. Combined with an **AGENTS.md** file that defines your project's conventions, your AI assistant becomes a LoT expert that follows your team's rules.
### Configure the MCP Connection
Add the Coreflux documentation MCP server to your AI client. Use **Native** to install from the contextual menu on any docs page, or **Manual** to add the server URL to a config file.
Open the **Copy page** menu (top right) on any Coreflux documentation page. From there you can copy the MCP URL, run the install command, or connect directly to VS Code or Cursor.
Select **Connect to VS Code** from the contextual menu.
Click **Install** or **Install in Workspace** on the MCP server page.
Reload VS Code if needed, open Copilot Chat in **Agent** mode, and enable the Coreflux tools.
Select **Connect to Cursor** from the contextual menu.
In **Settings → Tools & MCPs**, confirm the **Install MCP Server?** details and click **Install**.
Verify **Coreflux Documentation** appears under **Installed MCP Servers** with tools enabled.
Use **Copy MCP Server** or **Copy MCP install command** from the menu, then follow the **Manual** tab below or the [MCP Configuration Guide](/ai/mcp#setup) for Claude Desktop and other clients.
Add this URL to your editor's MCP configuration:
```text wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
https://docs.coreflux.org/mcp
```
Create or open `.vscode/mcp.json` in your workspace:
```json wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
{
"servers": {
"coreflux": {
"type": "sse",
"url": "https://docs.coreflux.org/mcp"
}
}
}
```
Reload VS Code. The tools become available in Copilot's **Agent** mode.
Add the following to `.cursor/mcp.json` (project) or `~/.cursor/mcp.json` (global):
```json wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
{
"mcpServers": {
"coreflux": {
"url": "https://docs.coreflux.org/mcp"
}
}
}
```
Restart Cursor and verify the connection under **Settings → MCP**.
Add the following to your Claude Desktop configuration file:
`%APPDATA%\Claude\claude_desktop_config.json`
`~/Library/Application Support/Claude/claude_desktop_config.json`
`~/.config/Claude/claude_desktop_config.json`
```json wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
{
"mcpServers": {
"coreflux": {
"url": "https://docs.coreflux.org/mcp"
}
}
}
```
Restart Claude Desktop. The Coreflux tools appear in the tool picker (hammer icon).
For full setup details, editor-specific steps, and verification, see the [MCP Configuration Guide](/ai/mcp#setup).
### Set Up AGENTS.md (Project Rules)
An **AGENTS.md** file in your project root tells your AI assistant how to write LoT code — naming conventions, patterns, and what to avoid. Without it, the AI may use inconsistent styles or invent syntax.
Create an `AGENTS.md` file in your project root with your project's conventions. The [Best Practices & AGENTS.md](/ai/best-practices#setting-up-your-ai-assistant) page provides a complete starter template covering:
* **Naming rules** — PascalCase for entities, snake\_case for variables
* **Topic hierarchy** — `sensors/`, `processed/`, `alerts/` namespaces
* **Code standards** — Type casting, state management, modular Actions
* **Boundaries** — What the AI should always do, ask about, and never do
For a faster start, clone the [Coreflux AI Starter](https://github.com/CorefluxCommunity/coreflux-ai-starter) repo — it includes ready-to-use templates and pre-built MCP configs for every editor.
Once both the MCP and AGENTS.md are configured, your AI assistant has real-time documentation access **and** your project's coding standards. You're ready to build.
***
## Step 3: Create LoT with AI
With your AI assistant connected via MCP, you can use natural language to perform complex IIoT tasks. The following examples demonstrate the full workflow — from prompt to deployable code.
### A. Generate a LoT Action from Scratch
The most common starting point: you describe a behavior, and the AI writes the Action.
**The Prompt:**
```text wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
Create a LoT Action called TempAlert that monitors the topic
"factory/sensor/temp". If the temperature value (from a JSON payload
with key "value") goes above 50, publish an alert to "alerts/hvac"
with the current value. Use proper type casting.
```
**AI-Generated Output:**
The AI consults the Coreflux MCP documentation and produces verified LoT syntax:
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
DEFINE ACTION TempAlert
ON TOPIC "factory/sensor/temp" DO
SET "current_temp" WITH (GET JSON "value" IN PAYLOAD AS DOUBLE)
IF {current_temp} > 50 THEN
PUBLISH TOPIC "alerts/hvac" WITH "High Temperature Detected: " + {current_temp}
```
**Why this works:**
| Line | What It Does |
| ----------------------------------------------------------------- | ------------------------------------------------------------------------- |
| `DEFINE ACTION TempAlert` | Creates an Action named `TempAlert` (PascalCase, verb-like) |
| `ON TOPIC "factory/sensor/temp" DO` | Triggers every time a message arrives on this topic |
| `SET "current_temp" WITH (GET JSON "value" IN PAYLOAD AS DOUBLE)` | Extracts the `value` field from the JSON payload and casts it to a number |
| `IF {current_temp} > 50 THEN` | Conditional check against the threshold |
| `PUBLISH TOPIC "alerts/hvac" WITH ...` | Sends an alert with a descriptive message and the value |
**Deploy and Test:**
Paste the code into a LoT Notebook cell (`.lotnb` file) and run it, or publish to `$SYS/Coreflux/Command` with the `-addAction` prefix.
Publish to `factory/sensor/temp`:
```json wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
{"value": 62.5}
```
Subscribe to `alerts/hvac`. You should see:
```text wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
High Temperature Detected: 62.5
```
***
### B. PLC Integration & Automated Routing
A real-world scenario: you have a Siemens S7 PLC on the factory floor, and you want its data flowing into a PostgreSQL database. This requires **two Routes** — one to read from the PLC, and one to store in the database. Topics are the bridge between them.
**The Prompt:**
```text wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
I have a Siemens S7-1500 PLC at IP 192.168.1.10. I need to:
1. Read the temperature (REAL at DB1.DBD100) and pressure (REAL at DB1.DBD104)
every 500 milliseconds and publish them to MQTT topics
2. Store all readings in a PostgreSQL database called "production_db"
at 192.168.1.50, user "flux_user", password "secure_pass"
Create the LoT Routes for both.
```
**AI Logic:**
1. **Analyze** — The AI identifies the source (Siemens S7 PLC), the data bridge (MQTT topics), and the destination (PostgreSQL)
2. **Design** — It creates two Routes: an industrial Route for the PLC, and a data storage Route for the database
3. **Generate** — Using the MCP to verify syntax, it produces:
**Route 1 — Read from the PLC:**
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
DEFINE ROUTE S7Factory WITH TYPE SIEMENS_S7
ADD S7_CONFIG
WITH IP "192.168.1.10"
WITH CPU_TYPE "S71500"
WITH RACK '0'
WITH SLOT '1'
ADD MAPPING SensorData
WITH EVERY 500 MILLISECONDS
ADD TAG Temperature
WITH ADDRESS "DB1.DBD100"
WITH DATA_TYPE "REAL"
WITH SOURCE_TOPIC "plc/s7/temperature"
WITH UNIT "°C"
WITH DECIMAL_PLACES "2"
ADD TAG Pressure
WITH ADDRESS "DB1.DBD104"
WITH DATA_TYPE "REAL"
WITH SOURCE_TOPIC "plc/s7/pressure"
WITH UNIT "bar"
WITH DECIMAL_PLACES "2"
```
This Route reads PLC data and publishes it to MQTT topics every 500ms.
**Route 2 — Store in PostgreSQL:**
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
DEFINE ROUTE ProductionDB WITH TYPE POSTGRESQL
ADD SQL_CONFIG
WITH SERVER "192.168.1.50"
WITH PORT '5432'
WITH DATABASE "production_db"
WITH USERNAME "flux_user"
WITH PASSWORD "secure_pass"
ADD EVENT StoreTemperature
WITH SOURCE_TOPIC "plc/s7/temperature"
WITH QUERY "INSERT INTO sensor_readings (recorded_at, sensor, value) VALUES (NOW(), 'temperature', '{value}')"
ADD EVENT StorePressure
WITH SOURCE_TOPIC "plc/s7/pressure"
WITH QUERY "INSERT INTO sensor_readings (recorded_at, sensor, value) VALUES (NOW(), 'pressure', '{value}')"
```
The AI generates a database Route that subscribes to the MQTT topics published by the PLC Route and inserts every reading into a `sensor_readings` table — complete with SQL templates, connection config, and event triggers, without you writing a single line of SQL manually.
**The Data Flow:**
| Step | Component | What Happens |
| ---- | ---------------- | -------------------------------------------------------------- |
| 1 | S7 Route | Reads `DB1.DBD100` and `DB1.DBD104` from the PLC |
| 2 | MQTT Topics | Values published to `plc/s7/temperature` and `plc/s7/pressure` |
| 3 | PostgreSQL Route | Inserts each reading into the `sensor_readings` table |
The PLC Route and PostgreSQL Route are independent — they communicate through MQTT topics. This decoupled architecture means you can add more consumers (alerts, dashboards, other databases) without modifying the PLC Route. For a step-by-step walkthrough of building a database Route from scratch, see the [Developing with LoT Using AI](/ai/developing-with-lot#step-4-connect-to-a-database-route) guide.
***
### C. Modify Existing Code & Generate Documentation
AI excels at iterating on existing code and producing documentation alongside it.
**The Prompt:**
```text wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
Update the TempAlert action to also log every alert to "system/logs"
with a timestamp. Then generate a markdown summary documenting this
action on a project-specific LoT Notebook (LoTNB file).
```
**AI-Updated Code:**
The AI modifies the existing Action and adds the logging line:
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
DEFINE ACTION TempAlert
ON TOPIC "factory/sensor/temp" DO
SET "current_temp" WITH (GET JSON "value" IN PAYLOAD AS DOUBLE)
IF {current_temp} > 50 THEN
PUBLISH TOPIC "alerts/hvac" WITH "High Temperature Detected: " + {current_temp}
SET "log_time" WITH TIMESTAMP "UTC"
PUBLISH TOPIC "system/logs" WITH "TempAlert triggered at " + {log_time} + " — value: " + {current_temp}
```
| What Changed | Why |
| ------------------------------------------- | -------------------------------------------------- |
| Added `SET "log_time" WITH TIMESTAMP "UTC"` | Captures the current UTC timestamp into a variable |
| Added `PUBLISH TOPIC "system/logs"` | Creates an audit trail with timestamp and value |
**AI-Generated Documentation:**
The AI also produces a markdown summary you can add directly to your project's LoT Notebook (LoTNB):
```markdown wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
## Action: TempAlert
**Description:** Monitors temperature thresholds for the HVAC system.
Triggers an alert when the factory temperature sensor exceeds 50°C and
logs every alert event for auditing.
| Property | Value |
|----------|-------|
| **Trigger** | `factory/sensor/temp` |
| **Condition** | Temperature > 50°C |
**Outputs:**
| Topic | Purpose |
|-------|---------|
| `alerts/hvac` | Real-time alert with temperature value |
| `system/logs` | Timestamped audit trail of all triggered alerts |
**Payload Format:** Expects JSON with a `value` key (e.g., `{"value": 62.5}`)
```
This is the complete cycle: **build → modify → document** — all driven by natural language prompts.
***
## Best Practices
To get the most out of AI-assisted LoT development, keep these principles in mind:
* **Always verify the MCP connection** before starting a session. Without it, the AI may generate plausible-looking but incorrect LoT syntax
* **Be specific in your prompts** — include topic names, payload formats, thresholds, and hardware details. The more context you provide, the more accurate the output
* **Build incrementally** — deploy and test one Action, Route, or Model at a time before moving to the next
For detailed prompt templates, see [Prompt Patterns That Work](/ai/developing-with-lot#prompt-patterns-that-work). For naming conventions and a project rules template, see [Best Practices & AGENTS.md](/ai/best-practices).
If your AI assistant generates LoT code **without** consulting the MCP, the syntax may be invented. Always verify that the MCP tools are active before trusting the output.
***
## Next Steps
Deploy AI agents on the broker — chat, automation, and multi-agent teams defined in LoT.
Deep-dive into the full AI-assisted workflow — planning, building, verifying, and scaling your IIoT system.
Set up naming conventions, code patterns, and a project rules template to keep your AI assistant consistent.
# Using Coreflux HUB
Source: https://docs.coreflux.org/latest/quick-start/using-hub
Access the Coreflux HUB and manage your broker — MQTT topics, LoT code, routes, and system settings in one place.
## Manage Your Broker from the HUB
You have a Coreflux broker running. Now you want to explore MQTT topics, deploy LoT code, configure routes, and manage users — all without installing desktop tools or memorizing command-line syntax. The Coreflux HUB gives you a desktop or browser interface that puts every management task in one place.
**Like the dashboard of your car.** You don't pop the hood to check the speed or fuel level — you glance at the dashboard. The HUB does the same for your broker: real-time data, configuration, and diagnostics, all visible at a glance.
### When to Use This
* You want to **manage your broker visually** without installing MQTT Explorer or VS Code
* You need to **deploy and test LoT code quickly** from any machine
* You're **configuring routes** to databases or PLCs and want a guided, visual workflow
* You want to **monitor connected clients and system health** in real time
***
**Haven't installed the HUB yet?** Follow [Installation](/latest/quick-start/installation) — Docker, Docker Compose (HUB at `http://localhost:3000`), or the **HUB desktop** tab for Windows x64, Linux x64, and Linux ARM64.
## Access the HUB
Open `http://localhost:3000`. Click **New Connection** and set **Host** to `broker` (**Protocol** `mqtt://`, **Port** `1883`, or **Protocol** `ws://`, **Port** `5000`, **Path** `mqtt`) — the HUB proxies MQTT from inside Docker, so **Host** `localhost` does not reach the broker service.
| Setting | Value |
| -------- | ---------- |
| Username | `root` |
| Password | `coreflux` |
Run the downloaded HUB executable for your platform (Windows x64, Linux x64, or Linux ARM64). The HUB window opens — click **New Connection**, enter the broker host, protocol, port, and credentials, then click **Connect**.
| Setting | Value |
| -------- | ---------- |
| Username | `root` |
| Password | `coreflux` |
Change the default password before deploying to production. Go to **System → User Management** to update credentials.
***
## What Can You Do with Coreflux HUB?
Once you're logged in, the HUB gives you four dedicated apps that together cover every broker management task — from inspecting live data to deploying logic and connecting external systems.
Browse live topics, inspect payloads, publish test messages, and see which clients are connected.
Write LoT and Python as notebooks and deploy any cell to the broker — no external tools needed.
Connect your broker to databases, PLCs, and external systems using templates or LoT code, then monitor each connection in real time.
Configure the broker, manage users and permissions, run commands, and stream real-time logs for diagnostics.
The table below maps common tasks to the app that handles them so you always know where to go.
| Task | HUB App |
| -------------------------------------------------------------------- | ---------------- |
| Browse topics and inspect message payloads | **MQTT** |
| Publish a test message to any topic | **MQTT** |
| See which devices are connected | **MQTT** |
| Write or update an Action, Model, Rule, Route, or Python script | **LoT Editor** |
| Deploy LoT code to the broker instantly | **LoT Editor** |
| Check whether your code matches what the broker is running | **LoT Editor** |
| Add a route to a database or PLC | **Routes** |
| Monitor route connection health and logs | **Routes** |
| Change broker network or TLS settings | **System** |
| Create users and assign permissions | **System** |
| View real-time broker logs and run commands | **System** |
| Ask for insights, build dashboards, or draft LoT from plain language | **AI Assistant** |
The sections below walk you through each of these tasks hands-on.
***
## See Your MQTT Data
Open the **MQTT** app to browse every topic flowing through your broker in real time. The topic tree on the left lets you drill into any hierarchy, and selecting a topic shows its latest payload, QoS, and timestamp on the right.
Use this when you need to verify that a device is publishing, check payload formats, or publish a test message — all without leaving the browser. For a full walkthrough, see [Data Viewer](/hub/mqtt/data-viewer).
***
## Connect to an External Device with a Route
Open the **Routes** app to connect your broker to databases, PLCs, or other external systems. Pick a template, fill in the connection details, map MQTT topics to fields, and activate — the HUB handles the rest.
Each route card shows a live status indicator so you can tell at a glance whether the connection is healthy. For configuration details, see [Routes](/hub/routes/overview).
***
## Quickly Change the LoT Running on the Broker
Open the **LoT Editor** to write or redeploy LoT and Python as notebook cells. Run a single cell to deploy just that definition, or **Deploy All** for the whole project. Changes take effect immediately — no build step, no file transfer, no restart.
Every cell reports whether it matches the broker, so you can tell at a glance which definitions are live and which are still local. For the full editor reference, see [LoT Editor](/hub/lot-editor).
***
## Build With the AI Assistant
Open the **AI Assistant** from the Coreflux icon in the bottom dock and describe what you want in plain language — ask for insights from live data, build a dashboard with live widgets, or develop a LoT Action. The assistant uses the broker's own tools to draft, refine, and deploy the change in the same panel.
Use **Insight** mode for read-only exploration or switch to **Agent** mode when you want the assistant to create or update Routes, Actions, and panels. For setup, modes, and built-in safeguards, see [AI Assistant](/hub/ai-assistant).
***
## Debug Your LoT and Broker
Open the **System** app when something isn't behaving as expected. **Broker Logs** stream trace-level output in real time, and the **Command Console** lets you run broker commands directly — restart actions, inspect state, or change configuration on the fly.
Combine logs with the Data Viewer to trace a message from publish to action execution. For all system tools, see [System](/hub/system/overview).
***
## Next Steps
Deep-dive into each app — Data Viewer, LoT Editor, Routes, and System tools.
Build and deploy a working LoT Action in under 15 minutes.
# Using LoT Notebooks
Source: https://docs.coreflux.org/latest/quick-start/vscode
Use LoT Notebooks as living documentation—run LoT code, document your project, and see MQTT data all in one place.
## Overview
**LoT Notebooks** are **living documentation**: Markdown for documentation and runnable LoT code in the same file. You **run LoT** to deploy to the **Coreflux MQTT broker**, document with **Markdown** and use the **Data Viewer** to **see what is happening on the broker**.
**Write it, run it, see the result in the broker.** With LoT Notebooks and LoT VS Code Extension, document the "why," deploy the "what," and see the MQTT traffic and execution logic side by side with your notebook.
## Before you start
* **Visual Studio Code** installed
* A **Coreflux broker** installed or in another machine you can reach
## Install and connect
1. Open VS Code.
2. Open the Extensions view in the sidebar (or use the shortcut `Ctrl+Shift+X` on Windows/Linux, `Cmd+Shift+X` on macOS).
3. Search for **LoT Notebooks** by **Coreflux** and install it.
1. Click on the MQTT Connection Status on bottom left corner of your VS Code or click **Connect to Broker** in the **Data Viewer** (Coreflux sidebar).
2. Enter your broker **URL**, **username**, and **password**.
Use a full URL such as `mqtt://localhost:1883` or `mqtts://your-host:8883` for TLS.
| Setting | Example (local defaults) |
| -------- | --------------------------------------------- |
| URL | `mqtt://localhost:1883` |
| Username | `root` |
| Password | `coreflux` (change immediately in production) |
When you are connected, the status bar shows a green **MQTT: Connected**. If any issues occur, the status bar will become red.
## Create a LoT notebook and deploy your first LoT code
1. In the Activity Bar (left bar), open **Coreflux**.
2. Under **PROJECT**, use **New File**, give your **LoT Notebook** a name, and it will be created within the **workspace folder** you opened in VS Code.
3. Add a code cell from the notebook toolbar (**+ Code**). Optionally, add a Markdown cell (**+ Markdown**) to document your code.
4. Write or paste a LoT code (for example the Action below) and click **Execute Cell** icon on the cell or **Run All** option above.
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
DEFINE ACTION Heartbeat
ON EVERY 10 SECONDS DO
PUBLISH TOPIC "system/heartbeat" WITH TIMESTAMP "UTC"
```
5. See the data change in the **Data Viewer**, by subscribing to all topics, just like MQTT Explorer. Additionally, you can click on a data topic to see it in more detail, including a replay of the data changes.
## Next Steps
Workflow, cells, broker connection, commands, Git, best practices, and troubleshooting.
Data Viewer, replay, execution trace, and debug on LoT Notebooks.
# Updating Your Coreflux
Source: https://docs.coreflux.org/latest/updating-coreflux
Upgrade Coreflux on your platform — Docker, Docker Compose, Cloud Trial, DigitalOcean, Windows, or Linux (APT and DNF packages).
## When to Update
Update Coreflux when a new release includes fixes or features you need. Check [Release Notes](/product-updates/release-notes) for what changed in each version.
**Keep your project files.** Docker named volumes, package install directories, and manual install folders retain LoT, routes, and configuration when you upgrade only the broker runtime.
***
## Update Coreflux
To run the latest image, stop and remove the existing container, then start a new one with the same `docker run` command from [Installation](/latest/quick-start/installation). The `--pull=always` flag ensures Docker fetches the newest `latest` tag before each start.
**Use your actual names.** The examples below use the default container name `coreflux_broker` and volume `project-volume`. If you chose different names when you first installed, replace them in every command.
```bash wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
docker stop coreflux_broker
docker rm coreflux_broker
```
This stops the broker and removes the container. Named volumes (such as `project-volume`) are **not** deleted—your project files remain on disk.
Run the same command you used for installation. For the standard setup:
```bash wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
docker run --pull=always -d --name coreflux_broker -p 1883:1883 -p 5000:5000 -p 9100:9100 -v project-volume:/etc/project coreflux/coreflux-mqtt-broker:latest
```
If you use TLS ports, use the command from the [TLS/SSL accordion on Installation](/latest/quick-start/installation)—still with `--pull=always`, the same container name, and the same `project-volume` mount.
Confirm the broker is up with `curl http://localhost:9100/health`, or subscribe to `$SYS/Coreflux/Version` from an MQTT client. The broker image does **not** include the HUB — if you also run the HUB container, open [Coreflux HUB](/hub/overview) at `http://localhost:3000`.
Pull the latest images and recreate the stack. Named volumes (`broker-data`, `hub-data`) are kept unless you pass `-v` to `docker compose down`.
From the directory that contains `docker-compose.yml`:
```bash wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
docker compose --profile full pull
docker compose --profile full up -d
```
Both services use the `latest` tag, so `docker compose pull` fetches the newest broker and HUB images. See [Installation](/latest/quick-start/installation) for the Compose file.
Open the [Coreflux HUB](/hub/overview) at `http://localhost:3000` and connect with `mqtt://broker:1883`. Subscribe to `$SYS/Coreflux/Version` in the Data Viewer to confirm the broker version. You can also probe `http://localhost:9100/health`.
Cloud Trial environments are managed by Coreflux. When an update is available, your trial instance is upgraded on the hosted side—you do not run install or update commands locally.
Continue using the **URL and credentials** from your trial email. If the HUB or MQTT client fails right after a platform update, wait up to 60 seconds and reconnect—the broker may still be restarting.
See [Release Notes](/product-updates/release-notes) for new features you can use once the trial environment is on the latest version.
Coreflux on a DigitalOcean droplet runs as a **systemd** service. SSH into the droplet and replace the broker binary, then restart the service.
**Use your actual paths and service name.** Marketplace images typically use the `coreflux` systemd unit and install under `/opt/coreflux`. Adjust commands if you changed these during setup.
```bash wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
ssh root@
```
```bash wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
sudo systemctl stop coreflux
```
Download the latest Linux build from [coreflux.org/start-for-free](https://www.coreflux.org/start-for-free), extract it, and replace `CorefluxMQTTBroker` in your install directory (for example `/opt/coreflux`). Keep existing configuration and project files in that directory.
```bash wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
sudo systemctl start coreflux
sudo systemctl status coreflux
```
Update a Windows service install by stopping the service, replacing the executable, and starting it again.
**Use your actual service and paths.** Examples use the service name `CorefluxBroker` and install path `C:\Coreflux`. Change these if you registered the service differently—see [Installation](/latest/quick-start/installation).
Open Command Prompt **as Administrator** and run:
```cmd wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
sc stop CorefluxBroker
```
Download the latest Windows zip from [coreflux.org/start-for-free](https://www.coreflux.org/start-for-free), extract it, and copy `CorefluxMQTTBroker.exe` over the file in your install folder (for example `C:\Coreflux`). Do not remove your existing config or project files in that folder.
```cmd wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
sc start CorefluxBroker
sc query CorefluxBroker
```
**\~2 min** if you installed from [packages.coreflux.org](https://packages.coreflux.org) — refresh indexes and upgrade the `coreflux-broker` package.
Use the tab that matches how you installed the broker. See [Installation](/latest/quick-start/installation) if you still need to add the Coreflux repository.
```bash wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
sudo apt update
```
Optional — see which version is installed and what is available:
```bash wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
apt-cache policy coreflux-broker
```
```bash wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
sudo apt upgrade coreflux-broker
```
To upgrade only the broker without touching other packages:
```bash wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
sudo apt install --only-upgrade coreflux-broker
```
The package upgrade restarts `coreflux-broker` via systemd. Confirm it is running:
```bash wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
systemctl status coreflux-broker.service
```
Subscribe to `$SYS/Coreflux/Version` from an MQTT client, or open [Coreflux HUB](/hub/overview) if you run it separately.
```bash wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
sudo dnf makecache --refresh
```
Optional — list available versions:
```bash wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
dnf list --showduplicates coreflux-broker
```
```bash wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
sudo dnf upgrade coreflux-broker
```
The package upgrade restarts `coreflux-broker` via systemd. Confirm it is running:
```bash wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
systemctl status coreflux-broker.service
```
Subscribe to `$SYS/Coreflux/Version` from an MQTT client, or open [Coreflux HUB](/hub/overview) if you run it separately.
***
## Next Steps
See what changed in recent releases.
Full install commands for every platform.
# AI-Assisted Development Best Practices
Source: https://docs.coreflux.org/ai/best-practices
Set up your AI coding assistant to generate correct, consistent LoT code using AGENTS.md templates and project rules
## Make Your AI Assistant a LoT Expert
AI coding assistants — in Cursor, VS Code with Copilot, or Claude — can generate excellent LoT (Language of Things) code when they know your project's conventions. The key is giving them the right context upfront through an **AGENTS.md** or project rules file.
This page shows you how to set up that context so your assistant produces LoT code that matches your team's standards from the first prompt.
**Like a style guide that your co-pilot actually reads.** Just as a design system keeps a UI consistent across 10 developers, a project rules file keeps your LoT code consistent across humans and AI assistants alike.
### When to Use This
* You want your **AI assistant to generate correct LoT code** without constant corrections
* You're setting up an **AGENTS.md**, `.cursor/rules/`, or `CLAUDE.md` file for your project
* You're onboarding **new team members** (human or AI) and need a single reference
* You're starting a **new Coreflux project** and want AI-ready conventions from day one
This page focuses on **AI assistant configuration**. For the underlying conventions, naming rules, topic architecture, code patterns, and anti-patterns, see [Naming conventions](/latest/lot-language/usage-patterns/naming-conventions), [Working with JSON](/latest/lot-language/usage-patterns/working-with-json), [Designing your data layer](/latest/lot-language/usage-patterns/data-layer), and [Anti-patterns](/latest/lot-language/usage-patterns/anti-patterns) — those standards are the foundation for everything below.
***
## Setting Up Your AI Assistant
An **AGENTS.md** file (or equivalent project rules file) tells your AI assistant how to work with your Coreflux project. It prevents the assistant from generating code with wrong naming, invented syntax, or inconsistent patterns. The conventions defined in [Naming conventions](/latest/lot-language/usage-patterns/naming-conventions) form the content — the structure below tells you how to organize them for your assistant.
### What Is an AGENTS.md?
An AGENTS.md is a markdown file in your project root that AI coding assistants read automatically. It works across tools:
| File / Location | Supported By |
| --------------------------------- | --------------------------------------------------------- |
| `AGENTS.md` (project root) | Cursor, GitHub Copilot, OpenAI Codex, Google Jules, Aider |
| `.cursor/rules/*.mdc` | Cursor (native format with glob pattern support) |
| `CLAUDE.md` (project root) | Claude Code |
| `.github/copilot-instructions.md` | GitHub Copilot |
For maximum compatibility, use **AGENTS.md** in your project root. If you use Cursor extensively, you can also maintain `.cursor/rules/` files for features like auto-attaching rules to specific file types.
### The Three-Tier Boundary System
The most effective structure for preventing AI mistakes is a **three-tier permission system**. Define what the assistant should always do, what it should ask about first, and what it should never do:
| Tier | Guideline | LoT Examples |
| ------------- | ------------------------- | ----------------------------------------------------------------------------------------------------------- |
| **Always do** | Follow without asking | Use PascalCase for entities, `snake_case` for variables, `lot` language tag for code blocks |
| **Ask first** | Confirm before proceeding | Adding new Routes (external connections), modifying Rules (access control), removing Actions |
| **Never do** | Refuse even if asked | Invent LoT syntax that doesn't exist, use `LOT` or `lot` capitalization, mention competitor broker products |
### Starter Template
The following template incorporates the conventions from [Naming conventions](/latest/lot-language/usage-patterns/naming-conventions) and related pattern pages. Copy it into an `AGENTS.md` file at your project root and customize the project-specific sections:
**Prefer to grab it from GitHub?** The [Coreflux AI Starter](https://github.com/CorefluxCommunity/coreflux-ai-starter) repository has ready-to-use templates (minimal, full, and industrial), pre-built MCP configs for every editor, and filled-in examples you can clone directly.
A cross-platform template that works with Cursor, Copilot, Claude, and other AI tools:
```markdown wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
# AGENTS.md
## Project Overview
This is a Coreflux LoT (Language of Things) project for [describe your system].
Built on the Coreflux MQTT broker with LoT Actions, Models, Rules, and Routes.
## Tech Stack
- **Platform:** Coreflux MQTT Broker
- **Language:** LoT (Language of Things)
- **Extensions:** Python integration (for complex logic only)
- **IDE:** VS Code with LoT Notebooks extension / Cursor
## LoT Naming Conventions
- **Actions:** PascalCase, verb-first (ProcessTemperature, MonitorPressure)
- **Models:** PascalCase, noun-based (SensorReading, EquipmentStatus)
- **Rules:** PascalCase, descriptive scope (AllowAdminActions, ProtectSysTopics)
- **Routes:** PascalCase, destination-based (CloudBridge, SensorDatabase)
- **Variables:** snake_case in quotes ("sensor_id", "raw_value")
- **Topics:** lowercase/slash-separated (sensors/+/temperature, alerts/critical/+)
- **Model fields:** snake_case ("equipment_id", "runtime_hours")
## Topic Hierarchy
- sensors/ — Raw incoming data
- processed/ — Transformed data
- alerts/ — Threshold violations
- config/ — Configuration values
- state/ — Internal persistent state (KEEP TOPIC)
- cache/ — Cached values
- system/ — Heartbeats and diagnostics
- commands/ — Inbound instructions
## Code Standards
- Always type-cast in calculations: PAYLOAD AS DOUBLE, GET TOPIC ... AS DOUBLE
- Use KEEP TOPIC for internal state, PUBLISH TOPIC for external broadcast
- Mark the primary data field AS TRIGGER in models — never timestamps
- Use COLLAPSED models when Actions need control over publish timing
- Break complex Actions into callable Actions with INPUT/OUTPUT
- Use LoT for publish, subscribe, conditionals, math — Python only for
ML, complex parsing, or external library calls
## Do
- Always respect the indentation of 4 spaces per additional level
- Use PascalCase for all entity names
- Use snake_case for all variables and model fields
- Add type casts to every numeric operation
- Use descriptive names that explain purpose
- Use lot as the code block language identifier
- Booleans and integers do not use quotes
- Consult the Coreflux MCP for documentation when unsure
- Routes that call functions from other protocols (like SQL queries) are not limited to the functionalities listed in this documentation, the full range of that external element is usually functional
## Don't
- Do not invent LoT syntax — only use documented keywords
- Do not use LOT (all caps) or lot (lowercase) — always LoT
- Do not mention competitor products (Mosquitto, HiveMQ, etc.)
- Do not trigger models on timestamp fields
- Do not use PUBLISH for internal-only state
- Do not create monolithic Actions — split into callables
## Ask First
- Before adding or modifying Routes (external system connections)
- Before modifying Rules (access control changes)
- Before removing any existing Action, Model, Rule, or Route
- Before using Python — confirm LoT can't handle it natively first
## Never
- Never hardcode credentials in Route definitions for production
- Never leave $SYS/# topics unrestricted
- Never skip type casting in mathematical operations
- Never generate LoT syntax you haven't verified in the documentation
```
For Cursor-native rules, create `.cursor/rules/lot-conventions.mdc` in your project:
```markdown wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
---
description: LoT development conventions for Coreflux projects
globs: ["*.lot", "*.lotnb", "*.md"]
---
# LoT Conventions
You are an expert LoT (Language of Things) developer for Coreflux.
## Naming
- Actions: PascalCase, verb-first (ProcessTemperature)
- Models: PascalCase, noun-based (SensorReading)
- Rules: PascalCase, descriptive (ProtectSysTopics)
- Routes: PascalCase, destination-based (CloudBridge)
- Variables: snake_case in quotes ("sensor_id")
- Topics: lowercase/slash-separated (sensors/+/temperature)
## Code Rules
- Always type-cast: PAYLOAD AS DOUBLE, GET TOPIC ... AS DOUBLE
- KEEP TOPIC for internal state, PUBLISH TOPIC for broadcast
- AS TRIGGER on primary data field, never timestamps
- Use COLLAPSED models when Actions control publish timing
- Use lot as the language tag for code blocks
- Python only for ML, complex parsing, or external libraries
## Prohibited
- Do not invent LoT syntax beyond documented keywords
- Do not use LOT or lot — always LoT
- Do not mention competitor MQTT brokers
- Do not create monolithic Actions — split into callables
```
### Keeping It Effective
The best project rules files share these qualities:
| Quality | Why It Matters |
| ------------------ | -------------------------------------------------------------- |
| **Specific** | "Use PascalCase for Actions" beats "use good naming" |
| **Example-driven** | One code snippet beats three paragraphs of description |
| **Iterative** | Start small, then add rules when you see the AI make a mistake |
| **Current** | Update the file as your project conventions evolve |
| **Concise** | A 50-line focused file outperforms a 500-line generic one |
Do not include credentials, API keys, or sensitive connection strings in your AGENTS.md or rules files. These files are typically committed to version control.
***
## Quick Reference Checklist
Use this checklist when reviewing LoT code — whether written by you or generated by an AI assistant:
* [ ] All entity names are **PascalCase** and descriptive
* [ ] All variables and model fields are **snake\_case**
* [ ] All topics are **lowercase with slash separators**
* [ ] All numeric operations include **explicit type casts** (`AS DOUBLE`, `AS INT`)
* [ ] Models trigger on **primary data fields**, not timestamps
* [ ] Internal state uses **`KEEP TOPIC`**, not `PUBLISH TOPIC`
* [ ] Complex logic is split into **callable Actions** with `INPUT`/`OUTPUT`
* [ ] Rules use **permission tags** (`USER HAS`) over user names (`USER IS`)
* [ ] Python is used only where **LoT can't handle the task natively**
* [ ] `$SYS/#` access is left to the broker's **locked built-in Rules** — no permission flags granted to device or operator accounts
* [ ] Custom Rule priorities sit in the **user band** (`100`–`1 000 000`), with broad rules high and specific carve-outs near `100`
***
## Next Steps
Connect your AI assistant to the Coreflux documentation for real-time syntax help and guided answers.
GET JSON patterns and structured output with Models.
# Developing with LoT Using AI
Source: https://docs.coreflux.org/ai/developing-with-lot
A beginner's guide to building IIoT solutions with LoT — let your AI assistant handle the syntax while you focus on what to build
## Build IIoT Solutions Without Memorizing a Single Command
You don't need to be a LoT (Language of Things) expert to build industrial IoT solutions with Coreflux. With an AI assistant connected to the Coreflux MCP, you describe **what** you want in plain English, and the AI writes the LoT code for you — correctly, following best practices, using real syntax from the official documentation.
This page walks you through the entire workflow: from setting up the MCP connection to deploying a working IIoT feature, using only natural language prompts. Whether you're monitoring factory temperatures, logging production data to a database, or bridging sensor networks to the cloud — the process is the same.
**Like dictating a blueprint to an expert architect.** You describe the building you want — "a sensor system that alerts when temperatures spike" — and the architect draws up structurally sound plans, handles the engineering codes, and hands you something ready to build.
### When to Use This Guide
* You're **new to Coreflux** and want to build something real without learning LoT syntax first
* You have an **AI coding assistant** (Cursor, Claude, Copilot) and want to use it effectively for IIoT
* You want a **step-by-step walkthrough** of the AI-assisted development workflow
* You're exploring whether Coreflux fits your **industrial automation** or **IoT data pipeline** needs
***
## Prerequisites
Before starting, make sure you have:
| Requirement | Details |
| ------------------------- | ------------------------------------------------------------------------------------------ |
| **Coreflux Broker** | Installed and running. See the [Installation Guide](/latest/quick-start/installation) |
| **AI Assistant with MCP** | Cursor, Claude Desktop, Claude.ai, or VS Code with Copilot — connected to the Coreflux MCP |
| **MQTT Client** | MQTT Explorer or any MQTT client for verifying results |
If you haven't connected the Coreflux MCP to your AI assistant yet, follow the [MCP Setup Guide](/ai/mcp#setup) first. It takes under 5 minutes.
You do **not** need prior experience with LoT syntax, MQTT, or industrial protocols. The AI assistant handles the technical details — you focus on describing your goals.
***
## The AI-Assisted Workflow
Every AI-assisted Coreflux project follows three phases. This isn't a suggestion — it's the workflow that consistently produces working results.
| Phase | You Do | The AI Does |
| ------------- | ------------------------------------- | --------------------------------------------------- |
| **1. Plan** | Describe the feature in plain English | Asks clarifying questions, proposes an architecture |
| **2. Build** | Review and approve the AI's output | Writes LoT code using the MCP for accurate syntax |
| **3. Verify** | Test the result in MQTT Explorer | Explains what to check and helps debug issues |
The key insight is that **you are the domain expert** and **the AI is the LoT expert**. You know your factory floor, your sensor layout, your business rules. The AI knows the syntax, the patterns, and the best practices. Together, you build faster than either could alone.
***
## Phase 1: Plan Your Feature
The most important step happens before any code is written. A clear plan gives the AI the context it needs to produce working LoT code on the first try.
### Describe Your Goal, Not Your Code
Tell the AI **what** you want to achieve, not **how** to write it. Let it choose the right LoT building blocks.
The more specific you are on the components you are working with (for example: hardware brands, specific protocols, IP addresses, ports, etc.), the better the end result will tend to be.
A clear, goal-oriented description that gives the AI enough context to make architectural decisions:
```text wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
I have temperature sensors from brand Y, using ModBus, publishing readings to MQTT topics like
sensors/temp001/reading as JSON: {"value": 23.5, "unit": "celsius"}.
I want to:
1. Monitor these readings and trigger an alert when temperature exceeds 75°C
2. Store all readings in a PostgreSQL database
3. Convert Celsius to Fahrenheit and publish the converted value
The alert should publish to an alerts/ topic with the sensor ID and the value.
```
A vague prompt that forces the AI to guess about critical details:
```text wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
Make me a temperature action.
```
This tells the AI nothing about your topic structure, thresholds, data format, or what you want to happen. The result will be generic at best.
### The Planning Checklist
Before you prompt your AI assistant, gather answers to these questions. You don't need to answer all of them — but the more you provide, the better the result.
| Question | Why It Matters | Example Answer |
| ------------------------------------- | ----------------------------------------------------- | ----------------------------------------------- |
| **What data comes in?** | Defines the trigger topics and payload format | `sensors/+/reading` with JSON `{"value": 23.5}` |
| **What should happen to it?** | Determines which LoT building blocks to use | Transform, alert, store, forward |
| **Where should results go?** | Defines output topics, databases, or external systems | PostgreSQL table, alert topic, cloud broker |
| **What are the thresholds or rules?** | Sets up conditionals in Actions | Alert when temp > 75°C |
| **How many sensors/devices?** | Affects topic structure and wildcard usage | 50 sensors, each with a unique ID |
You can share this checklist directly with the AI. Paste it into your prompt and fill in the answers — the AI will use them to design the complete system.
### Ask the AI to Propose an Architecture
Once you've described your goal, ask the AI to **plan before coding**. This avoids wasted iteration.
```text wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
Before writing any code, propose an architecture for this system.
Tell me which LoT building blocks you'll use (Actions, Models, Routes, Rules)
and why. List the MQTT topics you'll create.
```
The AI will consult the Coreflux MCP documentation and respond with something like:
> *"For this system, I recommend:*
>
> * *An **Action** to monitor temperatures and trigger alerts*
> * *A **Model** to structure sensor readings into consistent JSON*
> * *A **Route** to store readings in PostgreSQL*
> * *A **callable Action** for the Celsius-to-Fahrenheit conversion (reusable)*
>
> *Topic structure: `sensors/+/reading` (input), `processed/+/fahrenheit` (converted), `alerts/temperature/+` (alerts)"*
Review this before saying "go ahead." It's much easier to adjust a plan than to rewrite code.
***
## Phase 2: Build with AI
Now let's walk through a complete guided demo. We'll build a **temperature monitoring system** that reads sensor data, converts units, triggers alerts, and logs to a database — all by prompting the AI.
### Step 1: Create the Core Logic (Actions)
Start with the Action that processes incoming data. Paste this prompt into your AI assistant:
```text wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
Using the Coreflux MCP for reference, create a LoT Action called
ProcessTemperature that:
- Triggers on topic "sensors/+/temperature"
- Extracts the sensor ID from the topic
- Gets the temperature value from a JSON payload with key "value"
- If the temperature exceeds 75, publishes an alert to
"alerts/temperature/" followed by the sensor ID
- Publishes the reading to "processed/" followed by the sensor ID
Use proper LoT syntax with type casting.
```
The AI will consult the Coreflux documentation through the MCP and produce working LoT code. Here is what a correct result looks like:
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
DEFINE ACTION ProcessTemperature
ON TOPIC "sensors/+/temperature" DO
SET "sensor_id" WITH TOPIC POSITION 2
SET "temp" WITH (GET JSON "value" IN PAYLOAD AS DOUBLE)
IF {temp} > 75 THEN
PUBLISH TOPIC "alerts/temperature/" + {sensor_id} WITH "HIGH TEMP: " + {temp} + "°C"
PUBLISH TOPIC "processed/" + {sensor_id} WITH {temp}
```
The AI uses the MCP's `consult_documentation` and `consult_internal_documentation` tools behind the scenes to look up correct syntax. You don't need to tell it to use the MCP — it does this automatically when it detects a Coreflux-related question.
### Step 2: Add Unit Conversion (Callable Action)
Now ask the AI to add a reusable conversion utility:
```text wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
Create a callable LoT Action called ConvertToFahrenheit that takes a Celsius
value as input and returns the Fahrenheit equivalent. Then update
ProcessTemperature to call it and publish the result to
"processed/" + sensor ID + "/fahrenheit".
```
The AI will produce two connected Actions:
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
DEFINE ACTION ConvertToFahrenheit
INPUT celsius AS DOUBLE
DO
SET "fahrenheit" WITH ({celsius} * 9 / 5 + 32)
RETURN
OUTPUT fahrenheit
DEFINE ACTION ProcessTemperature
ON TOPIC "sensors/+/temperature" DO
SET "sensor_id" WITH TOPIC POSITION 2
SET "temp" WITH (GET JSON "value" IN PAYLOAD AS DOUBLE)
IF {temp} > 75 THEN
PUBLISH TOPIC "alerts/temperature/" + {sensor_id} WITH "HIGH TEMP: " + {temp} + "°C"
PUBLISH TOPIC "processed/" + {sensor_id} WITH {temp}
CALL ACTION ConvertToFahrenheit
WITH celsius = {temp}
RETURN temp_f
PUBLISH TOPIC "processed/" + {sensor_id} + "/fahrenheit" WITH {temp_f}
```
### Step 3: Structure the Data (Model)
Ask the AI to create a Model that formats raw sensor readings into consistent JSON:
```text wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
Create a LoT Model called TemperatureReading that publishes structured JSON
to "sensors/formatted/temperature". It should include the sensor ID as a
static string "TEMP001", the raw value from the sensor topic as the trigger,
the unit as "celsius", and a UTC timestamp.
```
The result:
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
DEFINE MODEL TemperatureReading WITH TOPIC "sensors/formatted/temperature"
ADD STRING "sensor_id" WITH "TEMP001"
ADD DOUBLE "value" WITH TOPIC "sensors/raw/temperature" AS TRIGGER
ADD STRING "unit" WITH "celsius"
ADD STRING "timestamp" WITH TIMESTAMP "UTC"
```
### Step 4: Connect to a Database (Route)
Now bring in a Route to persist the data. Ask the AI:
```text wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
Create a LoT Route called SensorDatabase that stores temperature readings
in a PostgreSQL database. It should:
- Connect to a PostgreSQL server at "db.example.com" port 5432
- Use database "iot_data" with user "iot_writer" and password "secure_pass"
- Insert readings from "sensors/+/temperature" into a table called
"temperature_readings" with columns: timestamp, sensor_id, and value
Use proper LoT route syntax from the Coreflux documentation.
```
The AI generates:
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
DEFINE ROUTE SensorDatabase WITH TYPE POSTGRESQL
ADD SQL_CONFIG
WITH SERVER "db.example.com"
WITH PORT '5432'
WITH DATABASE "iot_data"
WITH USERNAME "iot_writer"
WITH PASSWORD "secure_pass"
ADD EVENT StoreReading
WITH SOURCE_TOPIC "sensors/+/temperature"
WITH QUERY "INSERT INTO temperature_readings (timestamp, sensor_id, value) VALUES (NOW(), '{value.topic.2}', '{value.json.value}')"
```
### Step 5: Deploy Everything
Ask the AI how to deploy your code:
```text wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
How do I deploy all of these LoT definitions to my Coreflux broker?
I'm using VS Code with the LoT Notebooks extension.
```
The AI will explain the deployment process:
The recommended way to deploy LoT code is through the **LoT Notebooks** extension in VS Code or Cursor.
Create a new `.lotnb` file in your project. Each cell can contain one or more LoT definitions.
Paste each definition into its own cell — this lets you deploy and test them individually.
Execute cells in order. The extension sends the LoT code to your connected Coreflux broker.
Open MQTT Explorer and subscribe to `#` (all topics). Publish a test payload to `sensors/temp001/temperature` and watch the system respond.
You can deploy LoT code by publishing directly to the broker's command topic using any MQTT client.
Publish to the topic `$SYS/Coreflux/Command` with the `-addAction` prefix followed by your LoT code:
```text wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
-addAction DEFINE ACTION ProcessTemperature
ON TOPIC "sensors/+/temperature" DO
SET "sensor_id" WITH TOPIC POSITION 2
SET "temp" WITH (GET JSON "value" IN PAYLOAD AS DOUBLE)
IF {temp} > 75 THEN
PUBLISH TOPIC "alerts/temperature/" + {sensor_id} WITH "HIGH TEMP: " + {temp} + "°C"
PUBLISH TOPIC "processed/" + {sensor_id} WITH {temp}
```
Use `-addModel` for Models and `-addRoute` for Routes.
***
## Phase 3: Verify and Iterate
After deploying, test the system by publishing a simulated sensor reading.
### Test Your System
Open MQTT Explorer (or any MQTT client) and follow these steps:
Subscribe to `#` to see all messages flowing through the broker.
Publish to `sensors/temp001/temperature` with this payload:
```json wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
{"value": 82.3}
```
You should see messages appear on these topics:
| Topic | Expected Value | Source |
| ------------------------------ | ------------------- | ---------------------------- |
| `processed/temp001` | `82.3` | ProcessTemperature Action |
| `processed/temp001/fahrenheit` | `180.14` | ConvertToFahrenheit callable |
| `alerts/temperature/temp001` | `HIGH TEMP: 82.3°C` | Alert threshold exceeded |
Publish to `sensors/temp001/temperature` with `{"value": 45.0}`. Verify that no alert is generated — only the processed and fahrenheit topics should update.
### When Something Doesn't Work
If the output isn't what you expect, describe the problem to the AI:
```text wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
I deployed the ProcessTemperature action, but when I publish
{"value": 82.3} to sensors/temp001/temperature, I don't see
any alert on alerts/temperature/temp001. What could be wrong?
```
The AI will consult the MCP documentation and help you debug — checking for type casting issues, topic mismatches, or deployment errors. **You don't need to debug LoT syntax yourself.** Describe the symptom, and the AI diagnoses the cause.
***
## Prompt Patterns That Work
After building dozens of IIoT features with AI, these prompt patterns consistently produce the best results.
### Pattern 1: Context → Goal → Constraints
Provide the situation, then state what you want, then add any constraints:
```text wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
Context: I have 20 pressure sensors publishing to "sensors/pressure/+/psi"
with numeric payloads.
Goal: Alert when any sensor exceeds 150 PSI and log all readings to a
PostgreSQL table.
Constraints: Alerts must include sensor ID and timestamp. Use a single
Action for monitoring and a Route for database logging.
```
### Pattern 2: Ask for Explanation Before Code
When learning, ask the AI to explain its choices:
```text wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
I need to store sensor data in a database. Should I use a Route or an Action
for this? Explain the trade-offs, then show me the LoT code for the
recommended approach. Use the Coreflux documentation for reference.
```
### Pattern 3: Incremental Building
Build complex systems one piece at a time rather than all at once:
```text wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
Step 1: "Create an Action that reads temperature and publishes to a processed topic"
Step 2: "Now add an alert when temperature exceeds 75"
Step 3: "Add a Route to store readings in PostgreSQL"
Step 4: "Create a Model to structure the data as JSON with sensor ID and timestamp"
```
Each step is small enough for the AI to get right, and you verify as you go.
### Pattern 4: Reference the MCP Explicitly
When you need maximum accuracy, tell the AI to consult the documentation:
```text wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
Check the Coreflux documentation: what is the correct syntax for defining
a Modbus TCP route that reads holding registers from a PLC at 192.168.1.10?
Give me a complete LoT route definition.
```
This triggers the AI to call `consult_documentation` or `consult_internal_documentation` through the MCP, ensuring the answer uses real, verified syntax.
***
## Common Pitfalls
These mistakes happen frequently when developers start using AI for LoT development. Knowing them upfront saves hours of debugging.
| Pitfall | Why It Happens | How to Avoid It |
| ------------------------------- | ---------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------- |
| **Vague prompts** | AI fills in gaps with assumptions that may not match your system | Always specify topic structure, payload format, and thresholds |
| **Skipping the plan** | Jumping straight to "write me an Action" without context | Ask the AI to propose an architecture before writing code |
| **Not verifying output** | Trusting AI code without testing it | Always deploy and test with a sample payload in MQTT Explorer |
| **Prompting without MCP** | The AI invents plausible-looking but incorrect LoT syntax | Ensure the Coreflux MCP is connected — check your MCP settings |
| **Building everything at once** | One massive prompt produces code that's hard to debug | Build incrementally: one Action, one Model, one Route at a time |
| **Ignoring type casting** | Numeric operations fail silently without `AS DOUBLE` or `AS INT` | Ask the AI to always include type casts — or review [Anti-patterns](/latest/lot-language/usage-patterns/anti-patterns) |
If your AI assistant is not connected to the Coreflux MCP, it may generate LoT-like code that **looks** correct but uses invented syntax. Always verify the MCP connection before starting a session. Check that the `consult_documentation` and `consult_internal_documentation` tools appear in your assistant's available tools.
***
## Expanding Your System
Once the basic monitoring system works, you can extend it with a single prompt each. Here are natural next steps:
| Feature | Example Prompt |
| ----------------------- | ---------------------------------------------------------------------------------------------------------- |
| **Email alerts** | *"Add a LoT email route that sends an email when alerts/temperature/+ receives a message"* |
| **Cloud sync** | *"Create an MQTT bridge route that forwards all processed/ topics to a cloud broker at cloud.example.com"* |
| **Industrial protocol** | *"Create a Modbus TCP route to read holding registers 0-10 from a PLC at 192.168.1.50 every 5 seconds"* |
| **Access control** | *"Create a LoT Rule that only allows admin users to publish to config/ topics"* |
| **Data aggregation** | *"Create an Action that calculates a 5-minute rolling average of temperature readings"* |
Each of these is a single prompt that the AI can handle using the MCP documentation. The workflow is always the same: describe → review → deploy → verify.
***
## Next Steps
Haven't connected the MCP yet? Set up your AI assistant's connection to the Coreflux documentation in under 5 minutes.
Level up your AI workflow with naming conventions, code patterns, and a project rules template that keeps your assistant consistent.
# Model Context Protocol (MCP)
Source: https://docs.coreflux.org/ai/mcp
Connect AI assistants to the Coreflux knowledge base using the Model Context Protocol
## The Bridge Between AI and Your Coreflux Environment
Modern AI assistants are powerful — but they don't natively understand LoT (Language of Things) syntax, Coreflux broker configuration, or the nuances of industrial IoT architecture. Without access to the official documentation, an AI assistant will guess, often producing code that looks plausible but uses invented syntax.
The **Model Context Protocol (MCP)** solves this. MCP is an open standard that lets AI assistants — like Claude, GitHub Copilot, or any MCP-compatible client — call external tools during a conversation. The **Coreflux MCP Server** exposes the entire Coreflux documentation as a set of tools your assistant can query in real time. When you ask "how do I create a time-based LoT Action?", your assistant doesn't guess — it looks up the answer in the official docs and responds with verified syntax and working examples.
The result: you stay in your editor, describe what you want in plain English, and get accurate LoT code grounded in real documentation.
**Like giving your AI assistant a library card for Coreflux.** Instead of guessing about LoT syntax or broker configuration, your assistant can look it up directly in the official docs and give you an accurate, sourced answer.
### When to Use This
* You want your AI assistant to **answer Coreflux questions accurately** using official documentation
* You need **LoT syntax help** while coding in an AI-powered editor (Cursor, VS Code with Copilot)
* You want to **search the documentation** through natural language without leaving your workflow
* You're building with Coreflux and want your assistant to have **up-to-date reference material**
***
## How It Works
MCP (Model Context Protocol) is a standard created by Anthropic that defines how AI assistants communicate with external services. It works like a plugin system: an MCP **server** exposes a set of tools, and an MCP **client** (your AI assistant) discovers and calls those tools during conversation.
Here is what happens when you ask your AI assistant a Coreflux question:
| Step | What Happens |
| ------------------------------- | ---------------------------------------------------------------------------------------------------------------- |
| **1. You ask** | You type a question in natural language — *"Create a LoT Action that monitors temperature sensors"* |
| **2. AI recognizes the domain** | Your assistant detects this is a Coreflux question and decides to consult the MCP tools |
| **3. MCP tool call** | The assistant calls the Coreflux MCP Server — searching the documentation or asking the docs assistant |
| **4. Documentation responds** | The MCP server returns relevant documentation snippets, syntax references, and code examples |
| **5. AI synthesizes** | Your assistant combines the documentation with your specific requirements to produce accurate, grounded LoT code |
| **6. You review** | You receive a response with correct syntax, proper patterns, and source references you can verify |
This loop happens automatically once the MCP is connected. You don't need to tell your assistant to "use the MCP" — it discovers the available tools and calls them whenever your question relates to Coreflux.
| Component | Role |
| -------------- | ---------------------------------------------------------------- |
| **MCP Server** | Coreflux's hosted service that exposes documentation tools |
| **MCP Client** | Your AI assistant (Claude, Copilot, etc.) that calls those tools |
| **Transport** | HTTP connection between client and server |
***
## Setup
Connect your AI assistant to the Coreflux documentation MCP server. Choose **Native** to install from the contextual menu on any documentation page, or **Manual** to paste the server URL into your editor's configuration file.
The fastest way to connect is through the **contextual menu** at the top of every documentation page. It copies the hosted MCP server URL or installs the server directly in supported editors — no JSON editing required.
| Menu option | What it does |
| ---------------------------- | ------------------------------------------------------ |
| **Copy MCP Server** | Copies the hosted MCP server URL to your clipboard |
| **Copy MCP install command** | Copies the `npx add-mcp` command to install the server |
| **Connect to Cursor** | Opens Cursor and installs the MCP server |
| **Connect to VS Code** | Opens VS Code and installs the MCP server |
On any Coreflux documentation page, click **Copy page** (top right) to expand the menu.
Select **Connect to VS Code**. VS Code opens the MCP server installation flow for **Coreflux Documentation**.
On the MCP server page, click **Install** (current window) or **Install in Workspace** (project only). Confirm trust if prompted.
Reload the window if needed, then open Copilot Chat in **Agent** mode and enable the Coreflux tools in the tool picker.
On any Coreflux documentation page, click **Copy page** (top right) to expand the menu.
Select **Connect to Cursor**. Cursor opens the MCP server installation flow for **Coreflux Documentation**.
In **Settings → Tools & MCPs**, review the pre-filled **Install MCP Server?** form (name, type, and URL), then click **Install**.
Verify **Coreflux Documentation** appears under **Installed MCP Servers** with tools enabled.
To install manually in Cursor instead, use **Copy MCP Server** or **Copy MCP install command**, then switch to the **Manual** tab and follow the **Cursor** steps.
For Claude Desktop, Claude.ai, or other MCP clients that are not in the menu:
Open the contextual menu and choose **Copy MCP Server**. Paste the URL into your client's MCP configuration.
Choose **Copy MCP install command** and run the copied `npx add-mcp` command in your terminal, then follow the prompts for your editor.
Restart the client and confirm the **Coreflux Documentation** server is connected. See the **Manual** tab for editor-specific configuration examples.
Add the Coreflux MCP server URL to your client's configuration file. Use this method when you need full control over the config or when the contextual menu is unavailable.
**Coreflux MCP Server URL:**
```text wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
https://docs.coreflux.org/mcp
```
The [Coreflux AI Starter](https://github.com/CorefluxCommunity/coreflux-ai-starter) repository has pre-built MCP config files for Cursor, VS Code, and Claude Desktop — copy the file for your editor instead of writing JSON by hand.
Cursor supports MCP servers natively. Add the Coreflux MCP to your global configuration or to a specific project.
Navigate to your Cursor MCP configuration file. You can use the global configuration at `~/.cursor/mcp.json`, or create a project-level `.cursor/mcp.json` in your workspace root.
Add the following entry to the `mcpServers` object:
```json wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
{
"mcpServers": {
"coreflux": {
"url": "https://docs.coreflux.org/mcp"
}
}
}
```
Restart Cursor or reload the window for the MCP server to be detected. You should see the Coreflux tools become available in your AI assistant's tool list.
Open Cursor Settings and navigate to the **MCP** section. You should see `coreflux` listed as a connected server with its tools available.
If you already have other MCP servers configured, simply add the `"coreflux"` entry alongside them inside the existing `mcpServers` object.
Claude Desktop supports MCP servers through its configuration file.
Open the Claude Desktop configuration file at:
```text wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
%APPDATA%\Claude\claude_desktop_config.json
```
```text wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
~/Library/Application Support/Claude/claude_desktop_config.json
```
```text wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
~/.config/Claude/claude_desktop_config.json
```
If the file doesn't exist, create it.
Add or merge the following configuration:
```json wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
{
"mcpServers": {
"coreflux": {
"url": "https://docs.coreflux.org/mcp"
}
}
}
```
Fully quit and reopen Claude Desktop. The Coreflux tools will appear in Claude's tool picker (the hammer icon) when starting a new conversation.
Claude.ai (the web interface) supports remote MCP server connections through its Integrations settings.
Go to [claude.ai](https://claude.ai), click your profile icon, and navigate to **Settings**.
Select the **Integrations** section in the settings panel.
Click **Add Integration** (or **Add MCP Server**), and enter the Coreflux MCP Server URL:
```text wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
https://docs.coreflux.org/mcp
```
Give it a name like **Coreflux** and confirm.
Open a new conversation. The Coreflux tools will be available for Claude to use when relevant to your prompts.
Remote MCP server support on Claude.ai may require a Pro or Team plan. Check your plan's feature availability.
VS Code supports MCP servers through GitHub Copilot's agent mode. Ensure you have the **GitHub Copilot** extension installed and updated.
Create or open the `.vscode/mcp.json` file in your workspace root. Alternatively, you can configure MCP servers globally in your VS Code `settings.json`.
In `.vscode/mcp.json`, add:
```json wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
{
"servers": {
"coreflux": {
"type": "sse",
"url": "https://docs.coreflux.org/mcp"
}
}
}
```
Or in your global `settings.json`:
```json wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
{
"mcp": {
"servers": {
"coreflux": {
"type": "sse",
"url": "https://docs.coreflux.org/mcp"
}
}
}
}
```
Reload the window (`Ctrl+Shift+P` → **Developer: Reload Window**). The Coreflux MCP tools will become available in Copilot's agent mode.
After reloading, confirm trust if prompted and open the "Configure Tools" button in the Chat input to verify the tools are enabled in the tool picker before proceeding.
Open the Copilot Chat panel and switch to **Agent** mode. The Coreflux tools will be available for Copilot to call when relevant.
***
## Verify Your Connection
After setup, confirm the MCP is working by sending a test prompt to your AI assistant. The specific prompt doesn't matter — what matters is that the assistant **calls the Coreflux MCP tools** rather than answering from memory.
### Quick Test
Send this prompt to your AI assistant:
```text wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
Using the Coreflux documentation, show me the correct syntax for
a LoT Action that triggers every 30 seconds and publishes a
heartbeat message.
```
It's working if your assistant calls one of the MCP tools during its response. In Cursor, you'll see the tool calls in the assistant's output. In Claude Desktop, look for the hammer icon indicating tool usage.
**It's not working** if the assistant answers from general knowledge without calling any Coreflux tools. Double-check your MCP configuration and restart your client.
### What a Connected Response Looks Like
When the MCP is active, your assistant's response will:
* Reference **specific LoT syntax** from the documentation (not invented patterns)
* Include **working code examples** that match the official docs
* Cite **source pages** you can open to verify the information
* Use **correct terminology** — LoT, Actions, Models, Rules, Routes — exactly as defined in the documentation
Without the MCP, the assistant might still produce LoT-like code, but it will be based on general training data rather than the official Coreflux documentation. Always verify the connection before starting a development session.
***
## Best Practices
The more specific your question, the better the results. Instead of asking "tell me about routes," try "how do I configure a PostgreSQL data storage route with authentication." Specific queries help the tools return more relevant documentation.
When you need to verify information, ask your assistant to include documentation source links. The Coreflux MCP tools can return references to official pages so you can open and verify the answer yourself.
Before deploying LoT code, ask your assistant to validate the syntax against the documentation. For example: *"Check the Coreflux docs — is this the correct syntax for a Modbus TCP route?"* This ensures your code follows the latest documented patterns.
The documentation assistant supports multi-turn conversations. Ask follow-up questions to drill deeper into a topic without repeating context — the assistant remembers what you discussed previously in the same conversation.
The Coreflux MCP Server is currently in **beta**. The server URL may change as the service evolves. Check this page for the latest configuration instructions.
***
## Next Steps
A guide on utilizing AI agents to write, debug, and deploy LoT scripts directly through the MCP interface.
Strategic advice on prompt engineering, security guardrails, and maintaining consistency when using AI to manage industrial assets.
# Broker Logs
Source: https://docs.coreflux.org/hub/system/broker-logs
View broker logs, filter by level and date, search, and export as JSON or LOG
## Why Use Broker Logs?
You need to see what the broker is doing—startup messages, connection events, errors, warnings. The Broker Logs in the Coreflux HUB show broker-wide logs with filtering, search, and export so you can troubleshoot and audit broker activity.
## When to Use It
* **Troubleshooting** — Find errors or warnings related to broker startup or connections
* **Auditing** — Review broker activity over a date range
* **Exporting** — Save logs as JSON or LOG for offline analysis or support
***
## Filter by Level
Use the **All Levels** dropdown to filter logs by severity:
| Level | Description |
| --------------- | ---------------------- |
| **All Levels** | Show all log entries |
| **Information** | Informational messages |
| **Warning** | Warnings |
| **Error** | Errors |
| **Critical** | Critical failures |
## Search for Keyword
Use the **Search logs...** field to search for specific text in log messages. Enter a keyword or phrase to filter the displayed logs.
## Export
Export logs in two formats:
* **JSON** — Structured export for parsing or integration
* **LOG** — Plain text log file
Click **Export** to download the filtered logs.
## Control Refresh Rate
Use the refresh control to set how often logs update:
| Option | Description |
| -------- | ------------------------------ |
| **Sync** | Real-time updates (continuous) |
| **1s** | Refresh every 1 second |
| **5s** | Refresh every 5 seconds |
| **10s** | Refresh every 10 seconds |
## Filter by Date
Use the **From** and **To** date fields to limit logs to a specific time range. Enter dates (and optionally times) to filter the displayed entries.
## Status Indicators
The Broker Logs view shows status such as:
* **Connected** — HUB is connected to the broker
* **Logging Initialized** — Logging is active
## Clear Logs
Use **Clear** to remove displayed logs from the view. This does not affect the broker—it only clears the HUB display.
***
## Next Steps
Learn about the Coreflux MQTT Broker.
Learn the fundamentals of the Language of Things and how it works.
# Command Console
Source: https://docs.coreflux.org/hub/system/command-console
Execute broker commands directly from the Coreflux HUB
## Why Use the Command Console?
You need to run broker commands—add Actions, remove Routes, or perform other operations—without connecting an external MQTT client. The Command Console lets you execute broker commands directly from the Coreflux HUB.
## When to Use It
* **Adding entities** — Send `-addAction`, `-addRoute`, or similar commands
* **Removing entities** — Send `-removeAction`, `-removeRoute`, etc.
* **Other operations** — Any command supported by the broker's command topic
***
## How It Works
The Command Console sends your input to `$SYS/Coreflux/Command`. The broker replies with a JSON envelope on `$SYS/Coreflux/Command/Output`, which the HUB displays.
You do not need to:
* Subscribe to the command topic yourself
* Use an external MQTT client (for example, MQTT Explorer)
* Publish from the command line
Use `-requestId` when you send several commands in a row and need to tell the replies apart. The full command list is in [Broker Commands](/latest/mqtt-broker/commands).
## Entering Commands
1. In the HUB top navigation, select **System**, then **Command Console**.
2. Enter the command in the input field (e.g., `-addAction DEFINE ACTION ...`).
3. Send the command. The broker processes it and returns a result.
***
## Next Steps
Learn available broker commands and syntax.
View command execution logs.
# System Configuration
Source: https://docs.coreflux.org/hub/system/configuration
Edit broker settings, connection parameters, and configuration options in the Coreflux HUB
## Why Use System Configuration?
You need to change broker settings—ports, TLS, connection limits, or other parameters—without editing configuration files. The System Configuration in the Coreflux HUB lets you view and modify broker settings from the browser.
## When to Use It
* **Adjusting ports** — Change MQTT, TLS, or WebSocket ports
* **TLS configuration** — Enable or configure TLS for secure connections
* **Binding settings** — Control which IP addresses the broker listens on
* **Other broker options** — Any configurable broker parameter exposed in the UI
Changes made here are sent to `$SYS/Coreflux/Config/New`. The broker processes these changes, and some may require a manual restart or sysadmin intervention to apply. The displayed configuration reflects `$SYS/Coreflux/Config`.
***
## Accessing System Configuration
In the HUB top navigation, select **System**, then **System Configuration**. The configuration panel shows available settings grouped by category.
## Configuration Panel
The **System Config** panel has three tabs — **System**, **ENV & Secrets**, and **License**. The **System** tab displays broker settings as editable form fields, organized into groups:
| Group | Settings |
| ----------- | ------------------------------------------------------------------------ |
| **General** | System Name |
| **Network** | MQTT Port, TLS Port, WebSocket Port, WebSocket TLS Port |
| **Binding** | MQTT Bind IP, MQTT TLS Bind IP, WebSocket Bind IP, WebSocket TLS Bind IP |
Edit values as needed and click the save button to apply changes. Use the import and export icons in the top-right of the General group to upload or download the configuration.
***
## ENV Variables & Secrets
The **ENV & Secrets** tab is where you keep reusable values out of your Actions and Routes. Instead of typing the same database address or API key in every place that needs it, you save it once here and reference it by name. When the value changes, you update it in one spot.
**Like a notebook and a vault.** ENV Variables are the notebook -- quick to read and easy to update for things like a server address. Secrets are the vault -- locked away and only used by the broker, perfect for passwords and API keys.
For advanced topics like encryption keys, Docker and Kubernetes deployment, CLI commands, and the full LoT (Language of Things) syntax, see [Environment Variables & Secrets](/latest/mqtt-broker/secrets-and-env).
### ENV Variables
ENV Variables hold plain text values that anyone with access to the HUB can read. Use them for things that change between environments but aren't sensitive -- a database hostname, a port number, an API base URL, or a feature flag.
#### Adding a Variable
Click **+ New variable** in the top-right corner of the ENV Variables list.
Give the variable a clear name like `DB_HOST` and enter its value, for example `postgres.example.com`.
Click **Save**. A confirmation toast like *"ENV NEW\_VARIABLE saved"* appears in the top-right and the variable shows up in the list.
#### Managing Existing Variables
Hover over any row in the list to reveal three quick actions on the right side:
| Action | What it does |
| -------------- | -------------------------------------------------------------------------------- |
| **Copy value** | Copies the current value to your clipboard so you can paste it elsewhere. |
| **Edit** | Reopens the form so you can change the value. The name stays the same. |
| **Delete** | Permanently removes the variable. Anything that uses it stops finding the value. |
The hint at the top of the list -- *In LoT: GET ENV "NAME" -- Drag onto a Storage card field* -- shows you how to use the variable later when you build Actions or Routes.
### Secrets
Secrets hold sensitive values that should never be shown again after you save them. The HUB encrypts them at rest, so even an administrator looking at the list only sees dots where the value should be. Use Secrets for passwords, API keys, tokens, and any other credentials.
#### Adding a Secret
Click **+ New secret** in the top-right corner of the Secrets list.
Give the secret a clear name like `API_KEY` and paste the value into the hidden field. The form notes that the value is *AES-256-GCM encrypted* and *never readable again after saving*.
Click **Save**. A confirmation toast like *"Secret API\_KEY saved"* appears and the secret shows up in the list with its value masked.
#### Managing Existing Secrets
Hover over any row in the list to reveal two quick actions on the right side:
| Action | What it does |
| ------------------- | --------------------------------------------------------------------------------------------------------- |
| **Set a new value** | Opens the form so you can overwrite the secret with a new value. There is no way to view the current one. |
| **Delete** | Permanently removes the secret. Anything that uses it stops finding the value. |
Once a secret is saved or deleted its original value cannot be recovered -- not even by an administrator. If you lose a credential, set a new value or remove it and create it again with the correct value.
***
## License
The **License** tab shows what your current Coreflux license allows, how much of each quota you are using, which premium features are unlocked, and where to upload a new license file when you receive one.
The page is divided into three blocks:
| Block | What you see |
| -------------------- | ------------------------------------------------------------------------------------------------------------------------------------- |
| **Usage** | Progress bars for **Artifacts** (Models, Actions, Scripts, Panels, Rules) and **Routes** with the current count vs the license limit. |
| **Features** | A list of premium features with a check icon next to the ones your license unlocks and a cross next to the ones it does not. |
| **Activate License** | A drop area where you upload a `.license` file you received from your Coreflux operator. |
#### Activating a New License
Request a `.license` file from your Coreflux contact. Save it somewhere you can find it on your computer.
In the **Activate License** section, click the drop area labeled *Click to select license file* and pick the `.license` file.
Click **Activate**. The Usage and Features sections refresh to reflect the new license.
If a feature you need shows as disabled after activation, contact your Coreflux operator -- the license file may not include it, or you may need a different tier.
***
## Next Steps
Create and manage broker users.
Learn configuration options and file-based setup.
# System Overview
Source: https://docs.coreflux.org/hub/system/overview
Broker administration, user management, command console, and broker logs
## Why Use the System Section?
You need to administer your broker—configure settings, manage users, run commands, and view broker logs. The System section of the Coreflux HUB provides tools for broker administration, monitoring, and debugging in one place.
## When to Use Each Tool
| Tool | Use When |
| ------------------------ | ------------------------------------------------------------------------------------ |
| **System Configuration** | Changing broker settings, connection parameters, or configuration options |
| **User management** | Creating, editing, or deleting users; managing credentials and access control |
| **Command Console** | Sending commands directly to `$SYS/Coreflux/Command` without an external MQTT client |
| **Broker Logs** | Viewing broker-wide logs, filtering by level, searching, and exporting |
***
## System Tools
Broker settings and configuration options.
Create, edit, and delete users; manage credentials.
Execute broker commands via the HUB.
View broker logs, filter by level and date, export.
***
## Next Steps
Configure broker settings.
Learn broker configuration options.
# User Management
Source: https://docs.coreflux.org/hub/system/user-management
Create, edit, and delete broker users; manage credentials and access control
## Why Use User Management?
You need to control who can connect to your broker—add users, set passwords, and manage access. The User Management in the Coreflux HUB lets you create, edit, and delete users without editing configuration files or using command-line tools.
## When to Use It
* **Adding users** — Create new users for devices, applications, or operators
* **Updating credentials** — Change passwords
* **Managing permissions** — Control which features each user can access
* **Removing access** — Delete users who no longer need broker access
For topic-level and group-based access control with LoT Rules, see [RBAC with LoT Rules](/latest/lot-language/rules/rbac).
***
## Accessing User Management
In the HUB top navigation, select **System**, then **User Management**. The user accounts table shows all configured users with their permissions.
## User Permissions
Each user has permission flags. The HUB table exposes three toggles that map to broker flags:
| HUB toggle | Broker flag | Description |
| ------------------- | ---------------------------- | ---------------------------------------------------------------------- |
| **Full Access** | `AllowedSystemConfiguration` | System configuration and broad administrative access |
| **User Management** | `AllowedUserManagement` | Create, edit, and delete other users; manage group membership via MQTT |
| **Log Management** | `AllowedLogManagement` | Access to broker logs, trace logs, and log export |
Toggle permissions on or off for each user. The `root` user has special permissions that cannot be fully modified.
### Groups
Users can belong to named groups used by LoT Rules (`USER IN GROUP`) and Visu panel sharing. Manage groups with MQTT commands (`-addUserToGroup`, `-removeUserFromGroup`, `-listUserGroups`) — see [Broker Commands](/latest/mqtt-broker/commands) and [RBAC](/latest/lot-language/rules/rbac).
## Creating a User
1. Click the **+** button in the top-right corner.
2. The **Add New User** modal opens.
3. Enter a username and password (with confirmation).
4. Click **Add User**. The user is created with default permissions and can now connect to the broker.
## Editing a User
Toggle permission switches directly in the user table to modify access. When you change a permission, the user row shows a **Modified** badge and action buttons appear:
| Action | Icon | Description |
| ------------------- | ----------- | ------------------------------------------- |
| **Save** | Floppy disk | Save the modified permissions to the broker |
| **Cancel** | X | Revert unsaved changes |
| **Change Password** | Key | Update the user's password |
| **Delete** | Trash | Remove the user from the broker |
The header bar shows badges for **modified** users and **System Updated** status when the broker configuration changes while you are editing.
## Deleting a User
Click the delete (trash) icon next to a user and confirm the deletion. The `root` user cannot be deleted.
***
## Next Steps
Groups, permission flags, and topic access rules.
Learn about authentication and security options.
# How to Debug in LoT Notebooks
Source: https://docs.coreflux.org/lot-notebooks/debug
Debug LoT and MQTT data with a Data Viewer and an Execution _Trace.
## Overview
Debugging in LoT Notebooks means confirming **data is moving on the right topics** and **your logic behaves as intended**. The extension includes a **Data Viewer** and an **Execution Trace** tied to each **LoT cell** so you can see data, replay flows and inspect every related **topic** and **variable**.
Without the right view, you are mostly in the **dark** — you write code hoping nothing goes wrong. The **Data Viewer** and the **execution trace** light up the room, **showing what that code actually did**.
***
## Data Viewer
Inside **Coreflux Explorer**, the Data Viewer has two parts that work together:
### Topic Tree Viewer
The **topic tree** gives you a **quick glance** at your hierarchy and **live values** as messages arrive—handy for spotting whether a sensor topic or alert is updating, without needing to open a separate tool.
Use **Subscribe to All topics** when you want the tree to show all broker data, and **Subscribe to a topic** when you want to focus on specific information.
### Replay Data Viewer
**Click a topic in the tree** to open the **full Data Viewer** for that stream. There you get:
* **Message history** and a **multi-topic** layout so you can follow several related topics at once
* **Replay** controls to move through captured messages over time
* A **data formatter** so you can interpret payloads the way they were meant to be read—pick the view that matches how the payload is encoded
Use this when you need to inspect structure, compare payloads, or see the data history — not just see that “something changed” on a topic.
#### Adjust how you see the data
Choose a formatter after opening a topic. The full viewer supports **multiple topics**, a **message history**, **replay** (timeline and playback controls), and **data views** such as raw, JSON, Protobuf, geopoint, image, audio and PDF.
***
## Execution trace
The **execution trace** lets you **see every topic and variable** the cell touches—publishes, subscriptions, and related state—in one place, follow a **time-ordered history** of messages across them, and use **replay** with playback controls and a **timeline** to play, pause, and scrub the run. That way you can confirm execution order, timing, and which step produced which message when logic spans several topics.
It is scoped to the **LoT cell** you are debugging, so you stay with that cell’s footprint on the broker instead of hunting traffic across the whole system.
***
## Troubleshooting
When the viewer or trace does not match what you expect, open the sections below.
If cells fail immediately or never show a response, verify credentials and network first.
1. Open the Command Palette (`Ctrl+Shift+P` or `Cmd+Shift+P`) or use the connection control in the activity bar / status area as shown in your setup.
2. Run **LoT Notebook: Change Credentials** and enter your broker URL, username, and password.
3. Watch the status bar for a connected state before running cells.
* Default credentials and TLS notes: [LoT Notebooks setup guide](/latest/quick-start/vscode)
* Confirm the broker is reachable (host, port, firewall, TLS if using `mqtts://`)
When you click **Run** on a LoT cell, the extension sends the definition to the broker and shows the result directly below the cell.
| What you see | What it means |
| ----------------------------- | -------------------------------------------------------------------------------------------------------- |
| Success message / green check | The broker compiled and registered the entity (Action, Model, Route, or Rule). |
| Error text | Often syntax, indentation, or broker-side validation—read the full message for line hints when provided. |
## Related guides
Cell execution, commands, broker topics, and troubleshooting accordions.
What LoT is, how it runs in the broker, and simple examples to build on.
# File Format
Source: https://docs.coreflux.org/lot-notebooks/file-format
Understand the .lotnb file structure, cell types, and how to create notebooks.
## The .lotnb Format
LoT Notebook files use the `.lotnb` extension and follow a JSON-based structure compatible with VS Code's notebook API. Each file contains an array of cells—documentation and code blocks that make up your notebook.
You don't need to edit the JSON directly. VS Code's notebook interface handles the structure automatically. This page is for understanding what's happening under the hood.
***
## File Structure
A `.lotnb` file is a JSON array where each element represents a cell:
```json wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
[
{
"kind": 1,
"language": "markdown",
"value": "# My First Notebook\n\nThis is a markdown cell with documentation."
},
{
"kind": 2,
"language": "lot",
"value": "DEFINE ACTION Heartbeat\nON EVERY 10 SECONDS DO\n PUBLISH TOPIC \"system/heartbeat\" WITH TIMESTAMP \"UTC\""
},
{
"kind": 2,
"language": "python",
"value": "# Script Name: DataProcessor\ndef calculate_average(values):\n return sum(values) / len(values)"
}
]
```
***
## Cell Properties
Each cell has three required properties:
| Property | Type | Description |
| ---------- | ------- | --------------------------------------------------- |
| `kind` | integer | Cell type: `1` = Markdown, `2` = Code |
| `language` | string | Cell language: `"markdown"`, `"lot"`, or `"python"` |
| `value` | string | Cell content (use `\n` for line breaks) |
### Kind Values
| Kind | Type | Purpose |
| ---- | -------- | ------------------------------------ |
| `1` | Markdown | Documentation, explanations, headers |
| `2` | Code | Executable LoT or Python code |
### Language Values
| Language | Cell Kind | Description |
| ---------- | --------- | ------------------------------------------------ |
| `markdown` | 1 | Rich text documentation |
| `lot` | 2 | LoT definitions (Actions, Models, Routes, Rules) |
| `python` | 2 | Python scripts for broker execution |
***
## Cell Types in Detail
### Markdown Cells
Markdown cells support standard Markdown syntax plus some extensions:
```json wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
{
"kind": 1,
"language": "markdown",
"value": "## Temperature Monitoring System\n\nThis notebook implements a complete temperature monitoring solution:\n\n- **Sensors**: Read from `sensors/+/temperature`\n- **Alerts**: Publish to `alerts/high-temp` when threshold exceeded\n- **Logging**: Store all readings in database\n\n### Architecture\n\n| Component | Topic | Purpose |\n|-----------|-------|--------|\n| Sensor Reader | sensors/+/temperature | Ingest data |\n| Alert Generator | alerts/+ | Notify operators |"
}
```
**Supported Markdown features:**
* Headers (`#`, `##`, `###`)
* Bold, italic, code formatting
* Bullet and numbered lists
* Tables
* Code blocks (for examples, not execution)
* Links and images
### LoT Code Cells
LoT cells contain executable Language of Things definitions:
```json wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
{
"kind": 2,
"language": "lot",
"value": "DEFINE ACTION TemperatureMonitor\nON TOPIC \"sensors/+/temperature\" DO\n SET \"sensor_id\" WITH TOPIC POSITION 2\n IF PAYLOAD > 80 THEN\n PUBLISH TOPIC \"alerts/high-temp/\" + {sensor_id} WITH \"Critical: \" + PAYLOAD + \"°C\""
}
```
**Valid LoT definitions:**
| Definition | Syntax |
| ---------- | ----------------------------------------- |
| Action | `DEFINE ACTION ActionName` |
| Model | `DEFINE MODEL ModelName WITH TOPIC "..."` |
| Route | `DEFINE ROUTE RouteName WITH TYPE ...` |
| Rule | `DEFINE RULE RuleName` |
### Python Cells
Python cells contain scripts that run inside the broker when called from LoT:
```json wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
{
"kind": 2,
"language": "python",
"value": "# Script Name: Validators\ndef validate_temperature(value, min_temp, max_temp):\n \"\"\"Check if temperature is within acceptable range\"\"\"\n try:\n temp = float(value)\n return {\n \"valid\": min_temp <= temp <= max_temp,\n \"value\": temp,\n \"range\": f\"{min_temp}-{max_temp}\"\n }\n except (ValueError, TypeError):\n return {\"valid\": False, \"error\": \"Invalid temperature value\"}"
}
```
**Required format**: Python scripts must start with `# Script Name: YourScriptName` as the first line. This header tells the broker how to register and reference the script.
***
## Creating a New Notebook
### Method 1: VS Code Interface
1. Create a new file with `.lotnb` extension
2. VS Code automatically opens it in notebook mode
3. Use the **+ Code** and **+ Markdown** buttons to add cells
4. Select cell language from the dropdown (lot, python, markdown)
### Method 2: Manual Creation
Create a file with minimal JSON structure:
```json wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
[
{
"kind": 1,
"language": "markdown",
"value": "# My Notebook\n\nAdd your documentation here."
},
{
"kind": 2,
"language": "lot",
"value": "DEFINE ACTION MyFirstAction\nON EVERY 10 SECONDS DO\n PUBLISH TOPIC \"test/hello\" WITH \"Hello World\""
}
]
```
Save with `.lotnb` extension and open in VS Code.
***
## Example: Complete Notebook
Here's a full example showing all cell types working together:
```json wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
[
{
"kind": 1,
"language": "markdown",
"value": "# Production Monitoring System\n\nThis notebook implements production line monitoring with:\n- Real-time part counting\n- Quality validation using Python\n- Alert generation for anomalies"
},
{
"kind": 1,
"language": "markdown",
"value": "## Python Validators\n\nFirst, we define our validation functions:"
},
{
"kind": 2,
"language": "python",
"value": "# Script Name: QualityChecker\ndef check_dimensions(length, width, tolerance=0.5):\n \"\"\"Validate part dimensions against specifications\"\"\"\n target_length = 100.0\n target_width = 50.0\n \n length_ok = abs(float(length) - target_length) <= tolerance\n width_ok = abs(float(width) - target_width) <= tolerance\n \n return {\n \"passed\": length_ok and width_ok,\n \"length_deviation\": float(length) - target_length,\n \"width_deviation\": float(width) - target_width\n }"
},
{
"kind": 1,
"language": "markdown",
"value": "## Part Counter Action\n\nCounts produced parts and tracks hourly production:"
},
{
"kind": 2,
"language": "lot",
"value": "DEFINE ACTION PartCounter\nON TOPIC \"production/line1/part-complete\" DO\n SET \"count\" WITH (GET TOPIC \"production/line1/count\" AS INT) + 1\n PUBLISH TOPIC \"production/line1/count\" WITH {count}\n PUBLISH TOPIC \"production/line1/last-update\" WITH TIMESTAMP \"UTC\""
},
{
"kind": 1,
"language": "markdown",
"value": "## Quality Check Action\n\nValidates each part using our Python function:"
},
{
"kind": 2,
"language": "lot",
"value": "DEFINE ACTION QualityValidator\nON TOPIC \"production/line1/measurement\" DO\n SET \"length\" WITH (GET JSON \"length\" IN PAYLOAD AS DOUBLE)\n SET \"width\" WITH (GET JSON \"width\" IN PAYLOAD AS DOUBLE)\n \n CALL PYTHON \"QualityChecker.check_dimensions\"\n WITH ({length}, {width})\n RETURN AS {result}\n \n IF (GET JSON \"passed\" IN {result} AS BOOL) EQUALS TRUE THEN\n PUBLISH TOPIC \"production/line1/quality/passed\" WITH {result}\n ELSE\n PUBLISH TOPIC \"production/line1/quality/failed\" WITH {result}\n PUBLISH TOPIC \"alerts/quality\" WITH \"Part failed inspection\""
}
]
```
***
## Best Practices
Start notebooks with a title and overview. Use headers to organize sections logically.
Keep each Action, Model, Route, or Rule in its own cell. This makes debugging and selective execution easier.
Write a markdown cell explaining what the next code cell does before writing the code. This helps you think through the logic.
Follow a naming convention for your Actions, Models, and Python scripts. Example: `SensorTemperatureMonitor`, `ProductionPartCounter`.
***
## Troubleshooting
* Verify the file has `.lotnb` extension (not `.lotnb.json`)
* Check that the LoT Notebooks extension is installed
* Ensure the file contains valid JSON (use a JSON validator)
Common issues:
* Missing commas between cells
* Unescaped quotes in `value` strings (use `\"`)
* Unescaped newlines (use `\n`)
* Missing closing brackets
Check the `language` property matches the content type. VS Code should auto-detect, but you can manually switch using the language selector in the cell toolbar.
***
## Next Steps
Create your first LoT Action and verify your setup is working.
Explore the complete LoT syntax for Actions, Models, Routes, and Rules.
# Tutorials
Source: https://docs.coreflux.org/lot-notebooks/tutorials
Hands-on LoT Notebook tutorials covering Actions, Models, Routes, and Python integration.
## Learn by Building
The best way to master LoT is through hands-on practice. These tutorials are interactive notebooks you can download and run in VS Code—each one builds your skills progressively.
**Start with Basics, then branch out.** Complete the Basics tutorials first to understand LoT fundamentals, then explore the topics most relevant to your projects.
***
## Learning Path
Follow this recommended progression from beginner to advanced:
```mermaid theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
flowchart LR
subgraph Beginner
A[Timed Actions] --> B[Topic Actions]
end
subgraph Intermediate
B --> C[Conditional Logic]
C --> D[Basic Models]
D --> E[Action Models]
E --> F[Model Inheritance]
end
subgraph Advanced
F --> G[Routes]
G --> H[Python]
H --> I[Architecture]
end
```
***
## Basics
Start here to learn the fundamentals of LoT Actions and event-driven programming.
Create actions that run on schedules—heartbeats, counters, and periodic tasks. Learn `ON EVERY` triggers and state persistence.
React to MQTT messages in real-time. Master `ON TOPIC` triggers, wildcards, and payload processing.
### What You'll Learn
| Tutorial | Key Concepts |
| ----------------- | -------------------------------------------------------------------- |
| **Timed Actions** | `ON EVERY` syntax, time units, `PUBLISH TOPIC`, timestamps, counters |
| **Topic Actions** | `ON TOPIC` triggers, wildcards (`+`, `#`), `PAYLOAD`, topic parsing |
***
## Logic
Build intelligent automation with conditional logic and decision-making.
Implement IF/ELSE statements, comparisons, and boolean logic. Build threshold monitors, state machines, and quality control systems.
### What You'll Learn
| Tutorial | Key Concepts |
| --------------------- | --------------------------------------------------------------------------------------- |
| **Conditional Logic** | `IF/ELSE` statements, comparison operators, `AND/OR/NOT`, text matching with `CONTAINS` |
***
## Models
Structure and transform your data with LoT Models.
Define data schemas with typed fields. Create structured sensor readings, equipment status reports, and alarm messages.
Publish models dynamically from Actions. Combine event-driven logic with structured data output.
Extend base models to create specialized variants. Build hierarchies for sensors, equipment, and production data.
### What You'll Learn
| Tutorial | Key Concepts |
| --------------------- | -------------------------------------------------------------------------------- |
| **Basic Models** | `DEFINE MODEL`, field types (STRING, INT, DOUBLE, BOOL), triggers, topic sources |
| **Action Models** | `PUBLISH MODEL`, dynamic field values, combining Actions with Models |
| **Model Inheritance** | `EXTENDS`, base models, field overrides, model hierarchies |
***
## Routes
Connect your LoT system to external databases and other MQTT brokers.
Connect multiple MQTT brokers. Forward messages, sync data between systems, and build distributed architectures.
Store MQTT data in SQL databases. Configure connections, map topics to tables, and query historical data.
### What You'll Learn
| Tutorial | Key Concepts |
| ------------------- | --------------------------------------------------------------------------- |
| **MQTT Bridge** | `TYPE MQTT_BRIDGE`, source/destination config, mappings, bidirectional sync |
| **Database Routes** | `TYPE DATABASE`, connection strings, table mappings, data persistence |
***
## Python Integration
Extend LoT with Python for complex calculations, data processing, and advanced logic.
Get started with Python in LoT. Learn the script format, parameter passing, and return value handling.
Build sophisticated data pipelines. Use external libraries, handle JSON, and implement validation logic.
### What You'll Learn
| Tutorial | Key Concepts |
| ------------------- | ---------------------------------------------------------------------------- |
| **Simple Python** | `CALL PYTHON` syntax, script format, parameters, return values |
| **Advanced Python** | JSON processing, external libraries, error handling, complex transformations |
***
## Advanced Topics
Master architectural patterns and optimization techniques for production systems.
Understand route architecture patterns. Choose between mappings and events for optimal performance. Design scalable integration systems.
### What You'll Learn
| Tutorial | Key Concepts |
| --------------------- | --------------------------------------------------------------------------- |
| **Mapping vs Events** | Performance trade-offs, fan-out patterns, aggregation, hybrid architectures |
***
## Quick Reference
Need to review a specific concept? Here's where to find it:
| Concept | Tutorial |
| ------------------- | -------------------------------------- |
| Time-based triggers | [Timed Actions](#basics) |
| Message triggers | [Topic Actions](#basics) |
| IF/ELSE logic | [Conditional Logic](#logic) |
| Data schemas | [Basic Models](#models) |
| Dynamic publishing | [Action Models](#models) |
| Schema inheritance | [Model Inheritance](#models) |
| Broker bridging | [MQTT Bridge](#routes) |
| Data storage | [Database Routes](#routes) |
| Python scripting | [Simple Python](#python-integration) |
| Advanced processing | [Advanced Python](#python-integration) |
| Route optimization | [Mapping vs Events](#advanced-topics) |
***
## Download Tutorials
All tutorials are available in the Coreflux Community GitHub repository:
Download complete tutorial notebooks with documentation, examples, and exercises.
### Getting the Tutorials
Clone the repository or download as ZIP from GitHub.
Open any `.lotnb` file—VS Code automatically uses the LoT Notebooks extension.
Configure your broker credentials, then start running cells.
***
## Next Steps
Master cell execution, broker commands, and troubleshooting.
Explore the complete LoT syntax reference and advanced features.
# How to Use a LoT Notebook
Source: https://docs.coreflux.org/lot-notebooks/usage
Learn how to use LoT Notebooks to run, document and debug your LoT code.
## The Notebook Workflow
LoT Notebooks combine **Documentation in Markdown** and **LoT code** in a **single file**, so you can both run your code and keep your documentation together, using the notebook as **living documentation** for your project.
**Picture a smart kitchen:** **Markdown** is the recipe on the page, each **LoT code** cell is how you run each step of the recipe, and the **Data Viewer** shows you the results of each step.
***
## Install and connect
1. Open VS Code.
2. Open the Extensions view in the sidebar (or use the shortcut `Ctrl+Shift+X` on Windows/Linux, `Cmd+Shift+X` on macOS).
3. Search for **LoT Notebooks** by **Coreflux** and install it.
1. Click the **MQTT** connection status in the bottom-left of VS Code, or click **Connect to Broker** in the **Data Viewer** (Coreflux sidebar).
2. Enter your broker **URL**, **username**, and **password**.
Use a full URL such as `mqtt://localhost:1883` or `mqtts://your-host:8883` for TLS.
| Setting | Example (local defaults) |
| -------- | --------------------------------------------- |
| URL | `mqtt://localhost:1883` |
| Username | `root` |
| Password | `coreflux` (change immediately in production) |
When you are connected, the status bar shows a green **MQTT: Connected**. If any issues occur, the status bar will become red.
The extension securely stores your credentials. You only need to configure them once per workspace.
***
## Create a new project
1. In the Activity Bar, open the **Coreflux** view. Under **PROJECT**, use **New File** to create a notebook and save it in your workspace (use **New Folder** first if you want a dedicated project folder).
2. In the notebook, add **Markdown** and **LoT** cells from the toolbar as needed.
***
## Add LoT code to a LoT Notebook
1. Open your LoT Notebook in VS Code.
2. From the notebook toolbar, add a **code** cell.
3. Type or paste your LoT code (`DEFINE ACTION`, `DEFINE MODEL`, and so on). For a first example, use the code below.
```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
DEFINE ACTION Heartbeat
ON EVERY 10 SECONDS DO
PUBLISH TOPIC "system/heartbeat" WITH TIMESTAMP "UTC"
```
***
## What type of cells you can add to LoT Notebooks
LoT Notebooks support three types of cells, each with different behavior:
### Markdown Cells - Documentation
Markdown cells are for **documentation only**—they don't execute. Use them to:
* Explain what your code does and why
* Document configuration requirements
* Add diagrams, tables, and formatted text
* Create section headers and navigation
### LoT Code Cells - Runnable Logic
LoT cells contain executable **LoT (Language of Things)** code. When you run a LoT cell:
1. The extension parses your code
2. Sends it to the connected broker via MQTT
3. The broker compiles and registers the definition
4. You receive confirmation or error feedback
For what LoT is, how it fits the broker, and more examples, see [Introduction to LoT](/latest/lot-language/introduction).
### Python Cells - Advanced Logic
Python cells contain scripts that run inside the broker when called from LoT Actions. The broker's Python runtime executes your functions.
```python wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
# Script Name: DataProcessor
def calculate_average(values):
"""Calculate average from a list of numbers"""
if not values:
return 0
return sum(values) / len(values)
def format_alert(sensor_id, value, threshold):
"""Generate formatted alert message"""
return f"ALERT: Sensor {sensor_id} exceeded {threshold} with value {value}"
```
Python scripts require a specific format with `# Script Name: YourScriptName` as the first line. Without this header, the script won't be recognized.
***
## Running LoT
When you click the **Run** button on a cell, the Extension builds your code, checks for errors and deploys to the broker.
If successful, a simple Green response will appear. If any error is found, the code is not deployed and a error message will appear with the error details.
***
## What else you can do with the LoT extension
Use the **Coreflux** view (Activity Bar) for broker connection, entities, secrets, license, clients, and live MQTT—without a separate client for most tasks.
| Capability | Why it helps |
| ------------------------------------- | ------------------------------------------------------------------------------------------- |
| **See what is running in the Broker** | See what Actions, Models, Routes, and Rules the broker actually has after a deploy. |
| **Remove LoT code** | On Coreflux Explorer, remove one or more LoT entities. |
| **Manage secrets and env vars** | Configure the secrets within the broker, to manage secure credentials |
| **See license usage** | Spot **license limits** when deploy fails—before you debug LoT as a syntax issue. |
| **See connected clients** | See who is connected; useful when a device or app should be online but is not. |
| **MQTT Data Viewer** | Live topics and payloads in the Data Viewer. |
| **Execution Trace** | For a LoT cell, see it's execution history and which topics and variables that run touched. |
For more on the Data Viewer, replay, formatters, and trace, see [How to Debug in LoT Notebooks](./debug).
### Develop LoT Notebooks with AI
Alongside the extension, you can develop with AI using **Copilot**, just like any programming language. For better results, you can add the **Coreflux MCP** server to the agent, so it can read official docs, and add **AGENTS.md** file for LoT coding practices, so it works like a LoT expert.
To for more details on how to use AI with LoT, check this page: [Developing with LoT Using AI](/ai/developing-with-lot).
### Version control with Git
Use **Git** on your **workspace folder** like any project: commit **`.lotnb`** files aswell as any other files, like documentation, Docker files and supporting files.
***
## Best Practices
Use markdown cells liberally to explain your code. Future you (and teammates) will appreciate the context.
Keep each Action, Model, or Route in its own cell. This makes debugging easier and allows selective execution.
Run cells one at a time when developing. This helps isolate issues and understand system behavior.
Name your Actions, Models, and Routes clearly. They appear in the broker's entity lists and should be self-explanatory.
***
## Troubleshooting
* Check that you're connected to the broker (look for connection status in status bar)
* Verify your credentials are correct
* Ensure the broker is running and accessible
If running a cell shows an **error** and the broker rejects the definition because you have reached a **license limit** (for example on actions, routes, or another licensed resource), deployment stops and the message under the cell should describe the limit.
Open the **Coreflux** view in the Activity Bar and check the **Coreflux Explorer** section for your **license** status and usage so you can see what is capped and plan next steps (reduce definitions or adjust your license).
* Confirm the first line is `# Script Name: YourScriptName`
* Check for syntax errors in the Python code
* Ensure function names match what you're calling from LoT
* Check the output for error messages
* Verify no syntax errors in your LoT code
* Use the Coreflux Entities panel to refresh the list
***
## Next Steps
Use cell output, Coreflux Entities, and live MQTT values to fix issues.
Work through hands-on notebooks covering Actions, Models, Routes, and Python.
See what shipped in each VS Code extension release.
# Release Notes
Source: https://docs.coreflux.org/product-updates/release-notes
New features, improvements, and fixes in each release of the Coreflux MQTT broker and HUB.
**Breaking Changes** in this release:
**HUB is not in the broker image** — `coreflux/coreflux-mqtt-broker` is broker-only. The HUB is `coreflux/coreflux-hub`, always in its own container.
**HUB URL is `http://localhost:3000`** — broker ports `8080` (HTTP) and `8443` (HTTPS) are no longer HUB endpoints. Bookmarks and reverse proxies that pointed at those ports will not reach the HUB.
**Action:** use [Docker Compose](/latest/quick-start/installation) (the default `full` profile starts broker + HUB on one network) or run the HUB image beside the broker. Open `http://localhost:3000`. When both services share the Compose network, click **New Connection** and set **Host** to `broker` (**Protocol** `mqtt://`, **Port** `1883`, or **Protocol** `ws://`, **Port** `5000`, **Path** `mqtt`) — not `localhost`. See [Installation](/latest/quick-start/installation) and [Updating Your Coreflux](/latest/updating-coreflux).
### 2.14.3 - Default rules and Visu panel publishing
#### Bug fixes
* **Default rules load with an active project; Visu panels can be published** — When a project is already active at startup or after `-loadProject`, the broker's seeded permission rules now apply correctly so internal clients retain `$SYS` access and panels publish to `$Visu/...` topics again after project activation.
### 2.14.2 - Sparkplug B route, MQTT & LoT performance improvements
Sparkplug B Support, MQTT client lifecycle action triggers, better OT master/client visibility in broker logs, a banded Rules precedence model, project lifecycle improvements, and faster publish, LoT, and route dispatch.
#### Improvements
* **Rules priorities are now banded** — The rule priority number line is split into bands: `0`–`99` is reserved for the broker's immutable system rules, `100`–`999 999` is your band, and the permissive catch-alls `AllowPublishTopic` / `AllowSubscribeTopic` now sit at `1 000 000`. Only the strongest matching band decides an operation, where a DENY beats an ALLOW (deny-wins) and nothing matching means denied (default-deny). Moving the catch-alls to the weakest band **fixes a topic-visibility bypass**: they previously shipped tied with everything else, so a specific access rule could not reliably override the allow-all fallback — which left Visu panel view topics effectively public. `-addRule` now rejects priorities outside `100`–`1 000 000`. **Action:** custom rules written in the old `1`–`99` range are clamped to `100` on load and logged individually. Clamping keeps them in force, but several rules can land on `100` together, where ties resolve deny-wins — re-space them across the user band, putting the more specific rule at the lower number. See [RBAC with LoT Rules](/latest/lot-language/rules/rbac#rule-precedence).
* **`AllowConnect` seed and `FOR Connect` rules** — Fresh brokers now ship a third permissive catch-all, `AllowConnect`, at priority `1 000 000` (the same weakest band as `AllowPublishTopic` / `AllowSubscribeTopic`). Write `FOR Connect` rules in the user band to decide who may connect after authentication; they outrank the seed. Brokers with no Connect rule at all still leave CONNECT ungated. `-restoreRules` puts all three seeds back to their shipped ALLOW definitions. See [RBAC](/latest/lot-language/rules/rbac#connection-admission) and [`-restoreRules`](/latest/mqtt-broker/commands#restoring-seeded-rules).
* **Project manifests, entity ownership, and ad-hoc entities** — Each stored project publishes a retained content manifest on `$SYS/Coreflux/Projects/{name}/manifest` (SHA-256 of exact file bytes, plus entities declared in `.lotnb` cells). `-projectManifest` returns the same JSON on demand. Running entities are attributed on `$SYS/Coreflux/Entities/ownership` and in the `-listEntities` / `-loadProject` `origin` field. **Loading or unloading a project no longer deletes ad-hoc entities** added with `-addAction` and similar commands while no project was active. **Action:** if you relied on Activate to wipe the whole runtime, remove those entities explicitly before loading a project. See [Project Commands](/latest/mqtt-broker/commands#project-commands) and [Project Topics](/latest/mqtt-broker/sys-topics#project-topics).
* **Loaded projects stay active after a restart** — After `-loadProject` (or `--project`), the last loaded project is remembered and re-activated on the next start unless you pass `--project`. `-unloadProject` returns to standalone mode on the next restart. See [Startup Options](/latest/mqtt-broker/startup-options#loading-a-project-at-startup).
* **`ON START` runs on deploy; action publishes are no longer blocked by user ACL** — Deploying an `ON START` action on a live broker executes the body immediately (cold start still runs once after the MQTT broker is ready). Action `PUBLISH` / `KEEP` still put messages on MQTT for any subscriber; they are no longer denied by user publish rules, so bootstrap topics from actions are not silently dropped. Clients still need subscribe permission to receive those messages. See [Initialization Triggers](/latest/lot-language/actions/events#initialization-triggers).
* **OT string sizing — `LENGTH` is the preferred keyword** — Across OT routes that support configurable string sizing, `WITH LENGTH` is now the documented primary keyword; `STRING_SIZE` remains accepted as a parser alias. See the shared [String sizing by Route](/latest/lot-language/routes/industrial/overview#string-sizing-by-route) reference on the industrial routes overview.
* **Modbus Server STRING defaults and cap** — Unsized `STRING` tags no longer reserve a single register (which silently truncated barcodes and caused Illegal Data Address on FC16 writes). The server now defaults to **40 bytes (20 registers)** and warns when size is omitted. Route validation rejects `LENGTH` values above **246 bytes (123 registers)**, the FC16 write cap. See the [Modbus TCP Server Route guide](/latest/lot-language/routes/industrial/modbus-tcp-server).
* **Modbus Server and OPC UA Server activity logging** — Modbus Server routes log accepted and rejected master writes (plus rejected reads), with bank-aware function-code labels and `LENGTH` guidance when an undeclared holding-register range is the cause; connection open/close counts are logged from the existing watchdog (**the master's source IP is not available**). OPC UA Server routes log client session create/close events and accepted external writes on tags and STRUCT fields.
* **MQTT client lifecycle action triggers** — New `ON CONNECT DO`, `ON DISCONNECT DO`, and `ON SUBSCRIBE ["pattern"] DO` triggers run LoT automation when clients connect, disconnect, or subscribe. Action bodies expose session context via `CLIENTID`, `USER`, `ENDPOINT`, `CLEANSESSION`, `DISCONNECTREASON`, `SUBSCRIBETOPIC`, and `SUBSCRIBEQOS`. The broker only evaluates lifecycle hooks when at least one action defines them. See [Action Triggers](/latest/lot-language/actions/events#client-lifecycle-triggers).
* **Improved command response handling** — Broker command responses are now consistently structured as JSON. Each command may include a `requestId` field, allowing you to easily correlate requests and responses.
* **Sparkplug B routes (beta)** — Decode Sparkplug B protobuf into readable JSON, publish Coreflux topics as an edge node, or run Coreflux as the Primary Host Application with namespace state, commands, and automatic rebirth requests. See the [Sparkplug B Route](/latest/lot-language/routes/system/sparkplug-b) and [Sparkplug Host Route](/latest/lot-language/routes/system/sparkplug-host) guides.
#### Performance
* **MQTT between clients** — More messages complete per second, the slowest publishes finish sooner (about 10% lower p99 latency). Messages that update route topics stay in the order they arrived.
* **LoT rules and actions** — Permission checks, topic triggers, timers, and dashboard invokes no longer slow down as more distinct topics exist on the broker. LoT-heavy traffic that was about **10× slower at 30,000 topics** — and about **70× slower at 100,000** — now stays as fast as it was on a nearly empty broker. `USER HAS` and `USER IN GROUP` checks stay fast as the user list grows.
* **Route mappings** — Adding mappings no longer makes every publish more expensive, including on topics that no mapping uses. Exact-topic mappings (the common case) keep up with tens of thousands of messages per second at **sub-millisecond** latency with **no loss** — the same flood previously took around **25 seconds** and dropped some messages. Wildcard mappings (`+` / `#`) now deliver fully as well. When a route cannot keep up, memory stays bounded: the broker waits, then drops the **newest** arriving message rather than older ones already in flight. Set `ADD ROUTE_CONFIG` on a route (`OVERLOAD_POLICY`, `OVERLOAD_TIMEOUT`, `OVERLOAD_CAPACITY`) when that route needs a different wait-or-drop contract. See [Route Overload Configuration](/latest/lot-language/routes/route-config).
#### Bug fixes
* **Rules with `IF` and no `ELSE` now compile** — `IF USER IS "dashboard" THEN DENY` (no `ELSE`) previously failed compilation on first evaluation and was treated as a deny for every matching publish or subscribe. The missing branch is now the opposite of the `THEN` decision. See [Rules Syntax](/latest/lot-language/rules/syntax#combining-conditions).
* **Python scripts no longer lose their code on restart** — Scripts that contained blank lines were truncated down to the `# Script Name:` header after a reboot, so `CALL PYTHON` failed even though the script still appeared in listings. Complete scripts now persist across restarts. See [Python Integration](/latest/lot-language/actions/python-integration#writing-python-scripts).
* **Built-in rules survive project add/load/unload** — Adding or switching a project no longer wipes the broker's default permission rules, so internal clients keep `$SYS` access. See [RBAC](/latest/lot-language/rules/rbac#seed-catch-alls).
### 2.2.0 - Kafka, OPC UA Server & Modbus Server routes
* **Kafka route** — Connect Coreflux directly to Kafka as a producer or consumer, with support for security protocols and configurable batching and compression. See the [Kafka Route guide](/latest/lot-language/routes/streaming/kafka).
* **OPC UA Server route** — Expose Coreflux data to OPC UA clients with a secure-by-default setup, including support for array and struct nodes. See the [OPC UA Server Route guide](/latest/lot-language/routes/industrial/opcua-server).
* **Modbus Server route** — Serve Modbus data over TCP, with experimental support for Serial (RTU) connections. See the [Modbus TCP Server Route guide](/latest/lot-language/routes/industrial/modbus-tcp-server).
* **OT route persistence** — Server routes (OPC UA Server, Modbus Server) persist their state and recover automatically after a restart, so data isn't lost on reconnect. See [OPC UA Server persistence](/latest/lot-language/routes/industrial/opcua-server#data-persistence--recovery) and [Modbus Server persistence](/latest/lot-language/routes/industrial/modbus-tcp-server#data-persistence--recovery).
* **OPC UA client certificate authentication** — Authenticate OPC UA client connections using PKI certificates for more secure deployments.
* **Modbus one-based addressing** — Set `WITH ONE_BASED_ADDRESSES true` on Modbus client and server routes so tag addresses match device manuals that label registers starting at 1. See [Modbus addressing](/latest/lot-language/routes/industrial/modbus-tcp-client#modbus-addressing) (client) and [Address notation](/latest/lot-language/routes/industrial/modbus-tcp-server#address-notation) (server).
* **Publish payload as bytes** — Forward or retain binary MQTT payloads bit-for-bit with `PAYLOAD AS BYTES` on `PUBLISH TOPIC` and `KEEP TOPIC`. See [ACTION Operations](/latest/lot-language/actions/operations#binary-passthrough--payload-as-bytes).
* **EtherNet/IP route** — Joined attribute reads (`ENABLE_JOIN_ATTRIBUTE`), bit extraction (`BIT_ADDRESS`), ARRAY/STRUCT decoding from assembly buffers, bidirectional `WRITABLE` tags, and legacy Hub Asset field mapping for migrations. See the [EtherNet/IP Route guide](/latest/lot-language/routes/industrial/ethernetip).
* **Allen-Bradley route** — Symbolic tag-name access (`Program:MainProgram.Tag` format), `AB`/`ALLENBRADLEY` route aliases, `PATH` vs `SLOT` routing, family-specific STRING/TIME encoding, and cross-link to EtherNet/IP for explicit CIP paths. See the [Allen-Bradley Route guide](/latest/lot-language/routes/industrial/allen-bradley).
* **Bug fixes** — Fixed BACnet alarm retention, EtherNet/IP connection parameters and legacy-style compatibility, a Modbus TCP word-order issue, an OPC UA client connection issue, and a licensing load-order.
### 2.1.0 - BACnet route (building automation) and Protobuf models
* **BACnet route** — Connect Coreflux to building automation systems (HVAC, lighting, metering) and bridge them to MQTT
* Read live values and write setpoints to BACnet controllers
* Get instant updates on change, without constant polling
* Receive alarms and event notifications directly in MQTT
See the [BACnet Route guide](/v2.1/lot-language/routes/industrial/bacnet) to get started.
* **Protobuf LoT models (beta)** — Decode and encode binary protobuf on MQTT from LoT: `WITH FORMAT PROTOBUF` (or `BOTH`), `PROTO_TAG` to match vendor `.proto` field numbers, `ADD MODEL` for nested messages on publish, `GET PROTO` / `GET PROTOBUF` to read top-level fields, and `PUBLISH MODEL` to emit real protobuf bytes (not JSON-in-bytes). `WITH FORMAT BOTH` keeps JSON on the main topic and binary on a `/protobuf` sidecar topic.
See [Protobuf Models](/latest/lot-language/models/protobuf) for more details.
### 2.0.3 - OT tags & route stability
* **OT tag JSON publish** — Message payload now includes the `unit` field
* **Modbus and EthernetIP routes** — Stability improvements
### 2.0.2 - ON CHANGE trigger and bug fixes
* **`ON CHANGE` trigger for LoT actions** — Run actions only when the message payload on a topic is different from the last message on that same topic. Use it for high-frequency telemetry, historians, and forwarding when you do not need to process duplicate values; `ON TOPIC` still runs on every matching publish.
* **Bug fixes** — Stability and correctness improvements across the broker, LoT, and HUB
### 2.0.0 - AI Agent route in the broker
* **AI Agent route** — Create custom agents usable within LoT for AI-driven steps and logic
* **Cloud and local LLMs** — Support for both on AI features
### 1.10.0 - Protobuf & REST API route
Support for environment variables and secrets on LoT
* **Protobuf** support
* **REST API route** with OAuth and JWT support
* **Default project on boot** — Load a default project to a Coreflux broker when the broker starts
**Breaking Changes** in this release:
**HUB is not in the broker image** — `coreflux/coreflux-mqtt-broker` is broker-only. The HUB is `coreflux/coreflux-hub`, always in its own container.
**HUB URL is `http://localhost:3000`** — broker ports `8080` (HTTP) and `8443` (HTTPS) are no longer HUB endpoints. Bookmarks and reverse proxies that pointed at those ports will not reach the HUB.
**Action:** use [Docker Compose](/latest/quick-start/installation) (the default `full` profile starts broker + HUB on one network) or run the HUB image beside the broker. Open `http://localhost:3000`. When both services share the Compose network, click **New Connection** and set **Host** to `broker` (**Protocol** `mqtt://`, **Port** `1883`, or **Protocol** `ws://`, **Port** `5000`, **Path** `mqtt`) — not `localhost`. See [Installation](/latest/quick-start/installation) and [Updating Your Coreflux](/latest/updating-coreflux).
### 2.3.4 - Login, Deploy, and Data Viewer fixes
#### Bug fixes
* **Non-admin users no longer get bounced back to the login screen** — Signing in as a user without full system access could loop back to login instead of opening the HUB in Limited Mode. The HUB now recognizes a denied `$SYS` subscription for what it is and opens Limited Mode as designed.
* **Deploy project shows accurate status** — Deploying a project via [Connections Manager](/hub/connections) no longer surfaces the broker's success response as a false warning, and the project header no longer gets stuck on **Updating** after the deploy completes.
* **LoT Editor keeps dropped files on the project** — Images and other files dropped into a project (from the Finder, Explorer, or a mixed-OS drag) now stay attached instead of being lost.
* **Data Viewer confirms whether a publish actually landed** — Sending a message no longer shows **Published** when the broker silently rejected it for lacking a publish ACL; Data Viewer now waits for the topic echo before confirming.
### 2.3.0 - LoT Editor, Connections Manager, LoT Flow wiring
#### LoT Editor
* **Visual Studio Code-style editor** — An intuitive notebookeditor with a familiar interface inside the HUB.
* **Unified Projects view** — One Projects list covering every project stored on the broker and every draft held in the HUB. The live project is marked, local-only drafts stay distinct, and each row carries its own sync state: clean, unsaved edits, changed on the broker, or diverged.
* **Push only what changed** — Push stores the working copy and deploys just the drift, instead of redeploying a whole project.
* **Broker Runtime inspector** — A table of what is actually running on the broker, with read-only source for each entity, and a jump from any running entity to the notebook that declares it.
* **Documents alongside notebooks** — Open and store Markdown, text, PDF, and images in a project, import files from disk, and reference them in LoT notebook cells.
* **Copy entities between notebooks** — Copy or drag a declared or deployed entity into another notebook.
* **Version history** Git based version control for project files.
* **Activate and Deactivate projects** from the project tree.
#### Connections Manager
* **More than one broker** — The HUB keeps a named list of broker connections with favorites, replacing the single remembered URL. Each is shown as a card on the login screen.
* **Change Broker** — Switch the whole HUB to another saved connection from the account menu, without logging out and reconfiguring.
* **Per-connection TLS** — Store a CA certificate and a client certificate against an individual connection.
* **Credentials kept off the connection record** — Passwords and client keys live in a separate store; the desktop app encrypts it using the operating system's secure storage, and the browser app never writes them to disk.
* **Live reachability on the login list** — Saved connections are probed so you can see which brokers answer before you pick one.
#### LoT Flow
* **Cleaner connections between nodes** — `KEEP`-topic edges and "matches" edges no longer clutter the canvas, and `MODEL` nodes are drawn as terminals, so the remaining edges show real data flow.
* **Topics grouped by role** — Command and telemetry topics are grouped by the role they play, with disambiguated group labels, prefix hubs backed by a topic-set panel, and wildcard subscriptions expanded to the topics they match.
* **Topic pulse toggle** — Turn live topic activity animation on or off.
* **Export the canvas as an image** — Download the current flow as a PNG.
### 2.2.0 - UNS viewer & LoT Flow
* **LoT Flow** — A new interactive view of your LoT data flows, showing entity, topic, and Python nodes alongside live topic values, action traces, and route status.
* **UNS viewer** — Browse your Unified Namespace as a tree, with search, live payload previews, and a graph layout.
* **Data Viewer overhaul** — A refreshed explorer with a detailed topic view, timeline, faster topic indexing, and better subscription management.
* **In-app release notes** — A "What's New" dialog in the HUB so you can see what's changed without leaving the app.
* **Refreshed HUB styling** — Updated visual design and dock layout.
* **AI chat improvements** — A setup wizard, an artifact panel, and a generally expanded chat experience.
* **Expanded dashboard panels** — More display components, gauge options, and model-binding support for building richer dashboards.
* **Bug fixes** — Fixed the LoT compiler and dashboard rendering for `ON KEY PRESS`, `ON THRESHOLD BREACH`, conditional handlers, `WITH THEME`, numeric decimals and suffixes, threshold zones on gauges/charts, and callable actions. Fixed `INVOKE ACTION`, `BIND TO MODEL`, the accordion "Add Section," and tab titles in dashboards. Fixed real-time charts to show both traces with a legend and to correctly distinguish bar vs. line modes. Fixed select/dropdown interactions, topic-to-widget data binding (previously showing a single point instead of the full array), slider and toggle controls publishing incorrect values, icon visibility, multi-panel layouts, and events not being recognized on mount. Fixed the AI agent to properly detect uninstalled routes, and corrected its confirmation prompts. General mobile layout improvements.
### 2.0.0 - UI/UX overhaul, standalone installer, AI Agent
* **UI/UX improvements**
* **Standalone installer** — Windows and Linux
* **AI Agent**
* **Insights mode** — Ask questions about running data, using project information as context
* **Agent mode** — Build LoT, routes, and dashboards with an AI copilot
* **Editor Area Apps** — Data viewer, Logs Viewer, Secrets & Variables management, etc. in the VSCode editor area for better usability.
* **Connection Manager** — Multiple broker connections can be stored for easy switching
* **LoT Entities table view** — View deployed LoT entities running on the broker in a dedicated table view
* **LoT Projects support** — Create, edit, and deploy LoT projects
* **Improved VS Code extension (beta)**
* License usage
* Live MQTT data (topic tree)
* **Data Viewer** — View and replay historical and live data for MQTT topics, with viewers for raw, JSON, PDF, GeoJSON (map), audio, and more
* **Execution trace** — View and replay historical and live data from LoT execution, including success and error states, published topics, and internal variables
* Manage secrets and environment variables
# Release Notes
Source: https://docs.coreflux.org/product-updates/release-notes
New features, improvements, and fixes in each release of the Coreflux MQTT broker and HUB.
**Breaking Changes** in this release:
**HUB is not in the broker image** — `coreflux/coreflux-mqtt-broker` is broker-only. The HUB is `coreflux/coreflux-hub`, always in its own container.
**HUB URL is `http://localhost:3000`** — broker ports `8080` (HTTP) and `8443` (HTTPS) are no longer HUB endpoints. Bookmarks and reverse proxies that pointed at those ports will not reach the HUB.
**Action:** use [Docker Compose](/latest/quick-start/installation) (the default `full` profile starts broker + HUB on one network) or run the HUB image beside the broker. Open `http://localhost:3000`. When both services share the Compose network, click **New Connection** and set **Host** to `broker` (**Protocol** `mqtt://`, **Port** `1883`, or **Protocol** `ws://`, **Port** `5000`, **Path** `mqtt`) — not `localhost`. See [Installation](/latest/quick-start/installation) and [Updating Your Coreflux](/latest/updating-coreflux).
### 2.14.3 - Default rules and Visu panel publishing
#### Bug fixes
* **Default rules load with an active project; Visu panels can be published** — When a project is already active at startup or after `-loadProject`, the broker's seeded permission rules now apply correctly so internal clients retain `$SYS` access and panels publish to `$Visu/...` topics again after project activation.
### 2.14.2 - Sparkplug B route, MQTT & LoT performance improvements
Sparkplug B Support, MQTT client lifecycle action triggers, better OT master/client visibility in broker logs, a banded Rules precedence model, project lifecycle improvements, and faster publish, LoT, and route dispatch.
#### Improvements
* **Rules priorities are now banded** — The rule priority number line is split into bands: `0`–`99` is reserved for the broker's immutable system rules, `100`–`999 999` is your band, and the permissive catch-alls `AllowPublishTopic` / `AllowSubscribeTopic` now sit at `1 000 000`. Only the strongest matching band decides an operation, where a DENY beats an ALLOW (deny-wins) and nothing matching means denied (default-deny). Moving the catch-alls to the weakest band **fixes a topic-visibility bypass**: they previously shipped tied with everything else, so a specific access rule could not reliably override the allow-all fallback — which left Visu panel view topics effectively public. `-addRule` now rejects priorities outside `100`–`1 000 000`. **Action:** custom rules written in the old `1`–`99` range are clamped to `100` on load and logged individually. Clamping keeps them in force, but several rules can land on `100` together, where ties resolve deny-wins — re-space them across the user band, putting the more specific rule at the lower number. See [RBAC with LoT Rules](/latest/lot-language/rules/rbac#rule-precedence).
* **`AllowConnect` seed and `FOR Connect` rules** — Fresh brokers now ship a third permissive catch-all, `AllowConnect`, at priority `1 000 000` (the same weakest band as `AllowPublishTopic` / `AllowSubscribeTopic`). Write `FOR Connect` rules in the user band to decide who may connect after authentication; they outrank the seed. Brokers with no Connect rule at all still leave CONNECT ungated. `-restoreRules` puts all three seeds back to their shipped ALLOW definitions. See [RBAC](/latest/lot-language/rules/rbac#connection-admission) and [`-restoreRules`](/latest/mqtt-broker/commands#restoring-seeded-rules).
* **Project manifests, entity ownership, and ad-hoc entities** — Each stored project publishes a retained content manifest on `$SYS/Coreflux/Projects/{name}/manifest` (SHA-256 of exact file bytes, plus entities declared in `.lotnb` cells). `-projectManifest` returns the same JSON on demand. Running entities are attributed on `$SYS/Coreflux/Entities/ownership` and in the `-listEntities` / `-loadProject` `origin` field. **Loading or unloading a project no longer deletes ad-hoc entities** added with `-addAction` and similar commands while no project was active. **Action:** if you relied on Activate to wipe the whole runtime, remove those entities explicitly before loading a project. See [Project Commands](/latest/mqtt-broker/commands#project-commands) and [Project Topics](/latest/mqtt-broker/sys-topics#project-topics).
* **Loaded projects stay active after a restart** — After `-loadProject` (or `--project`), the last loaded project is remembered and re-activated on the next start unless you pass `--project`. `-unloadProject` returns to standalone mode on the next restart. See [Startup Options](/latest/mqtt-broker/startup-options#loading-a-project-at-startup).
* **`ON START` runs on deploy; action publishes are no longer blocked by user ACL** — Deploying an `ON START` action on a live broker executes the body immediately (cold start still runs once after the MQTT broker is ready). Action `PUBLISH` / `KEEP` still put messages on MQTT for any subscriber; they are no longer denied by user publish rules, so bootstrap topics from actions are not silently dropped. Clients still need subscribe permission to receive those messages. See [Initialization Triggers](/latest/lot-language/actions/events#initialization-triggers).
* **OT string sizing — `LENGTH` is the preferred keyword** — Across OT routes that support configurable string sizing, `WITH LENGTH` is now the documented primary keyword; `STRING_SIZE` remains accepted as a parser alias. See the shared [String sizing by Route](/latest/lot-language/routes/industrial/overview#string-sizing-by-route) reference on the industrial routes overview.
* **Modbus Server STRING defaults and cap** — Unsized `STRING` tags no longer reserve a single register (which silently truncated barcodes and caused Illegal Data Address on FC16 writes). The server now defaults to **40 bytes (20 registers)** and warns when size is omitted. Route validation rejects `LENGTH` values above **246 bytes (123 registers)**, the FC16 write cap. See the [Modbus TCP Server Route guide](/latest/lot-language/routes/industrial/modbus-tcp-server).
* **Modbus Server and OPC UA Server activity logging** — Modbus Server routes log accepted and rejected master writes (plus rejected reads), with bank-aware function-code labels and `LENGTH` guidance when an undeclared holding-register range is the cause; connection open/close counts are logged from the existing watchdog (**the master's source IP is not available**). OPC UA Server routes log client session create/close events and accepted external writes on tags and STRUCT fields.
* **MQTT client lifecycle action triggers** — New `ON CONNECT DO`, `ON DISCONNECT DO`, and `ON SUBSCRIBE ["pattern"] DO` triggers run LoT automation when clients connect, disconnect, or subscribe. Action bodies expose session context via `CLIENTID`, `USER`, `ENDPOINT`, `CLEANSESSION`, `DISCONNECTREASON`, `SUBSCRIBETOPIC`, and `SUBSCRIBEQOS`. The broker only evaluates lifecycle hooks when at least one action defines them. See [Action Triggers](/latest/lot-language/actions/events#client-lifecycle-triggers).
* **Improved command response handling** — Broker command responses are now consistently structured as JSON. Each command may include a `requestId` field, allowing you to easily correlate requests and responses.
* **Sparkplug B routes (beta)** — Decode Sparkplug B protobuf into readable JSON, publish Coreflux topics as an edge node, or run Coreflux as the Primary Host Application with namespace state, commands, and automatic rebirth requests. See the [Sparkplug B Route](/latest/lot-language/routes/system/sparkplug-b) and [Sparkplug Host Route](/latest/lot-language/routes/system/sparkplug-host) guides.
#### Performance
* **MQTT between clients** — More messages complete per second, the slowest publishes finish sooner (about 10% lower p99 latency). Messages that update route topics stay in the order they arrived.
* **LoT rules and actions** — Permission checks, topic triggers, timers, and dashboard invokes no longer slow down as more distinct topics exist on the broker. LoT-heavy traffic that was about **10× slower at 30,000 topics** — and about **70× slower at 100,000** — now stays as fast as it was on a nearly empty broker. `USER HAS` and `USER IN GROUP` checks stay fast as the user list grows.
* **Route mappings** — Adding mappings no longer makes every publish more expensive, including on topics that no mapping uses. Exact-topic mappings (the common case) keep up with tens of thousands of messages per second at **sub-millisecond** latency with **no loss** — the same flood previously took around **25 seconds** and dropped some messages. Wildcard mappings (`+` / `#`) now deliver fully as well. When a route cannot keep up, memory stays bounded: the broker waits, then drops the **newest** arriving message rather than older ones already in flight. Set `ADD ROUTE_CONFIG` on a route (`OVERLOAD_POLICY`, `OVERLOAD_TIMEOUT`, `OVERLOAD_CAPACITY`) when that route needs a different wait-or-drop contract. See [Route Overload Configuration](/latest/lot-language/routes/route-config).
#### Bug fixes
* **Rules with `IF` and no `ELSE` now compile** — `IF USER IS "dashboard" THEN DENY` (no `ELSE`) previously failed compilation on first evaluation and was treated as a deny for every matching publish or subscribe. The missing branch is now the opposite of the `THEN` decision. See [Rules Syntax](/latest/lot-language/rules/syntax#combining-conditions).
* **Python scripts no longer lose their code on restart** — Scripts that contained blank lines were truncated down to the `# Script Name:` header after a reboot, so `CALL PYTHON` failed even though the script still appeared in listings. Complete scripts now persist across restarts. See [Python Integration](/latest/lot-language/actions/python-integration#writing-python-scripts).
* **Built-in rules survive project add/load/unload** — Adding or switching a project no longer wipes the broker's default permission rules, so internal clients keep `$SYS` access. See [RBAC](/latest/lot-language/rules/rbac#seed-catch-alls).
### 2.2.0 - Kafka, OPC UA Server & Modbus Server routes
* **Kafka route** — Connect Coreflux directly to Kafka as a producer or consumer, with support for security protocols and configurable batching and compression. See the [Kafka Route guide](/latest/lot-language/routes/streaming/kafka).
* **OPC UA Server route** — Expose Coreflux data to OPC UA clients with a secure-by-default setup, including support for array and struct nodes. See the [OPC UA Server Route guide](/latest/lot-language/routes/industrial/opcua-server).
* **Modbus Server route** — Serve Modbus data over TCP, with experimental support for Serial (RTU) connections. See the [Modbus TCP Server Route guide](/latest/lot-language/routes/industrial/modbus-tcp-server).
* **OT route persistence** — Server routes (OPC UA Server, Modbus Server) persist their state and recover automatically after a restart, so data isn't lost on reconnect. See [OPC UA Server persistence](/latest/lot-language/routes/industrial/opcua-server#data-persistence--recovery) and [Modbus Server persistence](/latest/lot-language/routes/industrial/modbus-tcp-server#data-persistence--recovery).
* **OPC UA client certificate authentication** — Authenticate OPC UA client connections using PKI certificates for more secure deployments.
* **Modbus one-based addressing** — Set `WITH ONE_BASED_ADDRESSES true` on Modbus client and server routes so tag addresses match device manuals that label registers starting at 1. See [Modbus addressing](/latest/lot-language/routes/industrial/modbus-tcp-client#modbus-addressing) (client) and [Address notation](/latest/lot-language/routes/industrial/modbus-tcp-server#address-notation) (server).
* **Publish payload as bytes** — Forward or retain binary MQTT payloads bit-for-bit with `PAYLOAD AS BYTES` on `PUBLISH TOPIC` and `KEEP TOPIC`. See [ACTION Operations](/latest/lot-language/actions/operations#binary-passthrough--payload-as-bytes).
* **EtherNet/IP route** — Joined attribute reads (`ENABLE_JOIN_ATTRIBUTE`), bit extraction (`BIT_ADDRESS`), ARRAY/STRUCT decoding from assembly buffers, bidirectional `WRITABLE` tags, and legacy Hub Asset field mapping for migrations. See the [EtherNet/IP Route guide](/latest/lot-language/routes/industrial/ethernetip).
* **Allen-Bradley route** — Symbolic tag-name access (`Program:MainProgram.Tag` format), `AB`/`ALLENBRADLEY` route aliases, `PATH` vs `SLOT` routing, family-specific STRING/TIME encoding, and cross-link to EtherNet/IP for explicit CIP paths. See the [Allen-Bradley Route guide](/latest/lot-language/routes/industrial/allen-bradley).
* **Bug fixes** — Fixed BACnet alarm retention, EtherNet/IP connection parameters and legacy-style compatibility, a Modbus TCP word-order issue, an OPC UA client connection issue, and a licensing load-order.
### 2.1.0 - BACnet route (building automation) and Protobuf models
* **BACnet route** — Connect Coreflux to building automation systems (HVAC, lighting, metering) and bridge them to MQTT
* Read live values and write setpoints to BACnet controllers
* Get instant updates on change, without constant polling
* Receive alarms and event notifications directly in MQTT
See the [BACnet Route guide](/v2.1/lot-language/routes/industrial/bacnet) to get started.
* **Protobuf LoT models (beta)** — Decode and encode binary protobuf on MQTT from LoT: `WITH FORMAT PROTOBUF` (or `BOTH`), `PROTO_TAG` to match vendor `.proto` field numbers, `ADD MODEL` for nested messages on publish, `GET PROTO` / `GET PROTOBUF` to read top-level fields, and `PUBLISH MODEL` to emit real protobuf bytes (not JSON-in-bytes). `WITH FORMAT BOTH` keeps JSON on the main topic and binary on a `/protobuf` sidecar topic.
See [Protobuf Models](/latest/lot-language/models/protobuf) for more details.
### 2.0.3 - OT tags & route stability
* **OT tag JSON publish** — Message payload now includes the `unit` field
* **Modbus and EthernetIP routes** — Stability improvements
### 2.0.2 - ON CHANGE trigger and bug fixes
* **`ON CHANGE` trigger for LoT actions** — Run actions only when the message payload on a topic is different from the last message on that same topic. Use it for high-frequency telemetry, historians, and forwarding when you do not need to process duplicate values; `ON TOPIC` still runs on every matching publish.
* **Bug fixes** — Stability and correctness improvements across the broker, LoT, and HUB
### 2.0.0 - AI Agent route in the broker
* **AI Agent route** — Create custom agents usable within LoT for AI-driven steps and logic
* **Cloud and local LLMs** — Support for both on AI features
### 1.10.0 - Protobuf & REST API route
Support for environment variables and secrets on LoT
* **Protobuf** support
* **REST API route** with OAuth and JWT support
* **Default project on boot** — Load a default project to a Coreflux broker when the broker starts
**Breaking Changes** in this release:
**HUB is not in the broker image** — `coreflux/coreflux-mqtt-broker` is broker-only. The HUB is `coreflux/coreflux-hub`, always in its own container.
**HUB URL is `http://localhost:3000`** — broker ports `8080` (HTTP) and `8443` (HTTPS) are no longer HUB endpoints. Bookmarks and reverse proxies that pointed at those ports will not reach the HUB.
**Action:** use [Docker Compose](/latest/quick-start/installation) (the default `full` profile starts broker + HUB on one network) or run the HUB image beside the broker. Open `http://localhost:3000`. When both services share the Compose network, click **New Connection** and set **Host** to `broker` (**Protocol** `mqtt://`, **Port** `1883`, or **Protocol** `ws://`, **Port** `5000`, **Path** `mqtt`) — not `localhost`. See [Installation](/latest/quick-start/installation) and [Updating Your Coreflux](/latest/updating-coreflux).
### 2.3.4 - Login, Deploy, and Data Viewer fixes
#### Bug fixes
* **Non-admin users no longer get bounced back to the login screen** — Signing in as a user without full system access could loop back to login instead of opening the HUB in Limited Mode. The HUB now recognizes a denied `$SYS` subscription for what it is and opens Limited Mode as designed.
* **Deploy project shows accurate status** — Deploying a project via [Connections Manager](/hub/connections) no longer surfaces the broker's success response as a false warning, and the project header no longer gets stuck on **Updating** after the deploy completes.
* **LoT Editor keeps dropped files on the project** — Images and other files dropped into a project (from the Finder, Explorer, or a mixed-OS drag) now stay attached instead of being lost.
* **Data Viewer confirms whether a publish actually landed** — Sending a message no longer shows **Published** when the broker silently rejected it for lacking a publish ACL; Data Viewer now waits for the topic echo before confirming.
### 2.3.0 - LoT Editor, Connections Manager, LoT Flow wiring
#### LoT Editor
* **Visual Studio Code-style editor** — An intuitive notebookeditor with a familiar interface inside the HUB.
* **Unified Projects view** — One Projects list covering every project stored on the broker and every draft held in the HUB. The live project is marked, local-only drafts stay distinct, and each row carries its own sync state: clean, unsaved edits, changed on the broker, or diverged.
* **Push only what changed** — Push stores the working copy and deploys just the drift, instead of redeploying a whole project.
* **Broker Runtime inspector** — A table of what is actually running on the broker, with read-only source for each entity, and a jump from any running entity to the notebook that declares it.
* **Documents alongside notebooks** — Open and store Markdown, text, PDF, and images in a project, import files from disk, and reference them in LoT notebook cells.
* **Copy entities between notebooks** — Copy or drag a declared or deployed entity into another notebook.
* **Version history** Git based version control for project files.
* **Activate and Deactivate projects** from the project tree.
#### Connections Manager
* **More than one broker** — The HUB keeps a named list of broker connections with favorites, replacing the single remembered URL. Each is shown as a card on the login screen.
* **Change Broker** — Switch the whole HUB to another saved connection from the account menu, without logging out and reconfiguring.
* **Per-connection TLS** — Store a CA certificate and a client certificate against an individual connection.
* **Credentials kept off the connection record** — Passwords and client keys live in a separate store; the desktop app encrypts it using the operating system's secure storage, and the browser app never writes them to disk.
* **Live reachability on the login list** — Saved connections are probed so you can see which brokers answer before you pick one.
#### LoT Flow
* **Cleaner connections between nodes** — `KEEP`-topic edges and "matches" edges no longer clutter the canvas, and `MODEL` nodes are drawn as terminals, so the remaining edges show real data flow.
* **Topics grouped by role** — Command and telemetry topics are grouped by the role they play, with disambiguated group labels, prefix hubs backed by a topic-set panel, and wildcard subscriptions expanded to the topics they match.
* **Topic pulse toggle** — Turn live topic activity animation on or off.
* **Export the canvas as an image** — Download the current flow as a PNG.
### 2.2.0 - UNS viewer & LoT Flow
* **LoT Flow** — A new interactive view of your LoT data flows, showing entity, topic, and Python nodes alongside live topic values, action traces, and route status.
* **UNS viewer** — Browse your Unified Namespace as a tree, with search, live payload previews, and a graph layout.
* **Data Viewer overhaul** — A refreshed explorer with a detailed topic view, timeline, faster topic indexing, and better subscription management.
* **In-app release notes** — A "What's New" dialog in the HUB so you can see what's changed without leaving the app.
* **Refreshed HUB styling** — Updated visual design and dock layout.
* **AI chat improvements** — A setup wizard, an artifact panel, and a generally expanded chat experience.
* **Expanded dashboard panels** — More display components, gauge options, and model-binding support for building richer dashboards.
* **Bug fixes** — Fixed the LoT compiler and dashboard rendering for `ON KEY PRESS`, `ON THRESHOLD BREACH`, conditional handlers, `WITH THEME`, numeric decimals and suffixes, threshold zones on gauges/charts, and callable actions. Fixed `INVOKE ACTION`, `BIND TO MODEL`, the accordion "Add Section," and tab titles in dashboards. Fixed real-time charts to show both traces with a legend and to correctly distinguish bar vs. line modes. Fixed select/dropdown interactions, topic-to-widget data binding (previously showing a single point instead of the full array), slider and toggle controls publishing incorrect values, icon visibility, multi-panel layouts, and events not being recognized on mount. Fixed the AI agent to properly detect uninstalled routes, and corrected its confirmation prompts. General mobile layout improvements.
### 2.0.0 - UI/UX overhaul, standalone installer, AI Agent
* **UI/UX improvements**
* **Standalone installer** — Windows and Linux
* **AI Agent**
* **Insights mode** — Ask questions about running data, using project information as context
* **Agent mode** — Build LoT, routes, and dashboards with an AI copilot
* **Editor Area Apps** — Data viewer, Logs Viewer, Secrets & Variables management, etc. in the VSCode editor area for better usability.
* **Connection Manager** — Multiple broker connections can be stored for easy switching
* **LoT Entities table view** — View deployed LoT entities running on the broker in a dedicated table view
* **LoT Projects support** — Create, edit, and deploy LoT projects
* **Improved VS Code extension (beta)**
* License usage
* Live MQTT data (topic tree)
* **Data Viewer** — View and replay historical and live data for MQTT topics, with viewers for raw, JSON, PDF, GeoJSON (map), audio, and more
* **Execution trace** — View and replay historical and live data from LoT execution, including success and error states, published topics, and internal variables
* Manage secrets and environment variables
# Release Notes
Source: https://docs.coreflux.org/product-updates/release-notes
New features, improvements, and fixes in each release of the Coreflux MQTT broker and HUB.
**Breaking Changes** in this release:
**HUB is not in the broker image** — `coreflux/coreflux-mqtt-broker` is broker-only. The HUB is `coreflux/coreflux-hub`, always in its own container.
**HUB URL is `http://localhost:3000`** — broker ports `8080` (HTTP) and `8443` (HTTPS) are no longer HUB endpoints. Bookmarks and reverse proxies that pointed at those ports will not reach the HUB.
**Action:** use [Docker Compose](/latest/quick-start/installation) (the default `full` profile starts broker + HUB on one network) or run the HUB image beside the broker. Open `http://localhost:3000`. When both services share the Compose network, click **New Connection** and set **Host** to `broker` (**Protocol** `mqtt://`, **Port** `1883`, or **Protocol** `ws://`, **Port** `5000`, **Path** `mqtt`) — not `localhost`. See [Installation](/latest/quick-start/installation) and [Updating Your Coreflux](/latest/updating-coreflux).
### 2.14.3 - Default rules and Visu panel publishing
#### Bug fixes
* **Default rules load with an active project; Visu panels can be published** — When a project is already active at startup or after `-loadProject`, the broker's seeded permission rules now apply correctly so internal clients retain `$SYS` access and panels publish to `$Visu/...` topics again after project activation.
### 2.14.2 - Sparkplug B route, MQTT & LoT performance improvements
Sparkplug B Support, MQTT client lifecycle action triggers, better OT master/client visibility in broker logs, a banded Rules precedence model, project lifecycle improvements, and faster publish, LoT, and route dispatch.
#### Improvements
* **Rules priorities are now banded** — The rule priority number line is split into bands: `0`–`99` is reserved for the broker's immutable system rules, `100`–`999 999` is your band, and the permissive catch-alls `AllowPublishTopic` / `AllowSubscribeTopic` now sit at `1 000 000`. Only the strongest matching band decides an operation, where a DENY beats an ALLOW (deny-wins) and nothing matching means denied (default-deny). Moving the catch-alls to the weakest band **fixes a topic-visibility bypass**: they previously shipped tied with everything else, so a specific access rule could not reliably override the allow-all fallback — which left Visu panel view topics effectively public. `-addRule` now rejects priorities outside `100`–`1 000 000`. **Action:** custom rules written in the old `1`–`99` range are clamped to `100` on load and logged individually. Clamping keeps them in force, but several rules can land on `100` together, where ties resolve deny-wins — re-space them across the user band, putting the more specific rule at the lower number. See [RBAC with LoT Rules](/latest/lot-language/rules/rbac#rule-precedence).
* **`AllowConnect` seed and `FOR Connect` rules** — Fresh brokers now ship a third permissive catch-all, `AllowConnect`, at priority `1 000 000` (the same weakest band as `AllowPublishTopic` / `AllowSubscribeTopic`). Write `FOR Connect` rules in the user band to decide who may connect after authentication; they outrank the seed. Brokers with no Connect rule at all still leave CONNECT ungated. `-restoreRules` puts all three seeds back to their shipped ALLOW definitions. See [RBAC](/latest/lot-language/rules/rbac#connection-admission) and [`-restoreRules`](/latest/mqtt-broker/commands#restoring-seeded-rules).
* **Project manifests, entity ownership, and ad-hoc entities** — Each stored project publishes a retained content manifest on `$SYS/Coreflux/Projects/{name}/manifest` (SHA-256 of exact file bytes, plus entities declared in `.lotnb` cells). `-projectManifest` returns the same JSON on demand. Running entities are attributed on `$SYS/Coreflux/Entities/ownership` and in the `-listEntities` / `-loadProject` `origin` field. **Loading or unloading a project no longer deletes ad-hoc entities** added with `-addAction` and similar commands while no project was active. **Action:** if you relied on Activate to wipe the whole runtime, remove those entities explicitly before loading a project. See [Project Commands](/latest/mqtt-broker/commands#project-commands) and [Project Topics](/latest/mqtt-broker/sys-topics#project-topics).
* **Loaded projects stay active after a restart** — After `-loadProject` (or `--project`), the last loaded project is remembered and re-activated on the next start unless you pass `--project`. `-unloadProject` returns to standalone mode on the next restart. See [Startup Options](/latest/mqtt-broker/startup-options#loading-a-project-at-startup).
* **`ON START` runs on deploy; action publishes are no longer blocked by user ACL** — Deploying an `ON START` action on a live broker executes the body immediately (cold start still runs once after the MQTT broker is ready). Action `PUBLISH` / `KEEP` still put messages on MQTT for any subscriber; they are no longer denied by user publish rules, so bootstrap topics from actions are not silently dropped. Clients still need subscribe permission to receive those messages. See [Initialization Triggers](/latest/lot-language/actions/events#initialization-triggers).
* **OT string sizing — `LENGTH` is the preferred keyword** — Across OT routes that support configurable string sizing, `WITH LENGTH` is now the documented primary keyword; `STRING_SIZE` remains accepted as a parser alias. See the shared [String sizing by Route](/latest/lot-language/routes/industrial/overview#string-sizing-by-route) reference on the industrial routes overview.
* **Modbus Server STRING defaults and cap** — Unsized `STRING` tags no longer reserve a single register (which silently truncated barcodes and caused Illegal Data Address on FC16 writes). The server now defaults to **40 bytes (20 registers)** and warns when size is omitted. Route validation rejects `LENGTH` values above **246 bytes (123 registers)**, the FC16 write cap. See the [Modbus TCP Server Route guide](/latest/lot-language/routes/industrial/modbus-tcp-server).
* **Modbus Server and OPC UA Server activity logging** — Modbus Server routes log accepted and rejected master writes (plus rejected reads), with bank-aware function-code labels and `LENGTH` guidance when an undeclared holding-register range is the cause; connection open/close counts are logged from the existing watchdog (**the master's source IP is not available**). OPC UA Server routes log client session create/close events and accepted external writes on tags and STRUCT fields.
* **MQTT client lifecycle action triggers** — New `ON CONNECT DO`, `ON DISCONNECT DO`, and `ON SUBSCRIBE ["pattern"] DO` triggers run LoT automation when clients connect, disconnect, or subscribe. Action bodies expose session context via `CLIENTID`, `USER`, `ENDPOINT`, `CLEANSESSION`, `DISCONNECTREASON`, `SUBSCRIBETOPIC`, and `SUBSCRIBEQOS`. The broker only evaluates lifecycle hooks when at least one action defines them. See [Action Triggers](/latest/lot-language/actions/events#client-lifecycle-triggers).
* **Improved command response handling** — Broker command responses are now consistently structured as JSON. Each command may include a `requestId` field, allowing you to easily correlate requests and responses.
* **Sparkplug B routes (beta)** — Decode Sparkplug B protobuf into readable JSON, publish Coreflux topics as an edge node, or run Coreflux as the Primary Host Application with namespace state, commands, and automatic rebirth requests. See the [Sparkplug B Route](/latest/lot-language/routes/system/sparkplug-b) and [Sparkplug Host Route](/latest/lot-language/routes/system/sparkplug-host) guides.
#### Performance
* **MQTT between clients** — More messages complete per second, the slowest publishes finish sooner (about 10% lower p99 latency). Messages that update route topics stay in the order they arrived.
* **LoT rules and actions** — Permission checks, topic triggers, timers, and dashboard invokes no longer slow down as more distinct topics exist on the broker. LoT-heavy traffic that was about **10× slower at 30,000 topics** — and about **70× slower at 100,000** — now stays as fast as it was on a nearly empty broker. `USER HAS` and `USER IN GROUP` checks stay fast as the user list grows.
* **Route mappings** — Adding mappings no longer makes every publish more expensive, including on topics that no mapping uses. Exact-topic mappings (the common case) keep up with tens of thousands of messages per second at **sub-millisecond** latency with **no loss** — the same flood previously took around **25 seconds** and dropped some messages. Wildcard mappings (`+` / `#`) now deliver fully as well. When a route cannot keep up, memory stays bounded: the broker waits, then drops the **newest** arriving message rather than older ones already in flight. Set `ADD ROUTE_CONFIG` on a route (`OVERLOAD_POLICY`, `OVERLOAD_TIMEOUT`, `OVERLOAD_CAPACITY`) when that route needs a different wait-or-drop contract. See [Route Overload Configuration](/latest/lot-language/routes/route-config).
#### Bug fixes
* **Rules with `IF` and no `ELSE` now compile** — `IF USER IS "dashboard" THEN DENY` (no `ELSE`) previously failed compilation on first evaluation and was treated as a deny for every matching publish or subscribe. The missing branch is now the opposite of the `THEN` decision. See [Rules Syntax](/latest/lot-language/rules/syntax#combining-conditions).
* **Python scripts no longer lose their code on restart** — Scripts that contained blank lines were truncated down to the `# Script Name:` header after a reboot, so `CALL PYTHON` failed even though the script still appeared in listings. Complete scripts now persist across restarts. See [Python Integration](/latest/lot-language/actions/python-integration#writing-python-scripts).
* **Built-in rules survive project add/load/unload** — Adding or switching a project no longer wipes the broker's default permission rules, so internal clients keep `$SYS` access. See [RBAC](/latest/lot-language/rules/rbac#seed-catch-alls).
### 2.2.0 - Kafka, OPC UA Server & Modbus Server routes
* **Kafka route** — Connect Coreflux directly to Kafka as a producer or consumer, with support for security protocols and configurable batching and compression. See the [Kafka Route guide](/latest/lot-language/routes/streaming/kafka).
* **OPC UA Server route** — Expose Coreflux data to OPC UA clients with a secure-by-default setup, including support for array and struct nodes. See the [OPC UA Server Route guide](/latest/lot-language/routes/industrial/opcua-server).
* **Modbus Server route** — Serve Modbus data over TCP, with experimental support for Serial (RTU) connections. See the [Modbus TCP Server Route guide](/latest/lot-language/routes/industrial/modbus-tcp-server).
* **OT route persistence** — Server routes (OPC UA Server, Modbus Server) persist their state and recover automatically after a restart, so data isn't lost on reconnect. See [OPC UA Server persistence](/latest/lot-language/routes/industrial/opcua-server#data-persistence--recovery) and [Modbus Server persistence](/latest/lot-language/routes/industrial/modbus-tcp-server#data-persistence--recovery).
* **OPC UA client certificate authentication** — Authenticate OPC UA client connections using PKI certificates for more secure deployments.
* **Modbus one-based addressing** — Set `WITH ONE_BASED_ADDRESSES true` on Modbus client and server routes so tag addresses match device manuals that label registers starting at 1. See [Modbus addressing](/latest/lot-language/routes/industrial/modbus-tcp-client#modbus-addressing) (client) and [Address notation](/latest/lot-language/routes/industrial/modbus-tcp-server#address-notation) (server).
* **Publish payload as bytes** — Forward or retain binary MQTT payloads bit-for-bit with `PAYLOAD AS BYTES` on `PUBLISH TOPIC` and `KEEP TOPIC`. See [ACTION Operations](/latest/lot-language/actions/operations#binary-passthrough--payload-as-bytes).
* **EtherNet/IP route** — Joined attribute reads (`ENABLE_JOIN_ATTRIBUTE`), bit extraction (`BIT_ADDRESS`), ARRAY/STRUCT decoding from assembly buffers, bidirectional `WRITABLE` tags, and legacy Hub Asset field mapping for migrations. See the [EtherNet/IP Route guide](/latest/lot-language/routes/industrial/ethernetip).
* **Allen-Bradley route** — Symbolic tag-name access (`Program:MainProgram.Tag` format), `AB`/`ALLENBRADLEY` route aliases, `PATH` vs `SLOT` routing, family-specific STRING/TIME encoding, and cross-link to EtherNet/IP for explicit CIP paths. See the [Allen-Bradley Route guide](/latest/lot-language/routes/industrial/allen-bradley).
* **Bug fixes** — Fixed BACnet alarm retention, EtherNet/IP connection parameters and legacy-style compatibility, a Modbus TCP word-order issue, an OPC UA client connection issue, and a licensing load-order.
### 2.1.0 - BACnet route (building automation) and Protobuf models
* **BACnet route** — Connect Coreflux to building automation systems (HVAC, lighting, metering) and bridge them to MQTT
* Read live values and write setpoints to BACnet controllers
* Get instant updates on change, without constant polling
* Receive alarms and event notifications directly in MQTT
See the [BACnet Route guide](/v2.1/lot-language/routes/industrial/bacnet) to get started.
* **Protobuf LoT models (beta)** — Decode and encode binary protobuf on MQTT from LoT: `WITH FORMAT PROTOBUF` (or `BOTH`), `PROTO_TAG` to match vendor `.proto` field numbers, `ADD MODEL` for nested messages on publish, `GET PROTO` / `GET PROTOBUF` to read top-level fields, and `PUBLISH MODEL` to emit real protobuf bytes (not JSON-in-bytes). `WITH FORMAT BOTH` keeps JSON on the main topic and binary on a `/protobuf` sidecar topic.
See [Protobuf Models](/latest/lot-language/models/protobuf) for more details.
### 2.0.3 - OT tags & route stability
* **OT tag JSON publish** — Message payload now includes the `unit` field
* **Modbus and EthernetIP routes** — Stability improvements
### 2.0.2 - ON CHANGE trigger and bug fixes
* **`ON CHANGE` trigger for LoT actions** — Run actions only when the message payload on a topic is different from the last message on that same topic. Use it for high-frequency telemetry, historians, and forwarding when you do not need to process duplicate values; `ON TOPIC` still runs on every matching publish.
* **Bug fixes** — Stability and correctness improvements across the broker, LoT, and HUB
### 2.0.0 - AI Agent route in the broker
* **AI Agent route** — Create custom agents usable within LoT for AI-driven steps and logic
* **Cloud and local LLMs** — Support for both on AI features
### 1.10.0 - Protobuf & REST API route
Support for environment variables and secrets on LoT
* **Protobuf** support
* **REST API route** with OAuth and JWT support
* **Default project on boot** — Load a default project to a Coreflux broker when the broker starts
**Breaking Changes** in this release:
**HUB is not in the broker image** — `coreflux/coreflux-mqtt-broker` is broker-only. The HUB is `coreflux/coreflux-hub`, always in its own container.
**HUB URL is `http://localhost:3000`** — broker ports `8080` (HTTP) and `8443` (HTTPS) are no longer HUB endpoints. Bookmarks and reverse proxies that pointed at those ports will not reach the HUB.
**Action:** use [Docker Compose](/latest/quick-start/installation) (the default `full` profile starts broker + HUB on one network) or run the HUB image beside the broker. Open `http://localhost:3000`. When both services share the Compose network, click **New Connection** and set **Host** to `broker` (**Protocol** `mqtt://`, **Port** `1883`, or **Protocol** `ws://`, **Port** `5000`, **Path** `mqtt`) — not `localhost`. See [Installation](/latest/quick-start/installation) and [Updating Your Coreflux](/latest/updating-coreflux).
### 2.3.4 - Login, Deploy, and Data Viewer fixes
#### Bug fixes
* **Non-admin users no longer get bounced back to the login screen** — Signing in as a user without full system access could loop back to login instead of opening the HUB in Limited Mode. The HUB now recognizes a denied `$SYS` subscription for what it is and opens Limited Mode as designed.
* **Deploy project shows accurate status** — Deploying a project via [Connections Manager](/hub/connections) no longer surfaces the broker's success response as a false warning, and the project header no longer gets stuck on **Updating** after the deploy completes.
* **LoT Editor keeps dropped files on the project** — Images and other files dropped into a project (from the Finder, Explorer, or a mixed-OS drag) now stay attached instead of being lost.
* **Data Viewer confirms whether a publish actually landed** — Sending a message no longer shows **Published** when the broker silently rejected it for lacking a publish ACL; Data Viewer now waits for the topic echo before confirming.
### 2.3.0 - LoT Editor, Connections Manager, LoT Flow wiring
#### LoT Editor
* **Visual Studio Code-style editor** — An intuitive notebookeditor with a familiar interface inside the HUB.
* **Unified Projects view** — One Projects list covering every project stored on the broker and every draft held in the HUB. The live project is marked, local-only drafts stay distinct, and each row carries its own sync state: clean, unsaved edits, changed on the broker, or diverged.
* **Push only what changed** — Push stores the working copy and deploys just the drift, instead of redeploying a whole project.
* **Broker Runtime inspector** — A table of what is actually running on the broker, with read-only source for each entity, and a jump from any running entity to the notebook that declares it.
* **Documents alongside notebooks** — Open and store Markdown, text, PDF, and images in a project, import files from disk, and reference them in LoT notebook cells.
* **Copy entities between notebooks** — Copy or drag a declared or deployed entity into another notebook.
* **Version history** Git based version control for project files.
* **Activate and Deactivate projects** from the project tree.
#### Connections Manager
* **More than one broker** — The HUB keeps a named list of broker connections with favorites, replacing the single remembered URL. Each is shown as a card on the login screen.
* **Change Broker** — Switch the whole HUB to another saved connection from the account menu, without logging out and reconfiguring.
* **Per-connection TLS** — Store a CA certificate and a client certificate against an individual connection.
* **Credentials kept off the connection record** — Passwords and client keys live in a separate store; the desktop app encrypts it using the operating system's secure storage, and the browser app never writes them to disk.
* **Live reachability on the login list** — Saved connections are probed so you can see which brokers answer before you pick one.
#### LoT Flow
* **Cleaner connections between nodes** — `KEEP`-topic edges and "matches" edges no longer clutter the canvas, and `MODEL` nodes are drawn as terminals, so the remaining edges show real data flow.
* **Topics grouped by role** — Command and telemetry topics are grouped by the role they play, with disambiguated group labels, prefix hubs backed by a topic-set panel, and wildcard subscriptions expanded to the topics they match.
* **Topic pulse toggle** — Turn live topic activity animation on or off.
* **Export the canvas as an image** — Download the current flow as a PNG.
### 2.2.0 - UNS viewer & LoT Flow
* **LoT Flow** — A new interactive view of your LoT data flows, showing entity, topic, and Python nodes alongside live topic values, action traces, and route status.
* **UNS viewer** — Browse your Unified Namespace as a tree, with search, live payload previews, and a graph layout.
* **Data Viewer overhaul** — A refreshed explorer with a detailed topic view, timeline, faster topic indexing, and better subscription management.
* **In-app release notes** — A "What's New" dialog in the HUB so you can see what's changed without leaving the app.
* **Refreshed HUB styling** — Updated visual design and dock layout.
* **AI chat improvements** — A setup wizard, an artifact panel, and a generally expanded chat experience.
* **Expanded dashboard panels** — More display components, gauge options, and model-binding support for building richer dashboards.
* **Bug fixes** — Fixed the LoT compiler and dashboard rendering for `ON KEY PRESS`, `ON THRESHOLD BREACH`, conditional handlers, `WITH THEME`, numeric decimals and suffixes, threshold zones on gauges/charts, and callable actions. Fixed `INVOKE ACTION`, `BIND TO MODEL`, the accordion "Add Section," and tab titles in dashboards. Fixed real-time charts to show both traces with a legend and to correctly distinguish bar vs. line modes. Fixed select/dropdown interactions, topic-to-widget data binding (previously showing a single point instead of the full array), slider and toggle controls publishing incorrect values, icon visibility, multi-panel layouts, and events not being recognized on mount. Fixed the AI agent to properly detect uninstalled routes, and corrected its confirmation prompts. General mobile layout improvements.
### 2.0.0 - UI/UX overhaul, standalone installer, AI Agent
* **UI/UX improvements**
* **Standalone installer** — Windows and Linux
* **AI Agent**
* **Insights mode** — Ask questions about running data, using project information as context
* **Agent mode** — Build LoT, routes, and dashboards with an AI copilot
* **Editor Area Apps** — Data viewer, Logs Viewer, Secrets & Variables management, etc. in the VSCode editor area for better usability.
* **Connection Manager** — Multiple broker connections can be stored for easy switching
* **LoT Entities table view** — View deployed LoT entities running on the broker in a dedicated table view
* **LoT Projects support** — Create, edit, and deploy LoT projects
* **Improved VS Code extension (beta)**
* License usage
* Live MQTT data (topic tree)
* **Data Viewer** — View and replay historical and live data for MQTT topics, with viewers for raw, JSON, PDF, GeoJSON (map), audio, and more
* **Execution trace** — View and replay historical and live data from LoT execution, including success and error states, published topics, and internal variables
* Manage secrets and environment variables
# Release Notes
Source: https://docs.coreflux.org/product-updates/release-notes
New features, improvements, and fixes in each release of the Coreflux MQTT broker and HUB.
**Breaking Changes** in this release:
**HUB is not in the broker image** — `coreflux/coreflux-mqtt-broker` is broker-only. The HUB is `coreflux/coreflux-hub`, always in its own container.
**HUB URL is `http://localhost:3000`** — broker ports `8080` (HTTP) and `8443` (HTTPS) are no longer HUB endpoints. Bookmarks and reverse proxies that pointed at those ports will not reach the HUB.
**Action:** use [Docker Compose](/latest/quick-start/installation) (the default `full` profile starts broker + HUB on one network) or run the HUB image beside the broker. Open `http://localhost:3000`. When both services share the Compose network, click **New Connection** and set **Host** to `broker` (**Protocol** `mqtt://`, **Port** `1883`, or **Protocol** `ws://`, **Port** `5000`, **Path** `mqtt`) — not `localhost`. See [Installation](/latest/quick-start/installation) and [Updating Your Coreflux](/latest/updating-coreflux).
### 2.14.3 - Default rules and Visu panel publishing
#### Bug fixes
* **Default rules load with an active project; Visu panels can be published** — When a project is already active at startup or after `-loadProject`, the broker's seeded permission rules now apply correctly so internal clients retain `$SYS` access and panels publish to `$Visu/...` topics again after project activation.
### 2.14.2 - Sparkplug B route, MQTT & LoT performance improvements
Sparkplug B Support, MQTT client lifecycle action triggers, better OT master/client visibility in broker logs, a banded Rules precedence model, project lifecycle improvements, and faster publish, LoT, and route dispatch.
#### Improvements
* **Rules priorities are now banded** — The rule priority number line is split into bands: `0`–`99` is reserved for the broker's immutable system rules, `100`–`999 999` is your band, and the permissive catch-alls `AllowPublishTopic` / `AllowSubscribeTopic` now sit at `1 000 000`. Only the strongest matching band decides an operation, where a DENY beats an ALLOW (deny-wins) and nothing matching means denied (default-deny). Moving the catch-alls to the weakest band **fixes a topic-visibility bypass**: they previously shipped tied with everything else, so a specific access rule could not reliably override the allow-all fallback — which left Visu panel view topics effectively public. `-addRule` now rejects priorities outside `100`–`1 000 000`. **Action:** custom rules written in the old `1`–`99` range are clamped to `100` on load and logged individually. Clamping keeps them in force, but several rules can land on `100` together, where ties resolve deny-wins — re-space them across the user band, putting the more specific rule at the lower number. See [RBAC with LoT Rules](/latest/lot-language/rules/rbac#rule-precedence).
* **`AllowConnect` seed and `FOR Connect` rules** — Fresh brokers now ship a third permissive catch-all, `AllowConnect`, at priority `1 000 000` (the same weakest band as `AllowPublishTopic` / `AllowSubscribeTopic`). Write `FOR Connect` rules in the user band to decide who may connect after authentication; they outrank the seed. Brokers with no Connect rule at all still leave CONNECT ungated. `-restoreRules` puts all three seeds back to their shipped ALLOW definitions. See [RBAC](/latest/lot-language/rules/rbac#connection-admission) and [`-restoreRules`](/latest/mqtt-broker/commands#restoring-seeded-rules).
* **Project manifests, entity ownership, and ad-hoc entities** — Each stored project publishes a retained content manifest on `$SYS/Coreflux/Projects/{name}/manifest` (SHA-256 of exact file bytes, plus entities declared in `.lotnb` cells). `-projectManifest` returns the same JSON on demand. Running entities are attributed on `$SYS/Coreflux/Entities/ownership` and in the `-listEntities` / `-loadProject` `origin` field. **Loading or unloading a project no longer deletes ad-hoc entities** added with `-addAction` and similar commands while no project was active. **Action:** if you relied on Activate to wipe the whole runtime, remove those entities explicitly before loading a project. See [Project Commands](/latest/mqtt-broker/commands#project-commands) and [Project Topics](/latest/mqtt-broker/sys-topics#project-topics).
* **Loaded projects stay active after a restart** — After `-loadProject` (or `--project`), the last loaded project is remembered and re-activated on the next start unless you pass `--project`. `-unloadProject` returns to standalone mode on the next restart. See [Startup Options](/latest/mqtt-broker/startup-options#loading-a-project-at-startup).
* **`ON START` runs on deploy; action publishes are no longer blocked by user ACL** — Deploying an `ON START` action on a live broker executes the body immediately (cold start still runs once after the MQTT broker is ready). Action `PUBLISH` / `KEEP` still put messages on MQTT for any subscriber; they are no longer denied by user publish rules, so bootstrap topics from actions are not silently dropped. Clients still need subscribe permission to receive those messages. See [Initialization Triggers](/latest/lot-language/actions/events#initialization-triggers).
* **OT string sizing — `LENGTH` is the preferred keyword** — Across OT routes that support configurable string sizing, `WITH LENGTH` is now the documented primary keyword; `STRING_SIZE` remains accepted as a parser alias. See the shared [String sizing by Route](/latest/lot-language/routes/industrial/overview#string-sizing-by-route) reference on the industrial routes overview.
* **Modbus Server STRING defaults and cap** — Unsized `STRING` tags no longer reserve a single register (which silently truncated barcodes and caused Illegal Data Address on FC16 writes). The server now defaults to **40 bytes (20 registers)** and warns when size is omitted. Route validation rejects `LENGTH` values above **246 bytes (123 registers)**, the FC16 write cap. See the [Modbus TCP Server Route guide](/latest/lot-language/routes/industrial/modbus-tcp-server).
* **Modbus Server and OPC UA Server activity logging** — Modbus Server routes log accepted and rejected master writes (plus rejected reads), with bank-aware function-code labels and `LENGTH` guidance when an undeclared holding-register range is the cause; connection open/close counts are logged from the existing watchdog (**the master's source IP is not available**). OPC UA Server routes log client session create/close events and accepted external writes on tags and STRUCT fields.
* **MQTT client lifecycle action triggers** — New `ON CONNECT DO`, `ON DISCONNECT DO`, and `ON SUBSCRIBE ["pattern"] DO` triggers run LoT automation when clients connect, disconnect, or subscribe. Action bodies expose session context via `CLIENTID`, `USER`, `ENDPOINT`, `CLEANSESSION`, `DISCONNECTREASON`, `SUBSCRIBETOPIC`, and `SUBSCRIBEQOS`. The broker only evaluates lifecycle hooks when at least one action defines them. See [Action Triggers](/latest/lot-language/actions/events#client-lifecycle-triggers).
* **Improved command response handling** — Broker command responses are now consistently structured as JSON. Each command may include a `requestId` field, allowing you to easily correlate requests and responses.
* **Sparkplug B routes (beta)** — Decode Sparkplug B protobuf into readable JSON, publish Coreflux topics as an edge node, or run Coreflux as the Primary Host Application with namespace state, commands, and automatic rebirth requests. See the [Sparkplug B Route](/latest/lot-language/routes/system/sparkplug-b) and [Sparkplug Host Route](/latest/lot-language/routes/system/sparkplug-host) guides.
#### Performance
* **MQTT between clients** — More messages complete per second, the slowest publishes finish sooner (about 10% lower p99 latency). Messages that update route topics stay in the order they arrived.
* **LoT rules and actions** — Permission checks, topic triggers, timers, and dashboard invokes no longer slow down as more distinct topics exist on the broker. LoT-heavy traffic that was about **10× slower at 30,000 topics** — and about **70× slower at 100,000** — now stays as fast as it was on a nearly empty broker. `USER HAS` and `USER IN GROUP` checks stay fast as the user list grows.
* **Route mappings** — Adding mappings no longer makes every publish more expensive, including on topics that no mapping uses. Exact-topic mappings (the common case) keep up with tens of thousands of messages per second at **sub-millisecond** latency with **no loss** — the same flood previously took around **25 seconds** and dropped some messages. Wildcard mappings (`+` / `#`) now deliver fully as well. When a route cannot keep up, memory stays bounded: the broker waits, then drops the **newest** arriving message rather than older ones already in flight. Set `ADD ROUTE_CONFIG` on a route (`OVERLOAD_POLICY`, `OVERLOAD_TIMEOUT`, `OVERLOAD_CAPACITY`) when that route needs a different wait-or-drop contract. See [Route Overload Configuration](/latest/lot-language/routes/route-config).
#### Bug fixes
* **Rules with `IF` and no `ELSE` now compile** — `IF USER IS "dashboard" THEN DENY` (no `ELSE`) previously failed compilation on first evaluation and was treated as a deny for every matching publish or subscribe. The missing branch is now the opposite of the `THEN` decision. See [Rules Syntax](/latest/lot-language/rules/syntax#combining-conditions).
* **Python scripts no longer lose their code on restart** — Scripts that contained blank lines were truncated down to the `# Script Name:` header after a reboot, so `CALL PYTHON` failed even though the script still appeared in listings. Complete scripts now persist across restarts. See [Python Integration](/latest/lot-language/actions/python-integration#writing-python-scripts).
* **Built-in rules survive project add/load/unload** — Adding or switching a project no longer wipes the broker's default permission rules, so internal clients keep `$SYS` access. See [RBAC](/latest/lot-language/rules/rbac#seed-catch-alls).
### 2.2.0 - Kafka, OPC UA Server & Modbus Server routes
* **Kafka route** — Connect Coreflux directly to Kafka as a producer or consumer, with support for security protocols and configurable batching and compression. See the [Kafka Route guide](/latest/lot-language/routes/streaming/kafka).
* **OPC UA Server route** — Expose Coreflux data to OPC UA clients with a secure-by-default setup, including support for array and struct nodes. See the [OPC UA Server Route guide](/latest/lot-language/routes/industrial/opcua-server).
* **Modbus Server route** — Serve Modbus data over TCP, with experimental support for Serial (RTU) connections. See the [Modbus TCP Server Route guide](/latest/lot-language/routes/industrial/modbus-tcp-server).
* **OT route persistence** — Server routes (OPC UA Server, Modbus Server) persist their state and recover automatically after a restart, so data isn't lost on reconnect. See [OPC UA Server persistence](/latest/lot-language/routes/industrial/opcua-server#data-persistence--recovery) and [Modbus Server persistence](/latest/lot-language/routes/industrial/modbus-tcp-server#data-persistence--recovery).
* **OPC UA client certificate authentication** — Authenticate OPC UA client connections using PKI certificates for more secure deployments.
* **Modbus one-based addressing** — Set `WITH ONE_BASED_ADDRESSES true` on Modbus client and server routes so tag addresses match device manuals that label registers starting at 1. See [Modbus addressing](/latest/lot-language/routes/industrial/modbus-tcp-client#modbus-addressing) (client) and [Address notation](/latest/lot-language/routes/industrial/modbus-tcp-server#address-notation) (server).
* **Publish payload as bytes** — Forward or retain binary MQTT payloads bit-for-bit with `PAYLOAD AS BYTES` on `PUBLISH TOPIC` and `KEEP TOPIC`. See [ACTION Operations](/latest/lot-language/actions/operations#binary-passthrough--payload-as-bytes).
* **EtherNet/IP route** — Joined attribute reads (`ENABLE_JOIN_ATTRIBUTE`), bit extraction (`BIT_ADDRESS`), ARRAY/STRUCT decoding from assembly buffers, bidirectional `WRITABLE` tags, and legacy Hub Asset field mapping for migrations. See the [EtherNet/IP Route guide](/latest/lot-language/routes/industrial/ethernetip).
* **Allen-Bradley route** — Symbolic tag-name access (`Program:MainProgram.Tag` format), `AB`/`ALLENBRADLEY` route aliases, `PATH` vs `SLOT` routing, family-specific STRING/TIME encoding, and cross-link to EtherNet/IP for explicit CIP paths. See the [Allen-Bradley Route guide](/latest/lot-language/routes/industrial/allen-bradley).
* **Bug fixes** — Fixed BACnet alarm retention, EtherNet/IP connection parameters and legacy-style compatibility, a Modbus TCP word-order issue, an OPC UA client connection issue, and a licensing load-order.
### 2.1.0 - BACnet route (building automation) and Protobuf models
* **BACnet route** — Connect Coreflux to building automation systems (HVAC, lighting, metering) and bridge them to MQTT
* Read live values and write setpoints to BACnet controllers
* Get instant updates on change, without constant polling
* Receive alarms and event notifications directly in MQTT
See the [BACnet Route guide](/v2.1/lot-language/routes/industrial/bacnet) to get started.
* **Protobuf LoT models (beta)** — Decode and encode binary protobuf on MQTT from LoT: `WITH FORMAT PROTOBUF` (or `BOTH`), `PROTO_TAG` to match vendor `.proto` field numbers, `ADD MODEL` for nested messages on publish, `GET PROTO` / `GET PROTOBUF` to read top-level fields, and `PUBLISH MODEL` to emit real protobuf bytes (not JSON-in-bytes). `WITH FORMAT BOTH` keeps JSON on the main topic and binary on a `/protobuf` sidecar topic.
See [Protobuf Models](/latest/lot-language/models/protobuf) for more details.
### 2.0.3 - OT tags & route stability
* **OT tag JSON publish** — Message payload now includes the `unit` field
* **Modbus and EthernetIP routes** — Stability improvements
### 2.0.2 - ON CHANGE trigger and bug fixes
* **`ON CHANGE` trigger for LoT actions** — Run actions only when the message payload on a topic is different from the last message on that same topic. Use it for high-frequency telemetry, historians, and forwarding when you do not need to process duplicate values; `ON TOPIC` still runs on every matching publish.
* **Bug fixes** — Stability and correctness improvements across the broker, LoT, and HUB
### 2.0.0 - AI Agent route in the broker
* **AI Agent route** — Create custom agents usable within LoT for AI-driven steps and logic
* **Cloud and local LLMs** — Support for both on AI features
### 1.10.0 - Protobuf & REST API route
Support for environment variables and secrets on LoT
* **Protobuf** support
* **REST API route** with OAuth and JWT support
* **Default project on boot** — Load a default project to a Coreflux broker when the broker starts
**Breaking Changes** in this release:
**HUB is not in the broker image** — `coreflux/coreflux-mqtt-broker` is broker-only. The HUB is `coreflux/coreflux-hub`, always in its own container.
**HUB URL is `http://localhost:3000`** — broker ports `8080` (HTTP) and `8443` (HTTPS) are no longer HUB endpoints. Bookmarks and reverse proxies that pointed at those ports will not reach the HUB.
**Action:** use [Docker Compose](/latest/quick-start/installation) (the default `full` profile starts broker + HUB on one network) or run the HUB image beside the broker. Open `http://localhost:3000`. When both services share the Compose network, click **New Connection** and set **Host** to `broker` (**Protocol** `mqtt://`, **Port** `1883`, or **Protocol** `ws://`, **Port** `5000`, **Path** `mqtt`) — not `localhost`. See [Installation](/latest/quick-start/installation) and [Updating Your Coreflux](/latest/updating-coreflux).
### 2.3.4 - Login, Deploy, and Data Viewer fixes
#### Bug fixes
* **Non-admin users no longer get bounced back to the login screen** — Signing in as a user without full system access could loop back to login instead of opening the HUB in Limited Mode. The HUB now recognizes a denied `$SYS` subscription for what it is and opens Limited Mode as designed.
* **Deploy project shows accurate status** — Deploying a project via [Connections Manager](/hub/connections) no longer surfaces the broker's success response as a false warning, and the project header no longer gets stuck on **Updating** after the deploy completes.
* **LoT Editor keeps dropped files on the project** — Images and other files dropped into a project (from the Finder, Explorer, or a mixed-OS drag) now stay attached instead of being lost.
* **Data Viewer confirms whether a publish actually landed** — Sending a message no longer shows **Published** when the broker silently rejected it for lacking a publish ACL; Data Viewer now waits for the topic echo before confirming.
### 2.3.0 - LoT Editor, Connections Manager, LoT Flow wiring
#### LoT Editor
* **Visual Studio Code-style editor** — An intuitive notebookeditor with a familiar interface inside the HUB.
* **Unified Projects view** — One Projects list covering every project stored on the broker and every draft held in the HUB. The live project is marked, local-only drafts stay distinct, and each row carries its own sync state: clean, unsaved edits, changed on the broker, or diverged.
* **Push only what changed** — Push stores the working copy and deploys just the drift, instead of redeploying a whole project.
* **Broker Runtime inspector** — A table of what is actually running on the broker, with read-only source for each entity, and a jump from any running entity to the notebook that declares it.
* **Documents alongside notebooks** — Open and store Markdown, text, PDF, and images in a project, import files from disk, and reference them in LoT notebook cells.
* **Copy entities between notebooks** — Copy or drag a declared or deployed entity into another notebook.
* **Version history** Git based version control for project files.
* **Activate and Deactivate projects** from the project tree.
#### Connections Manager
* **More than one broker** — The HUB keeps a named list of broker connections with favorites, replacing the single remembered URL. Each is shown as a card on the login screen.
* **Change Broker** — Switch the whole HUB to another saved connection from the account menu, without logging out and reconfiguring.
* **Per-connection TLS** — Store a CA certificate and a client certificate against an individual connection.
* **Credentials kept off the connection record** — Passwords and client keys live in a separate store; the desktop app encrypts it using the operating system's secure storage, and the browser app never writes them to disk.
* **Live reachability on the login list** — Saved connections are probed so you can see which brokers answer before you pick one.
#### LoT Flow
* **Cleaner connections between nodes** — `KEEP`-topic edges and "matches" edges no longer clutter the canvas, and `MODEL` nodes are drawn as terminals, so the remaining edges show real data flow.
* **Topics grouped by role** — Command and telemetry topics are grouped by the role they play, with disambiguated group labels, prefix hubs backed by a topic-set panel, and wildcard subscriptions expanded to the topics they match.
* **Topic pulse toggle** — Turn live topic activity animation on or off.
* **Export the canvas as an image** — Download the current flow as a PNG.
### 2.2.0 - UNS viewer & LoT Flow
* **LoT Flow** — A new interactive view of your LoT data flows, showing entity, topic, and Python nodes alongside live topic values, action traces, and route status.
* **UNS viewer** — Browse your Unified Namespace as a tree, with search, live payload previews, and a graph layout.
* **Data Viewer overhaul** — A refreshed explorer with a detailed topic view, timeline, faster topic indexing, and better subscription management.
* **In-app release notes** — A "What's New" dialog in the HUB so you can see what's changed without leaving the app.
* **Refreshed HUB styling** — Updated visual design and dock layout.
* **AI chat improvements** — A setup wizard, an artifact panel, and a generally expanded chat experience.
* **Expanded dashboard panels** — More display components, gauge options, and model-binding support for building richer dashboards.
* **Bug fixes** — Fixed the LoT compiler and dashboard rendering for `ON KEY PRESS`, `ON THRESHOLD BREACH`, conditional handlers, `WITH THEME`, numeric decimals and suffixes, threshold zones on gauges/charts, and callable actions. Fixed `INVOKE ACTION`, `BIND TO MODEL`, the accordion "Add Section," and tab titles in dashboards. Fixed real-time charts to show both traces with a legend and to correctly distinguish bar vs. line modes. Fixed select/dropdown interactions, topic-to-widget data binding (previously showing a single point instead of the full array), slider and toggle controls publishing incorrect values, icon visibility, multi-panel layouts, and events not being recognized on mount. Fixed the AI agent to properly detect uninstalled routes, and corrected its confirmation prompts. General mobile layout improvements.
### 2.0.0 - UI/UX overhaul, standalone installer, AI Agent
* **UI/UX improvements**
* **Standalone installer** — Windows and Linux
* **AI Agent**
* **Insights mode** — Ask questions about running data, using project information as context
* **Agent mode** — Build LoT, routes, and dashboards with an AI copilot
* **Editor Area Apps** — Data viewer, Logs Viewer, Secrets & Variables management, etc. in the VSCode editor area for better usability.
* **Connection Manager** — Multiple broker connections can be stored for easy switching
* **LoT Entities table view** — View deployed LoT entities running on the broker in a dedicated table view
* **LoT Projects support** — Create, edit, and deploy LoT projects
* **Improved VS Code extension (beta)**
* License usage
* Live MQTT data (topic tree)
* **Data Viewer** — View and replay historical and live data for MQTT topics, with viewers for raw, JSON, PDF, GeoJSON (map), audio, and more
* **Execution trace** — View and replay historical and live data from LoT execution, including success and error states, published topics, and internal variables
* Manage secrets and environment variables