Skip to main content

Documentation Index

Fetch the complete documentation index at: https://docs.coreflux.org/llms.txt

Use this file to discover all available pages before exploring further.

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):
NamespacePurposeExample Topics
sensors/Raw incoming sensor datasensors/temp001/raw, sensors/+/temperature
processed/Transformed or enriched dataprocessed/temp001/fahrenheit, processed/+/status
alerts/Threshold violations and notificationsalerts/critical/pump03, alerts/+/temperature
config/Configuration values read by Actionsconfig/setpoint, config/max_temperature
state/Internal persistent state (used with KEEP)state/counter, state/last_run
cache/Cached values for quick referencecache/temp001/last_reading
system/Heartbeats, health checks, diagnosticssystem/heartbeat, system/status
commands/Inbound instructions from external systemscommands/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/<device_id>/<metric> 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, or structured JSON produced by a Model. These choices are independent: you can mix scalar topics, JSON ingress, and Model output 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 forExample topicPayloadIn Actions
PLC registers, simple sensors, Modbus/OPC-UA mapped tagsacme/dallas/line1/filler01/temperature23.5PAYLOAD AS DOUBLE
Status flags, on/off.../runningtrue or 1PAYLOAD AS BOOL
Commands with a single argumentcommands/device/restartnowPAYLOAD AS STRING
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 forExample topicPayload shape
Devices, gateways, REST/webhook Routessensors/temp001/telemetry{"temperature":23.5,"humidity":65,"unit":"celsius"}
Partner APIs with variable schemasintegrations/partner/eventsNested objects and arrays
In Actions, extract fields with GET JSON. For nested paths, missing keys, and publishing cleaned JSON from an Action, see 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.

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.
ApproachWhen to useHow it publishes
Triggered Model (WITH TOPIC + AS TRIGGER)One device type, automatic publish when a source field updatesBroker builds JSON when the trigger field changes
COLLAPSED Model + PUBLISH MODEL from an ActionYou control timing, destination, or merge JSON + scalar sourcesAction decides when and where to publish
Base + FROM inheritanceFamilies of devices share most fieldsSee Standardizing data across device types below
Triggered example — raw scalar in, structured JSON out:
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):
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).
  • 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.

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.
If your payload is…PreferAvoid
One number per topicScalar + topic semanticsJSON with a single "value" field unless the device forces it
Multiple fields per messageJSON ingress + Action or ModelParsing JSON with string splits in Actions
Same schema for many consumersModel on processed/Copy-pasting JSON templates in five Actions
Variant device typesBase Model + FROMSeparate 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.
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:
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:
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 and Working with JSON.

Next Steps

Working with JSON

GET JSON patterns, nested payloads, and PUBLISH MODEL workflows.

Model Inheritance

Deep dive on FROM, base models, and extending schemas.
Last modified on May 20, 2026