LoT (Language of Things) combines two programming approaches—imperative and declarative—each optimized for different tasks. Understanding when to use each one makes your code cleaner, more maintainable, and easier to debug.
Like a restaurant kitchen. Imperative commands are the chef shouting “Plate this now!” (immediate actions). Declarative definitions are the recipes and menu (structures that define how things work when triggered).
Blueprints that describe what should exist and when it activates. These define the structure of your automation.
Copy
Ask AI
DEFINE ACTION MonitorTemperatureON TOPIC "sensors/temp" DO PUBLISH TOPIC "processed/temp" WITH PAYLOADDEFINE MODEL SensorReading WITH TOPIC "sensors/formatted" ADD STRING "sensor_id" WITH "TEMP001" ADD DOUBLE "value" WITH TOPIC "sensors/raw" AS TRIGGER
A typical LoT solution combines all four building blocks:
Copy
Ask AI
-- 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-*"
Variables use snake_case inside double quotes for SET declarations, and curly braces for references.
Copy
Ask AI
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:
Copy
Ask AI
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:
Copy
Ask AI
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:
Copy
Ask AI
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:
Copy
Ask AI
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:
Copy
Ask AI
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:
Copy
Ask AI
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:
Copy
Ask AI
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:
Copy
Ask AI
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"