Everything in LoT (Language of Things) 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.
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.
A typical LoT solution combines all four building blocks:
-- 1. ROUTE: Connect to PostgreSQLDEFINE ROUTE SensorDB TO POSTGRESQL CONNECTION "Host=db.local;Database=iot" TOPIC "sensors/#"-- 2. MODEL: Structure incoming dataDEFINE MODEL SensorReading WITH TOPIC "sensors/formatted" ADD STRING "device_id" WITH TOPIC "sensors/id" ADD DOUBLE "temperature" WITH TOPIC "sensors/raw" AS TRIGGER-- 3. ACTION: Process and alertDEFINE ACTION TemperatureAlertON TOPIC "sensors/formatted" DO SET "temp" WITH (GET JSON "temperature" IN PAYLOAD AS DOUBLE) IF {temp} > 80 THEN PUBLISH TOPIC "alerts/high_temp" WITH PAYLOAD-- 4. RULE: Restrict who can publish alertsDEFINE RULE AlertAccess DENY PUBLISH "alerts/#" FOR ALL ALLOW PUBLISH "alerts/#" FOR "system-*"
Most MQTT payloads in IoT systems are JSON. LoT gives you two built-in ways to handle it — read individual fields from incoming JSON with GET JSON, and write structured JSON output using Models.
Use GET JSON inside an Action to extract typed values from a JSON payload. Always specify the target type with AS to ensure correct handling in comparisons and math.
To publish structured JSON, define a COLLAPSED Model with typed fields and use PUBLISH MODEL ... TO ... WITH from an Action. The broker builds the JSON for you — no manual string formatting needed.
This example reads multiple fields from an incoming JSON payload and publishes them as structured output through a Model:
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 ProcessSensorJSONON 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
Variables use snake_case inside double quotes for SET declarations, and curly braces for references.
DEFINE ACTION ProcessSensorDataON 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}
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 not only proper readability but also scalability of systems.
A well-designed topic tree is the most critical architectural decision in a LoT system. Every entity — Actions, Models, Rules, and Routes — communicates through topics. A clean hierarchy makes the entire system easier to build, debug, and extend.
Use + in Actions and Models for precise instance matching. Use # in Routes and Rules for broad coverage.A wildcard-triggered Action like ON TOPIC "sensors/+/raw" automatically resolves + in GET TOPIC and PUBLISH TOPIC to the same matched value — this is called wildcard context inheritance:
DEFINE ACTION ProcessWithContextON 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}
In this example, if triggered by sensors/temp001/temperature, the GET TOPIC reads from sensors/temp001/humidity — the wildcard resolves to the same instance.
Proven templates and patterns for common LoT logic. Each pattern addresses a specific architectural need — use them as starting points for your own implementations.
Each Action should do one thing well. Break complex logic into callable Actions with INPUT/OUTPUT:
Good: Modular
Bad: Monolithic
Separate reusable logic into callable Actions, then compose them in a main Action:
DEFINE ACTION ConvertCelsiusToFahrenheitINPUT celsius AS DOUBLEDO SET "fahrenheit" WITH ({celsius} * 9 / 5 + 32)RETURN OUTPUT fahrenheitDEFINE ACTION ProcessTemperatureReadingON 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:
DEFINE ACTION DoEverythingON 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}
Use KEEP TOPIC for internal persistent state and PUBLISH TOPIC for external broadcast. Never use PUBLISH for values only your system reads:
DEFINE ACTION PersistentCounterON 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)
Mark the primary data field as AS TRIGGER — never a timestamp or metadata field. The trigger determines when the model publishes, so it should fire when new meaningful data arrives:
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"
Use COLLAPSED models when you need full control over publishing timing and destination — the Action decides when and where to publish:
DEFINE MODEL AlarmRecord COLLAPSED ADD STRING "alarm_id" ADD STRING "equipment_id" ADD STRING "severity" ADD STRING "timestamp"DEFINE ACTION ProcessAlarmON TOPIC "alarms/+/raw" 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"
Rules are evaluated by priority (lower number = higher priority). Structure them as specific deny rules first, then broader allow rules:
DEFINE RULE ProtectSysTopics WITH PRIORITY 10 FOR PublishSys IF USER IS "root" THEN ALLOW ELSE DENYDEFINE RULE AllowDevicePublish WITH PRIORITY 50 FOR Publish TO TOPIC "sensors/#" IF USER HAS AllowedSensorPublish THEN ALLOW ELSE DENY
Practice
Guideline
Priority 1–20
Critical deny rules (system protection)
Priority 21–50
Specific allow rules (per-feature access)
Priority 51–100
General allow rules (broad access)
Condition style
Prefer USER HAS <permission> over USER IS "<name>" for maintainability
Name route mappings descriptively and group related mappings within a single route definition:
DEFINE ROUTE CloudSync 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 USE_TLS true ADD MAPPING sensorData WITH SOURCE_TOPIC "sensors/#" WITH DESTINATION_TOPIC "factory1/sensors/#" WITH DIRECTION "out" ADD MAPPING inboundCommands WITH SOURCE_TOPIC "local/commands/#" WITH DESTINATION_TOPIC "factory1/commands/#" WITH DIRECTION "in"