# Welcome to Coreflux Source: https://docs.coreflux.org/latest/index One MQTT broker that gets data from your devices, centralizes it, processes it with LoT (Language of Things), and sends it wherever it needs to go. Explore how the pieces fit together, or jump straight in. # Welcome to Coreflux One MQTT broker that gets data from your devices, centralizes it, processes it with LoT, and sends it wherever it needs to go. [Get started](/latest/quick-start/getting-started) [Install Coreflux](/latest/quick-start/installation) ## Quick Navigation > **[Getting Started](/latest/quick-start/getting-started)** > > Go from zero to your first LoT Action in under 5 minutes. > **[Installation](/latest/quick-start/installation)** > > Install Coreflux with Docker, Docker Compose, or a native binary. > **[Coreflux HUB](/hub/overview)** > > Manage your broker from the browser—explore topics, edit LoT, configure connectors. > **[LoT Language](/latest/lot-language/introduction)** > > Learn the Language of Things - Actions, Models, Rules, Connectors, and Panels. > **[Using AI with Coreflux](/latest/quick-start/using-ai)** > > Connect your AI assistant to the docs and build LoT with it. > **[Get Coreflux](https://portal.coreflux.org/downloads)** > > Download the MQTT broker and Coreflux HUB from the Downloads page. Join the community on [Discord](https://discord.com/invite/A3pPrptNMm) or explore examples on [GitHub](https://github.com/CorefluxCommunity). # 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. > [!TIP] > > **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 | > [!NOTE] > > 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":{"light":"css-variables","dark":"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":{"light":"css-variables","dark":"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":{"light":"css-variables","dark":"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":{"light":"css-variables","dark":"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":{"light":"css-variables","dark":"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":{"light":"css-variables","dark":"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":{"light":"css-variables","dark":"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":{"light":"css-variables","dark":"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":{"light":"css-variables","dark":"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":{"light":"css-variables","dark":"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 **Reusable Logic** Call a shared conversion from a topic-triggered ACTION instead of repeating the formula inline: ```lot wrap theme={"theme":{"light":"css-variables","dark":"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} ``` **On-Demand Scripts** A parameterless callable runs only when a triggered ACTION calls it—not on its own schedule or topic: ```lot wrap theme={"theme":{"light":"css-variables","dark":"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} ``` **Composing Callables** Chain callables in a triggered ACTION so the main body stays focused on orchestration: ```lot wrap theme={"theme":{"light":"css-variables","dark":"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 > **One Callable, One Job** > > 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`. > **Extract Before the Third Copy** > > 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. > **Name for Intent** > > Use PascalCase, function-like names: `ConvertCelsiusToFahrenheit`, `CalculateAverage`, `ValidateReading`. See [Naming conventions](/latest/lot-language/usage-patterns/naming-conventions). > **Keep Triggered ACTIONs Thin** > > 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 > **[Operations Reference](./operations)** > > Review all available LoT operations. > **[ACTION Triggers](./events)** > > 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. > [!TIP] > > **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 | > [!NOTE] > > **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. > [!NOTE] > > **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":{"light":"css-variables","dark":"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` | > [!NOTE] > > 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":{"light":"css-variables","dark":"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":{"light":"css-variables","dark":"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":{"light":"css-variables","dark":"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":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} DEFINE ACTION ON EVERY MONDAY AT "08:00:00" DO -- body ``` ```lot wrap theme={"theme":{"light":"css-variables","dark":"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":{"light":"css-variables","dark":"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":{"light":"css-variables","dark":"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":{"light":"css-variables","dark":"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":{"light":"css-variables","dark":"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":{"light":"css-variables","dark":"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. > **Fast poll + daily summary** > > ```lot wrap theme={"theme":{"light":"css-variables","dark":"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 > ``` > **Weekday operations + weekend maintenance** > > ```lot wrap theme={"theme":{"light":"css-variables","dark":"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" > ``` > **Weekly reset + daily snapshot + live throughput** > > ```lot wrap theme={"theme":{"light":"css-variables","dark":"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" > ``` > **One-shot cutover + ongoing heartbeat** > > ```lot wrap theme={"theme":{"light":"css-variables","dark":"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":{"light":"css-variables","dark":"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 > **System heartbeat** > > ```lot wrap theme={"theme":{"light":"css-variables","dark":"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" > ``` > **Counter with state** > > ```lot wrap theme={"theme":{"light":"css-variables","dark":"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":{"light":"css-variables","dark":"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 connector) 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)). > [!NOTE] > > **Same topic, in order.** On one concrete topic the broker runs the action in publish order and gives that run the exact bytes of the message that fired it (`PAYLOAD AS BYTES`, `GET PROTO … IN PAYLOAD`). A slow action on that topic waits for earlier messages on the same topic instead of overlapping them. Messages on other topics still run at the same time. ### `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":{"light":"css-variables","dark":"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. > [!NOTE] > > **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":{"light":"css-variables","dark":"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":{"light":"css-variables","dark":"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: **Single-Level (+)** The `+` wildcard matches exactly one topic level: ```lot wrap theme={"theme":{"light":"css-variables","dark":"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 ``` > [!TIP] > > **Matches:** `sensors/room1/temperature`, `sensors/room2/temperature` > [!WARNING] > > **Does NOT match:** `sensors/building1/floor2/temperature` (too many levels) **Multi-Level (+/+)** Use multiple single-level wildcards for deeper hierarchies: ```lot wrap theme={"theme":{"light":"css-variables","dark":"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 ``` > [!TIP] > > **Matches:** `factory/line1/machine1/status`, `factory/line2/press3/status` > [!WARNING] > > **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":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} DEFINE ACTION InitUNSRoot ON START DO PUBLISH TOPIC "config/uns_root" WITH "factory/lisbon" RETAIN PUBLISH TOPIC "factory/info/name" WITH "Lisbon Plant" RETAIN PUBLISH TOPIC "factory/info/timezone" WITH "Europe/Lisbon" RETAIN 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 * Optional `RETAIN` on `PUBLISH TOPIC` sets the MQTT retain flag on the published message, so a new subscriber receives the last payload. Optional `QOS0`, `QOS1`, or `QOS2` select MQTT QoS; the default remains QoS 0. Omitting `RETAIN` keeps the historical non-retained behaviour. `RETAIN` and a QoS keyword can appear in either order * `KEEP TOPIC` is different: it stores a value for `GET TOPIC` in the broker cache and does not set the MQTT retain flag. `KEEP` does not accept `RETAIN` (use `PUBLISH TOPIC ... RETAIN`) * There is no `ON STOP` equivalent — `ON START` is the correct place for setup logic > [!NOTE] > > Action `PUBLISH TOPIC` puts the payload on MQTT. It skips the **publish ACL** that applies to connected MQTT users — a restrictive rule on that topic does not stop the action from publishing, so bootstrap publishes 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. `KEEP TOPIC` is different: it stores a value for `GET TOPIC` in the broker cache and does not set the MQTT retain flag. *** ## 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 a UI client with `PUBLISH TOPIC ... RETAIN`, 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 | > [!NOTE] > > 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":{"light":"css-variables","dark":"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 connectors 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":{"light":"css-variables","dark":"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":{"light":"css-variables","dark":"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":{"light":"css-variables","dark":"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. > [!NOTE] > > **Full guide:** [Callable ACTIONs](./callable-actions)—reusable logic, on-demand scripts, and `CALL ACTION` syntax. ```lot wrap theme={"theme":{"light":"css-variables","dark":"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 > **[Callable ACTIONs](./callable-actions)** > > Reusable logic, on-demand scripts, and `CALL ACTION`. > **[Operations](./operations)** > > 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. > [!TIP] > > **Quick Reference:** Use the sidebar to jump to specific operations. *** ## Variable Operations > [!NOTE] > > 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":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} SET "variable_name" WITH ``` Variables exist only during action execution and are referenced using `{variable_name}`. **Static Value** ```lot wrap theme={"theme":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} SET "threshold" WITH 100 SET "status" WITH "active" SET "enabled" WITH TRUE ``` **From Topic** ```lot wrap theme={"theme":{"light":"css-variables","dark":"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 ``` **Calculated** ```lot wrap theme={"theme":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} SET "total" WITH (GET TOPIC "count/a" + GET TOPIC "count/b") SET "average" WITH ({sum} / {count}) ``` > [!NOTE] > > 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":{"light":"css-variables","dark":"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":{"light":"css-variables","dark":"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":{"light":"css-variables","dark":"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":{"light":"css-variables","dark":"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. > [!NOTE] > > 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":{"light":"css-variables","dark":"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":{"light":"css-variables","dark":"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":{"light":"css-variables","dark":"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":{"light":"css-variables","dark":"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":{"light":"css-variables","dark":"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":{"light":"css-variables","dark":"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 top-level message, even on a nested dotted path. Supported `AS` types: `INT`, `DOUBLE`, `FLOAT` (alias `SINGLE`), `STRING`, `BOOL`, `BYTES`. A missing or mismatched field returns `null` (not a type default). That usually means a wire-type mismatch, for example `ADD DOUBLE` on a proto `float`. Declare the parent message (unused arms stay as `ADD OBJECT` so wire numbers do not shift): ```lot wrap theme={"theme":{"light":"css-variables","dark":"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":{"light":"css-variables","dark":"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} ``` A dotted path reads a nested message. Declare that arm with `ADD MODEL`, and keep `USING MODEL` on the top-level message: ```lot wrap theme={"theme":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} DEFINE MODEL ReadSnapshot WITH FORMAT PROTOBUF COLLAPSED ADD STRING "tag_group" PROTO_TAG 1 ADD INT "max_age_ms" PROTO_TAG 2 DEFINE MODEL DeviceCommand WITH FORMAT PROTOBUF COLLAPSED ADD INT "command_type" PROTO_TAG 1 ADD MODEL ReadSnapshot "read_snapshot" PROTO_TAG 6 ``` ```lot wrap focus={4} theme={"theme":{"light":"css-variables","dark":"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") SET "tag_group" WITH (GET PROTO "read_snapshot.tag_group" IN PAYLOAD AS STRING USING MODEL "DeviceCommand") KEEP TOPIC "plant/pending_group" WITH {tag_group} ``` ### GET MODEL `GET MODEL` reads one record from a database connector and returns it shaped as a model: a JSON object with that model's fields. #### Basic Syntax ```lot theme={"theme":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} GET MODEL "" FROM "" [WHERE [AND …]] ``` * The query reads the table, collection, or index with the **same name as the model**, not the `STORE IN` table. * `WHERE` (or `WHEN`) filters on columns. Operators are `EQUALS`, `==`, `!=`, `>`, `<`, `>=`, and `<=`. Join conditions with `AND` in any mix of text and numeric comparisons. Text values are single-quoted, and a literal `'` is written `''` (the name `O'Brien` is `'O''Brien'`). A `{var}` value is resolved when the action runs. `OR` is not supported and is a syntax error. * The first matching record is returned. A model field the record doesn't have gets its type's default (`0`, `0.0`, `false`, or `""`). * With no match, an unknown model, or a query error, the result is an empty object `{}`, and the error goes to the broker log. Supported connectors: PostgreSQL, MySQL, MariaDB, SQL Server, MongoDB, OpenSearch, `FILE_STORAGE`, and `REST_API`. #### Example Usage Read the press row when its id, state, and temperature all match. `{machine}` is filled in when the action runs. ```lot wrap focus={8-11} theme={"theme":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} DEFINE MODEL MachineState ADD STRING "machine_id" ADD STRING "state" ADD DOUBLE "temperature" DEFINE ACTION ReportPress1 ON TOPIC "plant/press1/report/request" DO SET "machine" WITH "press1" SET "press" WITH GET MODEL "MachineState" FROM "PlantDB" WHERE machine_id EQUALS {machine} AND state EQUALS 'running' AND temperature > 80 PUBLISH TOPIC "plant/press1/report" WITH {press} PUBLISH TOPIC "plant/press1/hot" WITH GET MODEL "MachineState" FROM "PlantDB" WHERE temperature > 80 ``` `GET MODEL` runs inside the action and waits for the database. Keep it off high-rate triggers. In a cluster, a connector that runs only on its owner node, such as `FILE_STORAGE`, answers there only: on another node the call fails and names the owner. *** ## Environment Variables & Secrets > [!NOTE] > > **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. > [!TIP] > > 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":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} GET ENV "NAME" ``` #### Examples ```lot wrap theme={"theme":{"light":"css-variables","dark":"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":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} KEEP ENV "NAME" WITH ``` ```lot wrap theme={"theme":{"light":"css-variables","dark":"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":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} DELETE ENV "NAME" ``` ```lot wrap theme={"theme":{"light":"css-variables","dark":"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":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} GET SECRET "NAME" ``` #### Examples ```lot wrap theme={"theme":{"light":"css-variables","dark":"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":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} KEEP SECRET "NAME" WITH "value" ``` ```lot wrap theme={"theme":{"light":"css-variables","dark":"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":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} DELETE SECRET "NAME" ``` ```lot wrap theme={"theme":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} DELETE SECRET "OLD_API_KEY" ``` ### Complete Example ```lot wrap theme={"theme":{"light":"css-variables","dark":"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} ``` > [!NOTE] > > 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":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} + - * / % - ``` ### Basic Examples ```lot wrap theme={"theme":{"light":"css-variables","dark":"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":{"light":"css-variables","dark":"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":{"light":"css-variables","dark":"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":{"light":"css-variables","dark":"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":{"light":"css-variables","dark":"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":{"light":"css-variables","dark":"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":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} + ``` ### Basic Examples ```lot wrap theme={"theme":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} SET "greeting" WITH ("Hello, " + "World!") SET "fullname" WITH ({firstname} + " " + {lastname}) ``` ### Dynamic Topic Construction ```lot wrap theme={"theme":{"light":"css-variables","dark":"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":{"light":"css-variables","dark":"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":{"light":"css-variables","dark":"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":{"light":"css-variables","dark":"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":{"light":"css-variables","dark":"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":{"light":"css-variables","dark":"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":{"light":"css-variables","dark":"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 **Simulation** Generate test data for sensors: ```lot wrap theme={"theme":{"light":"css-variables","dark":"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} ``` **Unique IDs** Generate unique record identifiers: ```lot wrap theme={"theme":{"light":"css-variables","dark":"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} ``` **Load Balancing** Distribute work across workers: ```lot wrap theme={"theme":{"light":"css-variables","dark":"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":{"light":"css-variables","dark":"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":{"light":"css-variables","dark":"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":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} REPLACE "" WITH "" IN ``` #### Examples ```lot wrap theme={"theme":{"light":"css-variables","dark":"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":{"light":"css-variables","dark":"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":{"light":"css-variables","dark":"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":{"light":"css-variables","dark":"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} ``` > [!NOTE] > > 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 | MQTT retain | Typical use | | - | - | - | | `PUBLISH TOPIC` | Only when you add `RETAIN` | Current subscribers, connectors, dashboards, and external systems | | `KEEP TOPIC` | Never | Broker-internal cache for `GET TOPIC` — not an MQTT publish | Use `KEEP TOPIC` for inter-action state (counters, mode flags, last-known values). Use `PUBLISH TOPIC` when connectors, UIs, or other subscribers must react to the message. Use `PUBLISH TOPIC ... RETAIN` when new subscribers must receive the last payload. ```lot wrap theme={"theme":{"light":"css-variables","dark":"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" ``` > [!WARNING] > > Do **not** use `KEEP TOPIC` to drive connector events — that path is unreliable. Use `PUBLISH TOPIC` when a connector or external consumer must react to the message. ### PUBLISH TOPIC Sends a message to an MQTT topic. Current subscribers receive it immediately; the message can trigger other actions or connectors listening on that topic. Optional `RETAIN` sets the MQTT retain flag on the published message (the same keyword as on `DEFINE MODEL`). Optional `QOS0`, `QOS1`, or `QOS2` select MQTT QoS; the default remains QoS 0. Omitting `RETAIN` keeps the historical non-retained behaviour. `RETAIN` and a QoS keyword can appear in either order (`RETAIN QOS1` or `QOS1 RETAIN`). `KEEP TOPIC` is different: it stores a value for `GET TOPIC` in the broker cache and does **not** set the MQTT retain flag. Use `PUBLISH TOPIC ... RETAIN` when new subscribers must receive the last payload. `KEEP` does not accept `RETAIN`. Several `PUBLISH TOPIC` or `KEEP TOPIC` lines to the same topic in one run each go out, in the order the action wrote them, with that line's own retain, QoS, and keep setting. A later `GET TOPIC` in the same run still reads the value just written. In a cluster, only the last `KEEP` of each topic is replicated. #### Syntax ```lot wrap theme={"theme":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} PUBLISH TOPIC "" WITH [RETAIN] [QOS0|QOS1|QOS2] ``` **Retain** Store the last status so a new subscriber receives the last payload: ```lot wrap theme={"theme":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} DEFINE ACTION PublishOnline ON START DO PUBLISH TOPIC "status/last" WITH "online" RETAIN ``` **Retain and QoS 1** Set the MQTT retain flag and publish at QoS 1: ```lot wrap theme={"theme":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} DEFINE ACTION PublishOnlineQos1 ON START DO PUBLISH TOPIC "status/last" WITH "online" RETAIN QOS1 ``` **QoS only** Choose QoS without retaining the message. Omit the QoS keyword and the publish stays QoS 0: ```lot wrap theme={"theme":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} DEFINE ACTION PublishOnlineQos2 ON START DO PUBLISH TOPIC "status/last" WITH "online" QOS2 ``` #### Value Types You can publish various value types: ```lot wrap theme={"theme":{"light":"css-variables","dark":"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 stored with `KEEP TOPIC`, 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`). To hand those bytes to an external gRPC service, publish them on the source topic of a [gRPC connector](/latest/lot-language/routes/streaming/grpc) (`TYPE GRPC`). There is no `CALL GRPC`. ```lot wrap focus={3} theme={"theme":{"light":"css-variables","dark":"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":{"light":"css-variables","dark":"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 ``` > [!WARNING] > > **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. > [!WARNING] > > **Cannot Use Inline JSON Objects** > > `PUBLISH TOPIC` does **not** support inline JSON object literals. This will NOT work: > > ```lot wrap theme={"theme":{"light":"css-variables","dark":"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":{"light":"css-variables","dark":"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":{"light":"css-variables","dark":"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":{"light":"css-variables","dark":"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" ``` > [!TIP] > > **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 value for `GET TOPIC` in the broker cache. Unlike `SET` (which exists only during one execution), a `KEEP` value stays available to later runs on this broker. It is a broker-internal cache and never sets the MQTT retain flag. `KEEP` does not accept `RETAIN` (use `PUBLISH TOPIC ... RETAIN`). > [!WARNING] > > `KEEP TOPIC` does **not** publish to MQTT. The value stays inside the broker: subscribers, MQTT clients, the HUB Data Viewer, dashboards and connectors never receive it. Only `GET TOPIC` can read it. Use `PUBLISH TOPIC` (with `RETAIN` if new subscribers need the last value) whenever the value must be visible. #### 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":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} KEEP TOPIC "" WITH KEEP TOPIC "" WITH PAYLOAD AS BYTES ``` Store string or variable state, including dynamic topic paths: ```lot wrap theme={"theme":{"light":"css-variables","dark":"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":{"light":"css-variables","dark":"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 | MQTT retain | Delivered to subscribers | Readable with `GET TOPIC` | Typical use | | - | - | - | - | - | | `PUBLISH TOPIC` | Only with `RETAIN` | Yes | Yes (until overwritten) | Connectors, dashboards, external subscribers | | `KEEP TOPIC` | No | No | Yes | Broker-internal cache for `GET TOPIC` | *** ## Control Flow ### IF ... THEN ... ELSE Execute different logic based on conditions. ```lot wrap theme={"theme":{"light":"css-variables","dark":"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":{"light":"css-variables","dark":"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":{"light":"css-variables","dark":"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":{"light":"css-variables","dark":"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":{"light":"css-variables","dark":"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":{"light":"css-variables","dark":"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":{"light":"css-variables","dark":"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":{"light":"css-variables","dark":"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":{"light":"css-variables","dark":"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. > [!NOTE] > > For reusable logic, on-demand scripts, and composition patterns, see [Callable ACTIONs](./callable-actions). ### Defining a Callable Action ```lot wrap theme={"theme":{"light":"css-variables","dark":"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":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} CALL ACTION ActionName WITH param1 = , param2 = RETURN var1, var2 ``` ### Basic Example **Define** Create a reusable calculation: ```lot wrap theme={"theme":{"light":"css-variables","dark":"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 ``` **Call** Use the callable action: ```lot wrap theme={"theme":{"light":"css-variables","dark":"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":{"light":"css-variables","dark":"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":{"light":"css-variables","dark":"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":{"light":"css-variables","dark":"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 connector events or mappings. This is useful for initiating data pipeline operations or triggering external integrations. ### Syntax ```lot wrap theme={"theme":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} TRIGGER "" TO FOR ``` ### Components | Component | Description | | - | - | | `connector_name` | The name of the connector to trigger | | `data` | The data to send (PAYLOAD, variable, or expression) | | `mapping_name` | The specific mapping or event within the connector | ### Basic Examples ```lot wrap theme={"theme":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} // Trigger a connector 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":{"light":"css-variables","dark":"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 connector 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 **Email Notifications** ```lot wrap theme={"theme":{"light":"css-variables","dark":"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" ``` **Database Operations** ```lot wrap theme={"theme":{"light":"css-variables","dark":"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" ``` **External APIs** ```lot wrap theme={"theme":{"light":"css-variables","dark":"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) connectors. This enables integration with AI assistants and external tools. > [!NOTE] > > `CALL MCP` talks to an [MCP connector](/latest/lot-language/routes/mcp-routes) (`TYPE MCP`) — the broker as a **client** of an external server. To let an AI client operate **this** broker, enable the [MCP Server](/latest/mqtt-broker/mcp-server) instead. `TYPE MCP_SERVER` was removed. > > A [gRPC connector](/latest/lot-language/routes/streaming/grpc) (`TYPE GRPC`) is also a client connector, and it is not invoked with `CALL`. It forwards MQTT payloads from `SOURCE_TOPIC`. ### Prerequisites * An MCP connector must be defined and active * The connector must expose the tool you want to call ### Syntax ```lot wrap theme={"theme":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} CALL MCP "RouteName.tool_name" WITH (arg1 = , arg2 = , ...) RETURN AS {variable_name} ``` ### Example MCP Connector ```lot wrap theme={"theme":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} DEFINE CONNECTOR 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":{"light":"css-variables","dark":"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} ``` ### Return Value `RETURN AS` receives a JSON string with `success`, `tool_name`, `route_name`, `timestamp`, and `execution_time_ms`. A successful call adds `result`, the server's tool result. A failed call adds `error` instead. See [Return Value](/latest/lot-language/routes/mcp-routes#return-value). ### Complete Example ```lot wrap theme={"theme":{"light":"css-variables","dark":"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} SET "read_ok" WITH (GET JSON "success" IN {config_data} AS BOOL) IF {read_ok} EQUALS FALSE THEN PUBLISH TOPIC "devices/" + {device_id} + "/config/error" WITH (GET JSON "error" IN {config_data} AS STRING) ELSE PUBLISH TOPIC "devices/" + {device_id} + "/config" WITH {config_data} ``` *** ## CALL LLM `CALL LLM` sends a prompt to a [TYPE LLM connector](/latest/lot-language/routes/ai-routes) and returns JSON. The connector does not call tools. For an assistant that can read topics or publish, use `CALL AGENT`. ```lot wrap theme={"theme":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} DEFINE CONNECTOR PlantModel WITH TYPE LLM ADD LLM_CONFIG WITH PROVIDER "openai" WITH MODEL "gpt-6-luna" WITH API_KEY GET SECRET "OPENAI_API_KEY" WITH API_SURFACE "auto" DEFINE ACTION ExplainAlarm ON TOPIC "alarms/raw/+" DO CALL LLM "PlantModel.generate" WITH (prompt = PAYLOAD) RETURN AS {result} PUBLISH TOPIC "alarms/explained" WITH (GET JSON "response" IN {result} AS STRING) ``` `generate` is one turn. `chat` with `use_history = "true"` keeps an in-memory thread per `session_id`. `clear_history` drops that thread. `INVOKE LLM` is the same call. Check `success` in the JSON before you use `response`. *** ## CALL AGENT `CALL AGENT` runs a [DEFINE AGENT](/latest/lot-language/routes/ai-routes) and waits for the answer. The call runs as the agent's `DEFAULT_USER`. `execute` returns the answer text, or JSON `{ "success": false, "status", "code", "message" }` when the task is refused or fails. ```lot wrap theme={"theme":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} DEFINE CONNECTOR LocalLlama WITH TYPE LLM ADD LLM_CONFIG WITH MODEL "llama3.2" DEFINE AGENT Classifier USES LLM CONNECTOR LocalLlama WITH DEFAULT_USER "agent_svc" WITH SYSTEM_PROMPT "Answer with one word: safety, quality or other." WITH MEMORY "none" DEFINE ACTION ClassifyNote ON TOPIC "operators/notes" DO CALL AGENT "Classifier.execute" WITH (task = PAYLOAD) RETURN AS {label} PUBLISH TOPIC "operators/notes/classified" WITH {label} ``` `clear_history` takes `session_id` and forgets that session for `DEFAULT_USER`. A name that is not a running agent returns `unknown_agent`. *** ## 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":{"light":"css-variables","dark":"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 LLM` | Integration | Prompt a TYPE LLM connector | | `CALL AGENT` | Integration | Run a DEFINE AGENT and wait for the answer | | `CALL ACTION` | Control | Call other actions | | `TRIGGER` | Control | Trigger connector events | | `PUBLISH TOPIC` | Topic | Publish to connectors, dashboards, and subscribers. Optional `RETAIN` and `QOS0` / `QOS1` / `QOS2`. Omitting `RETAIN` keeps the historical non-retained behaviour. | | `PUBLISH TOPIC ... WITH PAYLOAD AS BYTES` | Topic | Forward binary payloads bit-for-bit | | `KEEP TOPIC` | Topic | Broker-internal cache for `GET TOPIC`. Never sets the MQTT retain flag. Does not accept `RETAIN`. | | `KEEP TOPIC ... WITH PAYLOAD AS BYTES` | Topic | Store binary payloads in the broker cache without string conversion | *** ## Next Steps > **[Python Integration](./python-integration)** > > Extend actions with Python functions for complex logic. > **[LoT Notebooks](/lot-notebooks/usage)** > > 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. > [!TIP] > > **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** | Connector 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 > **[Syntax Reference](./syntax)** > > Start here to learn the DEFINE ACTION structure and build your first action. > **[Action Triggers](./events)** > > Choose the right trigger type for your use case: time-based, topic-based, or callable. > **[Operations](./operations)** > > Master SET, GET, PUBLISH, conditionals, and all available LoT operations. > **[Python Integration](./python-integration)** > > 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":{"light":"css-variables","dark":"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":{"light":"css-variables","dark":"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":{"light":"css-variables","dark":"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":{"light":"css-variables","dark":"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":{"light":"css-variables","dark":"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 > [!NOTE] > > Learn more in [Callable ACTIONs](./callable-actions)—reusable logic, on-demand scripts, and `CALL ACTION`. *** ## Core Operations Actions use these operations to process data: > [!NOTE] > > 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. **Variables** Create and use variables within an action: ```lot wrap theme={"theme":{"light":"css-variables","dark":"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} ``` **Topic Operations** Read from and write to MQTT topics: ```lot wrap theme={"theme":{"light":"css-variables","dark":"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 ``` **JSON Extraction** Parse JSON payloads and extract fields: ```lot wrap theme={"theme":{"light":"css-variables","dark":"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) ``` **Conditionals** Implement branching logic: ```lot wrap theme={"theme":{"light":"css-variables","dark":"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":{"light":"css-variables","dark":"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. Connectors 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 connector 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 connector data, then publish it using a Model to produce structured JSON output. *** ## Next Steps > **[Syntax Reference](./syntax)** > > Learn the complete DEFINE ACTION syntax and components. > **[Action Triggers](./events)** > > Master time-based, topic-based, lifecycle, and ON START triggers. > **[Callable ACTIONs](./callable-actions)** > > 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. > [!TIP] > > **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) | > [!NOTE] > > 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":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} # Script Name: Calculator def add(a, b): return a + b def multiply(a, b): return a * b ``` > [!WARNING] > > 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":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} CALL PYTHON "ScriptName.function_name" WITH (parameter1, parameter2, ...) RETURN AS {variable_name} ``` > [!NOTE] > > Put `WITH` and `RETURN AS` on their own indented lines under `CALL PYTHON`. The one-line form `CALL PYTHON "X.f" WITH (…) RETURN AS {r}` is not supported and fails to parse. ## Parameter Passing ### Single Parameter ```lot wrap theme={"theme":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} CALL PYTHON "Processor.validate" WITH (PAYLOAD) RETURN AS {result} ``` ### Multiple Parameters ```lot wrap theme={"theme":{"light":"css-variables","dark":"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":{"light":"css-variables","dark":"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":{"light":"css-variables","dark":"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":{"light":"css-variables","dark":"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":{"light":"css-variables","dark":"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":{"light":"css-variables","dark":"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 > **Always Set the Script Name Header** > > 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. > **Keep Functions Fast** > > 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. > **Always Handle Errors** > > Wrap Python logic in try/except blocks and return structured error responses. This lets your LoT action gracefully handle failures instead of crashing. > **Return JSON-Serializable Types** > > Return dictionaries, lists, strings, numbers, and booleans. Custom objects and classes won't serialize properly. When in doubt, convert to a dictionary. > **Validate Input Parameters** > > 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 > **[Operations Reference](./operations)** > > Review all available LoT operations. > **[Actions Overview](./overview)** > > 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. > [!TIP] > > Actions run inside the MQTT broker itself, eliminating the need for external middleware or application servers. *** ## Basic Syntax ```lot wrap theme={"theme":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} DEFINE ACTION ON DO ``` ### Components **action\_name** `string` *required* A unique identifier for the action. Use descriptive names that indicate the action's purpose (e.g., `SensorDataProcessor`, `HeartbeatMonitor`). **EVENT\_TRIGGER** `trigger` *required* 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. **action\_logic** `statements` *required* 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":{"light":"css-variables","dark":"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 ``` ## Disable an Action Add `DISABLE true` on its own line to switch an action off without deleting it: ```lot wrap theme={"theme":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} DEFINE ACTION Heartbeat DISABLE true ON EVERY 5 SECONDS DO PUBLISH TOPIC "system/heartbeat" WITH TIMESTAMP "UTC" ``` A disabled action stays defined and listed, but no trigger runs it: topics, `ON EVERY`, schedules, `ON START`, and client connect, disconnect, or subscribe. `-runAction` refuses it, and an action that calls it with `CALL ACTION` fails to compile until it is enabled again. `DISABLE false`, or no `DISABLE` line, means enabled. A `DISABLE` line inside a comment is ignored. The line is saved with the definition, so the action stays disabled after a restart and in a project export. To switch it without editing the code, use `-disableAction ` and `-enableAction `. See [Disable and enable](/latest/mqtt-broker/commands#disable-and-enable). ## Trigger Types **Time-Based** Execute at regular intervals: ```lot wrap theme={"theme":{"light":"css-variables","dark":"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)` **Topic-Based** 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":{"light":"css-variables","dark":"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":{"light":"css-variables","dark":"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. **Client lifecycle** Execute when MQTT clients connect, disconnect, or subscribe: ```lot wrap theme={"theme":{"light":"css-variables","dark":"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). **Callable** No trigger - called manually or by other actions: ```lot wrap theme={"theme":{"light":"css-variables","dark":"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":{"light":"css-variables","dark":"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` | Publish to subscribers, connectors, and dashboards. Optional `RETAIN` sets the MQTT retain flag on the published message. Optional `QOS0`, `QOS1`, or `QOS2` select MQTT QoS; the default remains QoS 0. Omitting `RETAIN` keeps the historical non-retained behaviour. | | `KEEP TOPIC` | Broker-internal cache for `GET TOPIC`. Never sets the MQTT retain flag. Does not accept `RETAIN`. | ```lot wrap theme={"theme":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} PUBLISH TOPIC "output/data" WITH {temperature} PUBLISH TOPIC "devices/" + {device_id} + "/status" WITH "online" PUBLISH TOPIC "status/last" WITH "online" RETAIN PUBLISH TOPIC "status/last" WITH "online" RETAIN QOS1 PUBLISH TOPIC "status/last" WITH "online" QOS2 KEEP TOPIC "cache/last_reading" WITH PAYLOAD ``` `KEEP TOPIC` is different: it stores a value for `GET TOPIC` in the broker cache and does not set the MQTT retain flag. `KEEP` does not accept `RETAIN` (use `PUBLISH TOPIC ... RETAIN` when new subscribers must receive the last payload). `RETAIN` and a QoS keyword can appear in either order (`RETAIN QOS1` or `QOS1 RETAIN`). Each `PUBLISH TOPIC` and each `KEEP TOPIC` in one run is sent in the order it is written, including several writes to the same topic. Full actions are in [ACTION Operations](./operations#publish-topic). ### Control Flow | Keyword | Description | | - | - | | `IF ... THEN ... ELSE` | Conditional execution | ```lot wrap theme={"theme":{"light":"css-variables","dark":"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 | > [!NOTE] > > **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":{"light":"css-variables","dark":"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`, `FLOAT` (alias `SINGLE`, for protobuf `float`), `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 > [!NOTE] > > **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":{"light":"css-variables","dark":"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 > **[Action Triggers](./events)** > > Learn about time-based, topic-based, and client lifecycle triggers in detail. > **[Operations](./operations)** > > 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. LoT is built by defining **Actions**, **Models**, **Connectors**, 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. An **agent** (`DEFINE AGENT`) is a further entity: a tool-using assistant that runs on a `TYPE LLM` connector. See [AI Connectors and Agents](/latest/lot-language/routes/ai-routes). > [!TIP] > > Think of LoT as SQL for real-time MQTT data. Just as SQL transforms database records, LoT transforms live MQTT streams. > [!TIP] > > **Like building with LEGO blocks.** You pick the right block for the job — Actions for logic, Models for data structure, Connectors for connections, Rules for security — and snap them together into a complete system. ## The Building Blocks Each construct is 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 | | **Connectors** | Connect to external systems | Topic patterns | | **Rules** | Control who can publish/subscribe | Client identity, Topic patterns | | **Agents** | A tool-using assistant on a `TYPE LLM` connector | MQTT, another agent, or `CALL AGENT` | > **[Actions](./actions/overview)** > > Event-driven logic triggered by time or MQTT topics. > **[Models](./models/overview)** > > Data schemas that structure MQTT messages into JSON. > **[Rules](./rules/overview)** > > Access control for topics and operations. > **[Connectors](./routes/overview)** > > Connect to databases, brokers, and external services. > **[Agents](./routes/ai-routes)** > > A tool-using assistant on a TYPE LLM connector. *** ## 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), [Connectors](/latest/lot-language/routes/overview), and [Rules](/latest/lot-language/rules/overview) references. ```lot wrap theme={"theme":{"light":"css-variables","dark":"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. CONNECTOR: Persist sensor traffic to PostgreSQL DEFINE CONNECTOR 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 Connector 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":{"light":"css-variables","dark":"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":{"light":"css-variables","dark":"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":{"light":"css-variables","dark":"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 > **[Best Practices & Patterns](/latest/quick-start/best-practices)** > > Naming, topic design, reusable logic, and pitfalls to avoid. > **[Actions Overview](/latest/lot-language/actions/overview)** > > 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":{"light":"css-variables","dark":"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":{"light":"css-variables","dark":"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":{"light":"css-variables","dark":"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":{"light":"css-variables","dark":"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":{"light":"css-variables","dark":"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":{"light":"css-variables","dark":"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":{"light":"css-variables","dark":"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":{"light":"css-variables","dark":"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":{"light":"css-variables","dark":"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":{"light":"css-variables","dark":"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":{"light":"css-variables","dark":"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":{"light":"css-variables","dark":"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" } ``` > [!NOTE] > > 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":{"light":"css-variables","dark":"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":{"light":"css-variables","dark":"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":{"light":"css-variables","dark":"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 > [!TIP] > > 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":{"light":"css-variables","dark":"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":{"light":"css-variables","dark":"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":{"light":"css-variables","dark":"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":{"light":"css-variables","dark":"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":{"light":"css-variables","dark":"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":{"light":"css-variables","dark":"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":{"light":"css-variables","dark":"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":{"light":"css-variables","dark":"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":{"light":"css-variables","dark":"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 ``` > [!NOTE] > > Complex conditional logic (like tolerance checking) is cleaner in an action than inline in a model. The action can also connector to different topics based on the result. Define the quality reading schema: ```lot wrap theme={"theme":{"light":"css-variables","dark":"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 connectors accordingly: ```lot wrap theme={"theme":{"light":"css-variables","dark":"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":{"light":"css-variables","dark":"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 > [!NOTE] > > 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":{"light":"css-variables","dark":"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":{"light":"css-variables","dark":"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":{"light":"css-variables","dark":"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":{"light":"css-variables","dark":"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":{"light":"css-variables","dark":"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":{"light":"css-variables","dark":"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":{"light":"css-variables","dark":"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":{"light":"css-variables","dark":"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":{"light":"css-variables","dark":"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":{"light":"css-variables","dark":"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" } ``` > [!NOTE] > > The indentation rule applies everywhere `OBJECT` is used — in basic models, wildcard models, and `COLLAPSED` templates published from actions. *** ## Protobuf Examples > [!NOTE] > > 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":{"light":"css-variables","dark":"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":{"light":"css-variables","dark":"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":{"light":"css-variables","dark":"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":{"light":"css-variables","dark":"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":{"light":"css-variables","dark":"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":{"light":"css-variables","dark":"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. > [!NOTE] > > 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 > **Basic Models (WITH TOPIC + AS TRIGGER)** > > **Use when:** Data flows continuously and formatting is straightforward. > > **Best for:** Sensor readings, status updates, simple aggregation. > > **Trigger:** Automatic when source topic updates. > **Calculated Fields** > > **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. > **Wildcard Models (+)** > > **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. > **Action-Published Models (COLLAPSED)** > > **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. > **Inherited Models (FROM)** > > **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. > **Nested Objects (OBJECT)** > > **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. > **Protobuf (WITH FORMAT PROTOBUF)** > > **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 > **[Connectors Overview](/latest/lot-language/routes/overview)** > > Connect your broker to external systems and databases. > **[Actions Syntax](/latest/lot-language/actions/syntax)** > > 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. > [!TIP] > > **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":{"light":"css-variables","dark":"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":{"light":"css-variables","dark":"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) > [!TIP] > > 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":{"light":"css-variables","dark":"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" ``` > [!WARNING] > > 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":{"light":"css-variables","dark":"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 Alert** Temperature alerts add measurement data and thresholds: ```lot wrap theme={"theme":{"light":"css-variables","dark":"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 Alert** Pressure alerts track system-level concerns: ```lot wrap theme={"theme":{"light":"css-variables","dark":"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 Alert** Maintenance alerts focus on service schedules: ```lot wrap theme={"theme":{"light":"css-variables","dark":"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":{"light":"css-variables","dark":"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":{"light":"css-variables","dark":"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) ``` > [!NOTE] > > 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 **Equipment Hierarchy** A complete equipment monitoring system with specialized equipment types: ```lot wrap theme={"theme":{"light":"css-variables","dark":"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":{"light":"css-variables","dark":"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) ``` **Event Tracking** An event system for production, maintenance, and quality events: ```lot wrap theme={"theme":{"light":"css-variables","dark":"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" ``` **Sensor Network** A sensor hierarchy for different measurement types: ```lot wrap theme={"theme":{"light":"css-variables","dark":"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 > **Keep Base Models Focused** > > 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":{"light":"css-variables","dark":"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! > ``` > **Use Descriptive Model Names** > > Name child models to clearly indicate their specialization: > > ```lot wrap theme={"theme":{"light":"css-variables","dark":"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 > ``` > **Document the Hierarchy** > > When you have multiple inheritance levels, keep the structure clear: > > ``` > BaseEntity > └── BaseEquipment > ├── PumpEquipment > ├── ConveyorEquipment > └── MotorEquipment > ``` > **Provide All Fields When Publishing** > > When using `PUBLISH MODEL`, you must provide values for both inherited and specialized fields: > > ```lot wrap theme={"theme":{"light":"css-variables","dark":"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 > [!WARNING] > > **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 > **[Publishing Models](./publish-model)** > > Master the PUBLISH MODEL syntax for dynamic data creation. > **[Model Examples](./examples)** > > 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. > [!TIP] > > Models transform raw MQTT messages into structured, typed data - eliminating the need for external data transformation layers. ## Model Capabilities > **Data Structuring** > > Define fields with specific types (STRING, INT, DOUBLE, BOOL, ARRAY) to create consistent data schemas. > **Auto JSON Handling** > > Automatically explode incoming JSON payloads into individual topics for easy field access. > **Data Aggregation** > > Combine data from multiple MQTT topics into a single structured output. > **Calculations** > > 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":{"light":"css-variables","dark":"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":{"light":"css-variables","dark":"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"` > [!NOTE] > > 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":{"light":"css-variables","dark":"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":{"light":"css-variables","dark":"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":{"light":"css-variables","dark":"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":{"light":"css-variables","dark":"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":{"light":"css-variables","dark":"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":{"light":"css-variables","dark":"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. > [!WARNING] > > 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: > [!NOTE] > > * **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":{"light":"css-variables","dark":"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":{"light":"css-variables","dark":"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":{"light":"css-variables","dark":"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 > [!NOTE] > > 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":{"light":"css-variables","dark":"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 > [!NOTE] > > Learn more about designing model hierarchies in [Model Inheritance](./model-inheritance). *** ## Data Sources Model fields can be populated from various sources: **Static Values** Use static values for constants like units, default thresholds, or fixed identifiers that never change: ```lot wrap theme={"theme":{"light":"css-variables","dark":"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 ``` **Topic Data** Pull live values from MQTT topics. Use `AS TRIGGER` on the field that should cause the model to publish: ```lot wrap theme={"theme":{"light":"css-variables","dark":"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 ``` **Timestamps** Add automatic timestamps in UTC format or as Unix epoch integers for time-series data: ```lot wrap theme={"theme":{"light":"css-variables","dark":"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" ``` **Calculations** Compute derived values inline using arithmetic operations on topic data: ```lot wrap theme={"theme":{"light":"css-variables","dark":"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) ``` **Conditionals** Apply conditional logic to set field values based on dynamic conditions: ```lot wrap theme={"theme":{"light":"css-variables","dark":"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 > **[Model Syntax](./schema-definition)** > > Learn the complete syntax for defining model fields and triggers. > **[Model Inheritance](./model-inheritance)** > > 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. > [!NOTE] > > 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. > [!TIP] > > **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; * a [gRPC connector](/latest/lot-language/routes/streaming/grpc) should forward those bytes to a gRPC service. The connector does not decode protobuf. Use [JSON models](./overview) and [`GET JSON`](/latest/lot-language/actions/operations#get-json) when payloads are JSON. Use the [Sparkplug B Connector](/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 **Decode a command** 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":{"light":"css-variables","dark":"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":{"light":"css-variables","dark":"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} ``` **Publish telemetry** Nested messages you fill on publish use `ADD MODEL`: ```lot wrap theme={"theme":{"light":"css-variables","dark":"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":{"light":"css-variables","dark":"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} ``` **Nested float fields** Declare proto `float` with `ADD FLOAT` and nested messages with `ADD MODEL`. A dotted path walks into the child: ```lot wrap theme={"theme":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} DEFINE MODEL AudioInfo WITH FORMAT PROTOBUF COLLAPSED ADD FLOAT "sample_rate_hz" PROTO_TAG 1 ADD INT "channels" PROTO_TAG 2 DEFINE MODEL AudioFrame WITH FORMAT PROTOBUF COLLAPSED ADD STRING "service" PROTO_TAG 1 ADD MODEL AudioInfo "audio_info" PROTO_TAG 2 ``` Read the nested float and the top-level string. `USING MODEL` names the top-level message: ```lot wrap theme={"theme":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} DEFINE ACTION ReadAudioFrame ON TOPIC "edge/+/audio" DO SET "rate" WITH (GET PROTO "audio_info.sample_rate_hz" IN PAYLOAD AS FLOAT USING MODEL "AudioFrame") SET "service" WITH (GET PROTO "service" IN PAYLOAD AS STRING USING MODEL "AudioFrame") PUBLISH TOPIC "edge/audio/rate" WITH {rate} ``` *** ## 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 connector 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 connectors. ```mermaid actions={false} theme={"theme":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} sequenceDiagram participant GW as Gateway or SCADA participant Broker participant PLC as PLC via OT connector GW->>Broker: DeviceCommand proto bytes Broker->>Broker: GET PROTO command_type Broker->>PLC: TRIGGER connector 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` | | `float` | `ADD FLOAT` (alias `ADD SINGLE`) | `FLOAT`, `SINGLE`, or `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 FLOAT` is protobuf `float` — **not** a nested model named `FLOAT`. `FLOAT` and `SINGLE` are reserved type keywords; outside protobuf they behave as aliases of `DOUBLE`. After a `float` is decoded it becomes a LoT number, so `AS DOUBLE` on a `FLOAT` field is valid. LoT has no separate 32-bit numeric type. `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 | | Declared `DOUBLE`, sender sent `float` (or the reverse) | That field is skipped; every other field still decodes | 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` / `FLOAT` / `SINGLE` / `STRING` / `BOOL` / `BYTES`** selects the LoT type after decode. On a nested path, `USING MODEL` still names the top-level message. Read from the last payload stored on another topic: ```lot wrap focus={3} theme={"theme":{"light":"css-variables","dark":"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":{"light":"css-variables","dark":"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. * **`USING MODEL` always names the top-level message**, even when the path is nested. * **A mismatch inside the child does not fail the parent.** Sibling fields still decode. * **`ADD FLOAT` is what makes a 32-bit field readable.** Declaring it `ADD DOUBLE` makes the decoder expect 8 bytes where the sender wrote 4 — the field is skipped. 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":{"light":"css-variables","dark":"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 MODEL ReadSnapshot "read_snapshot" PROTO_TAG 6 DEFINE MODEL ReadSnapshot WITH FORMAT PROTOBUF COLLAPSED ADD STRING "tag_group" PROTO_TAG 1 ADD INT "max_age_ms" PROTO_TAG 2 ``` `ADD OBJECT` holds a wire slot open but carries no schema, so a dotted path through an `ADD OBJECT` field cannot resolve. Use `ADD MODEL` on the arm you read. *** ## Middle: tags stay JSON Between command and telemetry, most OT setups already have **tag values** on topics — from a `DEFINE CONNECTOR` 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":{"light":"css-variables","dark":"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":{"light":"css-variables","dark":"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":{"light":"css-variables","dark":"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":{"light":"css-variables","dark":"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 vs ADD MODEL** > > **`ADD OBJECT`** when **publishing** a populated nested **message** produces JSON-in-bytes. Use **`ADD MODEL`**. > **Missing PROTO\_TAG on sparse fields** > > Events at **101+** need an explicit tag. Omitting unused command slots on the parent also shifts later wire indexes. > **GET PROTO needs ADD MODEL** > > Dotted **`GET PROTO`** paths require the parent to declare the child with **`ADD MODEL`**. > **ADD DOUBLE on proto float** > > The field is skipped. Use **`ADD FLOAT`** (or **`ADD SINGLE`**). > **USING MODEL names the top** > > On a nested read, name the **top-level** message, not the child model. > **Reserved words as variable names** > > A variable may use a name shaped like a LoT keyword (`store`, `unit`, `limit`, `device`, and the rest). Inside `{…}` — expressions, conditions, topic strings, payloads, and `PUBLISH MODEL … WITH` field values — that name is a variable reference. Keyword matching is case-insensitive, so `{Device}` and `{device}` are the same variable. > > A protobuf field name is not that reference. The model can still declare a field called `device`; `{device}` reads the variable, not the field label. *** ## 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` | | One field is `null`, the rest decode | Wire-type mismatch on that field | Check `float` vs `double` and fixed-width vs varint in the `.proto` | | A field and **everything after it** is missing | Field declared at the wrong width, truncating the read | Same as above — most often `ADD DOUBLE` on a proto `float` | | A nested path is `null` but the parent decodes | Parent declares the child as `ADD OBJECT` | Use `ADD MODEL Child "field"` | 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`. > [!WARNING] > > **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. > * A `float` is promoted to a LoT number after decode, so a round-trip re-encodes at the width the field declares. *** ## Next Steps > **[Publishing Models](./publish-model)** > > Control when and where COLLAPSED models publish, including protobuf bytes. > **[GET PROTO in Actions](/latest/lot-language/actions/operations#get-proto)** > > 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 > [!TIP] > > 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. Connector critical alarms to a special topic 4. Publish a structured record ```lot wrap theme={"theme":{"light":"css-variables","dark":"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 **1. Define a COLLAPSED Model** Create a template with all required fields. The `COLLAPSED` keyword indicates this model has no automatic trigger: ```lot wrap theme={"theme":{"light":"css-variables","dark":"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" ``` **2. Create the Processing Action** Define an action that extracts data and publishes the model: ```lot wrap theme={"theme":{"light":"css-variables","dark":"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" ``` **3. Test the Flow** **Publish to:** `sensors/temp001/json_data` ```json wrap theme={"theme":{"light":"css-variables","dark":"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":{"light":"css-variables","dark":"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 **Alarm Processing** Connector alarms by severity while creating structured records: ```lot wrap theme={"theme":{"light":"css-variables","dark":"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" // Connector 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 ``` **Quality Reports** Calculate metrics and generate quality reports: ```lot wrap theme={"theme":{"light":"css-variables","dark":"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} + "%" ``` **Event Logging** Create structured event logs with context: ```lot wrap theme={"theme":{"light":"css-variables","dark":"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":{"light":"css-variables","dark":"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":{"light":"css-variables","dark":"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 > **Always Use Type Casting with GET JSON** > > Explicit type casting prevents runtime errors: > > ```lot wrap theme={"theme":{"light":"css-variables","dark":"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) > ``` > **Validate Extracted Data** > > Check data before publishing: > > ```lot wrap theme={"theme":{"light":"css-variables","dark":"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} > ``` > **Use Dynamic Topics** > > Include identifiers in output topics for easy filtering: > > ```lot wrap theme={"theme":{"light":"css-variables","dark":"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 ... > ``` > **Generate Unique IDs** > > Use `RANDOM UUID` for record identification: > > ```lot wrap theme={"theme":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} > PUBLISH MODEL Record TO "records/new" WITH > record_id = (RANDOM UUID) > ... > ``` ## Next Steps > **[Model Examples](./examples)** > > See complete examples for common use cases. > **[Action Operations](../actions/operations)** > > 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. > [!TIP] > > **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":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} DEFINE MODEL WITH TOPIC "" ADD "" WITH [AS TRIGGER] ADD "" WITH ... ``` ### Components **ModelName** `string` *required* Unique identifier for the model. Use descriptive names like `SensorReading`, `EquipmentStatus`, `ProductionRecord`. **WITH TOPIC** `string` *required* 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`. **WITH FORMAT** `enum` Optional. `JSON` (default), `PROTOBUF` / `PROTO`, or `BOTH`. Controls what `PUBLISH MODEL` puts on the destination topic. See [Protobuf Models](./protobuf). **FIELD\_TYPE** `type` *required* One of: `STRING`, `INT`, `DOUBLE`, `FLOAT` (alias `SINGLE`), `BOOL`, `ARRAY`, `OBJECT`, or `MODEL` (nested protobuf message) **AS TRIGGER** `modifier` 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. > [!TIP] > > **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":{"light":"css-variables","dark":"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. > [!NOTE] > > 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":{"light":"css-variables","dark":"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":{"light":"css-variables","dark":"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":{"light":"css-variables","dark":"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":{"light":"css-variables","dark":"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) ``` ### FLOAT Protobuf `float` (32-bit). Alias: `ADD SINGLE`. Use this when the `.proto` field is `float`, not `double`. After decode the value is a LoT number, so `GET PROTO … AS DOUBLE` on a `FLOAT` field is valid. ```lot wrap theme={"theme":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} ADD FLOAT "sample_rate_hz" PROTO_TAG 1 ADD SINGLE "gain" PROTO_TAG 2 ``` > [!NOTE] > > Outside protobuf, `FLOAT` and `SINGLE` behave as aliases of `DOUBLE`. For binary schemas, `ADD DOUBLE` on a proto `float` skips that field. See [Protobuf Models](./protobuf#type-mapping). ### BOOL Boolean values (true/false): ```lot wrap theme={"theme":{"light":"css-variables","dark":"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":{"light":"css-variables","dark":"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":{"light":"css-variables","dark":"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":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} { "metadata": { "location": "Building A", "floor": 3 } } ``` > [!WARNING] > > 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, and when you read a dotted `GET PROTO` path. Pair with `PROTO_TAG` so the wire number matches the `.proto`. ```lot wrap theme={"theme":{"light":"css-variables","dark":"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":{"light":"css-variables","dark":"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":{"light":"css-variables","dark":"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":{"light":"css-variables","dark":"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" ``` > [!TIP] > > **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":{"light":"css-variables","dark":"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":{"light":"css-variables","dark":"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) ``` > [!WARNING] > > 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":{"light":"css-variables","dark":"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":{"light":"css-variables","dark":"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":{"light":"css-variables","dark":"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":{"light":"css-variables","dark":"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 **Equipment Status** This model aggregates equipment data from multiple topics into a single, structured output. It triggers whenever the status changes. ```lot wrap theme={"theme":{"light":"css-variables","dark":"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":{"light":"css-variables","dark":"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" } ``` **Production Record** This model calculates efficiency metrics inline. It triggers when the quantity updates, capturing a snapshot of the production run. ```lot wrap theme={"theme":{"light":"css-variables","dark":"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":{"light":"css-variables","dark":"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 > **Use Consistent Field Naming** > > Establish naming conventions and follow them across all models: > > ```lot wrap theme={"theme":{"light":"css-variables","dark":"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" > ``` > **Always Type Cast in Calculations** > > Explicit type casting prevents errors and ensures predictable results: > > ```lot wrap theme={"theme":{"light":"css-variables","dark":"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) > ``` > **Include Essential Context** > > Always include fields that help downstream consumers understand the data: > > * Identification fields (IDs, names) > * Timestamps > * Units of measurement > * Status or quality indicators > **Choose Triggers Carefully** > > Trigger on the primary data field, not metadata: > > ```lot wrap theme={"theme":{"light":"css-variables","dark":"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 > **[Publishing Models](./publish-model)** > > Learn to publish models dynamically from Actions. > **[Model Examples](./examples)** > > See complete model examples for common use cases. # CrateDB Connector 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` connector 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. > [!NOTE] > > 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":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} DEFINE CONNECTOR 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 **HOST** `string` *required* CrateDB server hostname or IP address. **USERNAME** `string` *required* Database username. **PASSWORD** `string` *required* Database password. > **Optional Settings** > > **PORT** `integer` > > CrateDB PostgreSQL protocol port. Default: 5432. > > **DATABASE** `string` > > Target database/schema name. > [!WARNING] > > **Never hardcode credentials in production.** Use environment variables and encrypted secrets to keep sensitive values out of your connector definitions: > > ```lot wrap theme={"theme":{"light":"css-variables","dark":"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 **Basic Time-Series** Store sensor readings with timestamps: ```lot wrap theme={"theme":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} DEFINE CONNECTOR 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 (?, ?, ?)" ``` **High-Volume Metrics** Optimized for high ingestion rates: ```lot wrap theme={"theme":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} DEFINE CONNECTOR 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 (?, ?, ?)" ``` **Multiple Tables** Connector to different tables by topic: ```lot wrap theme={"theme":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} DEFINE CONNECTOR 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 (?, ?, ?)" ``` **UPSERT** Insert or ignore on conflict. CrateDB supports `ON CONFLICT DO NOTHING` to skip duplicate inserts for idempotent ingestion: ```lot wrap theme={"theme":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} DEFINE CONNECTOR 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. **UPDATE** Modify existing records when a status change arrives: ```lot wrap theme={"theme":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} DEFINE CONNECTOR 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}'" ``` **DELETE** Remove records on a decommission or cleanup event: ```lot wrap theme={"theme":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} DEFINE CONNECTOR 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}'" ``` > [!NOTE] > > **Alternative: STORE IN with Models** — Instead of writing EVENT queries, you can bind a [model](/latest/lot-language/models/overview) directly to this connector. Every `PUBLISH MODEL` call automatically inserts a row — no query needed: > > ```lot wrap theme={"theme":{"light":"css-variables","dark":"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":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} DEFINE CONNECTOR 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. > [!NOTE] > > CrateDB makes new rows visible to reads about once a second. A `SELECT` event that runs right after an insert may not see that row yet. Run `REFRESH TABLE ` first when you need 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":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} DEFINE CONNECTOR 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":{"light":"css-variables","dark":"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 > **Connection Failed** > > * Verify HOST and PORT are correct > * Check CrateDB is running and accepting connections > * Verify firewall allows connections on port 5432 > **Authentication Failed** > > * Verify USERNAME and PASSWORD are correct > * Check user has permissions on the DATABASE > **Query Errors** > > * Verify table exists and has correct schema > * Check column types match inserted data *** ## Next Steps > **[Data Storage Connectors Overview](./overview)** > > Compare all storage options. > **[Snowflake Connector](./snowflake)** > > Configure Snowflake cloud data warehouse. # File Storage Connector 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` connector writes MQTT messages to local CSV or JSON files. It provides simple persistence for logging, backups, and data export without requiring a database server. > [!NOTE] > > 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":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} DEFINE CONNECTOR DataLogger WITH TYPE FILE_STORAGE ADD FILE_STORAGE_CONFIG WITH STORAGE_DIR "/data/logs" WITH FORMAT "CSV" WITH FILE_PATTERN "{0}.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 **STORAGE\_DIR** `string` *required* Directory path for storing files. > **Storage Location Options** > > **FILE\_PATTERN** `string` > > File name pattern. `{0}` is replaced with the table name. Default: `{0}.csv` for CSV and `{0}.json` for JSON. > > `{0}` is the only placeholder. Others, such as `{date}`, are not supported: the connector fails when it writes. For daily files, publish to a table name that includes the date. > **Format** > > **FORMAT** `string` > > File format: `CSV` or `JSON`. Default: JSON. > > **CSV\_DELIMITER** `string` > > CSV delimiter character. Default: comma (`,`). > **Write Options** > > **APPEND** `boolean` > > Append to existing files. Default: true. > > **INCLUDE\_TIMESTAMP** `boolean` > > Auto-add timestamp field. Default: false. > > **MAX\_RECORDS** `integer` > > JSON only: the most records a file keeps. Older records are dropped once the file is full. Default: 10000. Files are not rotated. > **Timestamp** > > **DATE\_FORMAT** `string` > > Timestamp format string. Default: `yyyy-MM-dd HH:mm:ss`. > [!WARNING] > > **Use environment variables for paths in production.** Keep deployment-specific values out of your connector definitions: > > ```lot wrap theme={"theme":{"light":"css-variables","dark":"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 connectors use the CLEAN format: ```lot wrap theme={"theme":{"light":"css-variables","dark":"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 **CSV Logging** Log sensor data to daily CSV files: ```lot wrap theme={"theme":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} DEFINE CONNECTOR CSVLogger WITH TYPE FILE_STORAGE ADD FILE_STORAGE_CONFIG WITH STORAGE_DIR "/data/logs" WITH FORMAT "CSV" WITH FILE_PATTERN "{0}.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` **JSON Storage** Store events as JSON lines: ```lot wrap theme={"theme":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} DEFINE CONNECTOR 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}}" ``` **Multiple Files** Connector to different files by data type: ```lot wrap theme={"theme":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} DEFINE CONNECTOR MultiFileLogger WITH TYPE FILE_STORAGE ADD FILE_STORAGE_CONFIG WITH STORAGE_DIR "/data/logs" WITH FORMAT "CSV" WITH FILE_PATTERN "{0}.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` **Backup Storage** Create a rolling backup file that keeps the newest records: ```lot wrap theme={"theme":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} DEFINE CONNECTOR BackupStorage WITH TYPE FILE_STORAGE ADD FILE_STORAGE_CONFIG WITH STORAGE_DIR "/backup/mqtt" WITH FORMAT "JSON" WITH FILE_PATTERN "{0}.json" 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}}" ``` > [!NOTE] > > **Alternative: STORE IN with Models** — Instead of writing EVENT queries, you can bind a [model](/latest/lot-language/models/overview) directly to this connector. Every `PUBLISH MODEL` call automatically inserts a row — no query needed: > > ```lot wrap theme={"theme":{"light":"css-variables","dark":"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":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} DEFINE CONNECTOR DataLogger WITH TYPE FILE_STORAGE ADD FILE_STORAGE_CONFIG WITH STORAGE_DIR "/data/logs" WITH FORMAT "CSV" WITH FILE_PATTERN "{0}.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` | `{0}` is the only supported placeholder. A pattern with others, such as `{date}`, fails when the connector writes. *** ## Output Examples ### CSV Output ```csv wrap theme={"theme":{"light":"css-variables","dark":"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":{"light":"css-variables","dark":"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 > **Permission Denied** > > * Verify STORAGE\_DIR exists > * Check write permissions on directory > * Ensure Coreflux process has access > **File Not Created** > > * Verify STORAGE\_DIR path is correct > * Check disk space is available > * Ensure directory is writable > **Data Not Appearing** > > * Verify SOURCE\_TOPIC matches incoming messages > * Check QUERY syntax is correct > * Verify APPEND is set correctly > **Connector fails to write** > > * `FILE_PATTERN` accepts only `{0}` (the table name). Remove other placeholders such as `{date}`. > * For JSON, `MAX_RECORDS` caps the file and drops the oldest records. Files are not rotated. *** ## Next Steps > **[Data Storage Connectors Overview](./overview)** > > Compare all storage options. > **[PostgreSQL Connector](./postgresql)** > > Configure PostgreSQL database storage. # MariaDB Connector 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` connector stores MQTT messages in MariaDB databases. MariaDB is a MySQL-compatible database with enhanced features and performance optimizations. > [!NOTE] > > MariaDB offers drop-in MySQL compatibility with additional features like improved replication and storage engines. Use the same configuration as MySQL with the `MARIADB` connector type. ## Basic Syntax ```lot wrap theme={"theme":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} DEFINE CONNECTOR 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 **SERVER** `string` *required* MariaDB server hostname or IP address. **DATABASE** `string` *required* Target database name. **USERNAME** `string` *required* Database username. **PASSWORD** `string` *required* Database password. > **Optional Settings** > > **PORT** `integer` > > MariaDB port. Default: 3306. > > **USE\_SSL** `boolean` > > Enable SSL connection. Default: false. > [!WARNING] > > **Never hardcode credentials in production.** Use environment variables and encrypted secrets to keep sensitive values out of your connector definitions: > > ```lot wrap theme={"theme":{"light":"css-variables","dark":"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 **Basic Storage** Store sensor data in MariaDB: ```lot wrap theme={"theme":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} DEFINE CONNECTOR 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}')" ``` **With SSL** Secure connection: ```lot wrap theme={"theme":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} DEFINE CONNECTOR 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}')" ``` **UPSERT** 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":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} DEFINE CONNECTOR 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()" ``` **UPDATE** Modify existing records when a status change arrives: ```lot wrap theme={"theme":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} DEFINE CONNECTOR 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}'" ``` **DELETE** Remove records on a decommission or cleanup event: ```lot wrap theme={"theme":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} DEFINE CONNECTOR 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" ``` > [!NOTE] > > **Alternative: STORE IN with Models** — Instead of writing EVENT queries, you can bind a [model](/latest/lot-language/models/overview) directly to this connector. Every `PUBLISH MODEL` call automatically inserts a row — no query needed: > > ```lot wrap theme={"theme":{"light":"css-variables","dark":"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":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} DEFINE CONNECTOR 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":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} DEFINE CONNECTOR 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 > **Connection Refused** > > * Verify SERVER and PORT are correct > * Check MariaDB is running and accepting connections > * Verify firewall allows connections on port 3306 > **Authentication Failed** > > * Verify USERNAME and PASSWORD are correct > * Check user has permissions on the DATABASE > * Verify user can connect from your host *** ## Next Steps > **[Data Storage Connectors Overview](./overview)** > > Compare all storage options. > **[SQL Server Connector](./sqlserver)** > > Configure Microsoft SQL Server storage. # MongoDB Connector 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` connector 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. > [!NOTE] > > 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":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} DEFINE CONNECTOR 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 **CONNECTION\_STRING** `string` *required* MongoDB connection URI. Format: `mongodb://user:password@host:port/database?options` > **Optional Settings** > > **DATABASE** `string` > > Target database name (can also be specified in connection string). > [!WARNING] > > **Never hardcode credentials in production.** Use environment variables and encrypted secrets to keep sensitive values out of your connector definitions: > > ```lot wrap theme={"theme":{"light":"css-variables","dark":"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 connectors use the CLEAN format for simplified document insertion: ```lot wrap theme={"theme":{"light":"css-variables","dark":"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 **SOURCE\_TOPIC** `string` *required* MQTT topic pattern that triggers the INSERT. Supports wildcards. **QUERY** `string` *required* CLEAN format query for document insertion. > **Optional Settings** > > **DESTINATION\_TOPIC** `string` > > MQTT topic to publish operation status. *** ## Writing Data **Basic Document Storage** Store sensor readings as documents: ```lot wrap theme={"theme":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} DEFINE CONNECTOR 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}}}" ``` **Structured Documents** Create structured documents with specific fields: ```lot wrap theme={"theme":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} DEFINE CONNECTOR 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":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} {"temperature": 23.5, "humidity": 65} ``` **Stored document:** ```json wrap theme={"theme":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} { "sensor_id": "temp001", "temperature": 23.5, "humidity": 65, "recorded_at": "2025-01-15T10:30:00Z" } ``` **Multiple Collections** Connector to different collections based on topic: ```lot wrap theme={"theme":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} DEFINE CONNECTOR 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}}}" ``` **Replica Set** Connect to a MongoDB replica set: ```lot wrap theme={"theme":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} DEFINE CONNECTOR 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}}}" ``` > [!NOTE] > > **Alternative: STORE IN with Models** — Instead of writing EVENT queries, you can bind a [model](/latest/lot-language/models/overview) directly to this connector. Every `PUBLISH MODEL` call automatically inserts a row — no query needed: > > ```lot wrap theme={"theme":{"light":"css-variables","dark":"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":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} DEFINE CONNECTOR 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 > **Connection Failed** > > * 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 > **Authentication Failed** > > * Verify username and password in connection string > * Check authSource parameter matches authentication database > * Verify user has permissions on target database > **CLEAN Query Errors** > > * Verify collection name is valid > * Check placeholder names match topic structure > * Ensure document structure is valid JSON *** ## Next Steps > **[Data Storage Connectors Overview](./overview)** > > Compare all storage options. > **[OpenSearch Connector](./opensearch)** > > Configure OpenSearch for search and analytics. # MySQL Connector 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` connector stores MQTT messages in MySQL databases. It supports SSL connections, parameterized queries, and automatic reconnection for reliable data persistence. > [!NOTE] > > 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":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} DEFINE CONNECTOR 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 **SERVER** `string` *required* MySQL server hostname or IP address. **DATABASE** `string` *required* Target database name. **USERNAME** `string` *required* Database username. **PASSWORD** `string` *required* Database password. > **Optional Settings** > > **PORT** `integer` > > MySQL port. Default: 3306. > > **USE\_SSL** `boolean` > > Enable SSL connection. Default: false. > > **CONNECTION\_TIMEOUT** `integer` > > Connection timeout in seconds. Default: 30. > [!WARNING] > > **Never hardcode credentials in production.** Use environment variables and encrypted secrets to keep sensitive values out of your connector definitions: > > ```lot wrap theme={"theme":{"light":"css-variables","dark":"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 **SOURCE\_TOPIC** `string` *required* MQTT topic pattern that triggers the INSERT. Supports wildcards. **QUERY** `string` *required* SQL INSERT statement with placeholders for dynamic values. > **Optional Settings** > > **DESTINATION\_TOPIC** `string` > > 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 **Basic Sensor Storage** Store sensor readings with timestamps: ```lot wrap theme={"theme":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} DEFINE CONNECTOR 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})" ``` **With SSL** Secure connection with SSL: ```lot wrap theme={"theme":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} DEFINE CONNECTOR 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}')" ``` **Multiple Events** Different tables for different data types: ```lot wrap theme={"theme":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} DEFINE CONNECTOR 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}')" ``` **UPSERT** Insert a record or update it if a duplicate key exists. MySQL uses `ON DUPLICATE KEY UPDATE`: ```lot wrap theme={"theme":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} DEFINE CONNECTOR 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. **UPDATE** Modify existing records when a status or value change arrives: ```lot wrap theme={"theme":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} DEFINE CONNECTOR 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}'" ``` **DELETE** Remove records on a decommission or cleanup event: ```lot wrap theme={"theme":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} DEFINE CONNECTOR 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" ``` > [!NOTE] > > **Alternative: STORE IN with Models** — Instead of writing EVENT queries, you can bind a [model](/latest/lot-language/models/overview) directly to this connector. Every `PUBLISH MODEL` call automatically inserts a row — no query needed: > > ```lot wrap theme={"theme":{"light":"css-variables","dark":"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":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} DEFINE CONNECTOR 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":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} DEFINE CONNECTOR 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":{"light":"css-variables","dark":"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 > **Connection Refused** > > * 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 > **Authentication Failed** > > * Verify USERNAME and PASSWORD are correct > * Check user has permissions on the DATABASE > * Verify user can connect from your host > **Query Syntax Errors** > > * Check placeholder names match topic structure > * Verify table and column names exist > * Test query manually in MySQL client first *** ## Next Steps > **[Data Storage Connectors Overview](./overview)** > > Compare all storage options. > **[MariaDB Connector](./mariadb)** > > Configure MariaDB storage (MySQL-compatible). # OpenSearch Connector 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` connector 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. > [!NOTE] > > 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":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} DEFINE CONNECTOR 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 **BASE\_URL** `string` *required* OpenSearch endpoint URL including port (e.g., `https://opensearch.example.com:9200`). **USERNAME** `string` *required* OpenSearch username. **PASSWORD** `string` *required* OpenSearch password. > **SSL/Security** > > **USE\_SSL** `boolean` > > Enable HTTPS. Default: false. > > **IGNORE\_CERT\_ERRORS** `boolean` > > Ignore certificate validation errors. Default: false. > [!WARNING] > > **Never hardcode credentials in production.** Use environment variables and encrypted secrets to keep sensitive values out of your connector definitions: > > ```lot wrap theme={"theme":{"light":"css-variables","dark":"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 connectors use the CLEAN format for document indexing: ```lot wrap theme={"theme":{"light":"css-variables","dark":"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 **Basic Indexing** Index sensor data for search: ```lot wrap theme={"theme":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} DEFINE CONNECTOR 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}}}" ``` **Log Aggregation** Aggregate application logs: ```lot wrap theme={"theme":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} DEFINE CONNECTOR 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}}}" ``` **Time-Series Index** Use date-based index names for time-series: ```lot wrap theme={"theme":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} DEFINE CONNECTOR 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. **Self-Signed Certs** Connect with certificate validation disabled: ```lot wrap theme={"theme":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} DEFINE CONNECTOR 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}}}" ``` > [!WARNING] > > Only use IGNORE\_CERT\_ERRORS in development environments. > [!NOTE] > > **Alternative: STORE IN with Models** — Instead of writing EVENT queries, you can bind a [model](/latest/lot-language/models/overview) directly to this connector. Every `PUBLISH MODEL` call automatically inserts a row — no query needed: > > ```lot wrap theme={"theme":{"light":"css-variables","dark":"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":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} DEFINE CONNECTOR 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":{"light":"css-variables","dark":"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 > **Connection Failed** > > * Verify BASE\_URL is correct including port > * Check OpenSearch is running and accessible > * Verify firewall allows connections on port 9200 > **Authentication Failed** > > * Verify USERNAME and PASSWORD are correct > * Check user has permissions to index documents > **SSL/Certificate Errors** > > * Ensure USE\_SSL matches the endpoint (http vs https) > * For self-signed certs, set IGNORE\_CERT\_ERRORS "true" > * Verify certificate chain is complete > **Index Errors** > > * Verify index name is valid (lowercase, no special chars) > * Check field mappings don't conflict > * Review OpenSearch logs for detailed errors *** ## Next Steps > **[Data Storage Connectors Overview](./overview)** > > Compare all storage options. > **[CrateDB Connector](./cratedb)** > > Configure CrateDB for time-series data. # Data Storage Connectors 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 connectors 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. > [!TIP] > > **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 > **[PostgreSQL](./postgresql)** > > Open-source relational database for structured data storage. > **[MySQL](./mysql)** > > Popular relational database with wide ecosystem support. > **[MariaDB](./mariadb)** > > MySQL-compatible database with enhanced features. > **[SQL Server](./sqlserver)** > > Microsoft SQL Server for enterprise environments. > **[MongoDB](./mongodb)** > > Document database for flexible JSON storage. > **[OpenSearch](./opensearch)** > > Real-time analytics, search, and visualization. > **[CrateDB](./cratedb)** > > Distributed SQL optimized for time-series data. > **[Snowflake](./snowflake)** > > Cloud data warehouse for large-scale analytics. > **[File Storage](./file-storage)** > > 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 | > [!NOTE] > > **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 Connector A data storage connector 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 connector connects to a PostgreSQL database and automatically stores every sensor reading that arrives on `sensors/+/reading`: ```lot wrap theme={"theme":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} DEFINE CONNECTOR 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 Connector Connection Every data storage connector starts with a connection configuration. Define the connector type and provide the connection details for your database: ```lot wrap theme={"theme":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} DEFINE CONNECTOR 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" ``` > [!NOTE] > > Each database type has its own config block (`SQL_CONFIG`, `MONGODB_CONFIG`, `OPENSEARCH_CONFIG`, etc.). See the specific database page for exact parameters. > [!WARNING] > > 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":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} > DEFINE CONNECTOR 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: **With Models (STORE IN)** The `STORE IN` approach ties a model directly to a database connector. 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":{"light":"css-variables","dark":"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" ``` **Store only some records.** Add `WHEN` with a condition on the model's fields. The row is inserted only when the condition is true: ```lot wrap focus={8} theme={"theme":{"light":"css-variables","dark":"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" WHEN {count} > 0 ``` Conditions support comparisons, `AND`/`OR`, string comparisons, and nested fields such as `{data.temperature}`. A field the model doesn't define is rejected at deploy. **2. Define the connector the model stores into:** ```lot wrap theme={"theme":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} DEFINE CONNECTOR 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":{"light":"css-variables","dark":"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. **With Events (INSERT)** 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 connector:** ```lot wrap theme={"theme":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} DEFINE CONNECTOR 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":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} DEFINE CONNECTOR 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 connectors: | 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":{"light":"css-variables","dark":"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":{"light":"css-variables","dark":"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 > **Trigger & Routing** > > **SOURCE\_TOPIC** `string` *required* > > 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}`. > > **DESTINATION\_TOPIC** `string` > > 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). > **Query** > > **QUERY** `string` *required* > > SQL statement or CLEAN format query with placeholders. Supports both write operations (`INSERT INTO ...`) and read operations (`SELECT ... FROM ...`). *** ## Best Practices > **Use SSL and Certificates in Production** > > Always enable SSL for database connections. Most database connectors support SSL configuration through the config block: > > ```lot wrap theme={"theme":{"light":"css-variables","dark":"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. > **Secure Your Credentials** > > Never hardcode passwords or API keys in connector 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 connector. > > See [Environment Variables and Secrets](/latest/mqtt-broker/secrets-and-env) for full setup and deployment instructions. > **Monitor Database Size** > > Implement data retention policies: > > * Index lifecycle management > * Time-based partitioning > * Automatic cleanup jobs > **Use STORE IN for Automatic Persistence** > > 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":{"light":"css-variables","dark":"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" > ``` > **Use Connection Pooling** > > For high-throughput scenarios, configure connection pools where supported. *** ## Next Steps Choose the database that matches your requirements: > **[PostgreSQL](./postgresql)** > > Start with the most popular open-source database. > **[MongoDB](./mongodb)** > > Store flexible JSON documents. # PostgreSQL Connector 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` connector stores MQTT messages in PostgreSQL databases. It supports SSL connections, parameterized queries, and automatic reconnection for reliable data persistence. Because the connector speaks the **PostgreSQL wire protocol**, it also works with any service built on PostgreSQL — you use the same `POSTGRESQL` connector 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 | > [!NOTE] > > 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. > [!TIP] > > 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 connector executes your `QUERY` strings against that database. ## Basic Syntax ```lot wrap theme={"theme":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} DEFINE CONNECTOR 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 **SERVER** `string` *required* PostgreSQL server hostname or IP address. **DATABASE** `string` *required* Target database name. **USERNAME** `string` *required* Database username. **PASSWORD** `string` *required* Database password. > **Optional Settings** > > **PORT** `integer` > > PostgreSQL port. Default: 5432. > **SSL/Security** > > **USE\_SSL** `boolean` > > Enable SSL connection. Default: false. > > **TRUST\_SERVER\_CERTIFICATE** `boolean` > > Trust server certificate without validation. Default: false. > **Timeouts** > > **CONNECTION\_TIMEOUT** `integer` > > Connection timeout in seconds. Default: 30. > > **COMMAND\_TIMEOUT** `integer` > > Command timeout in seconds. Default: 30. > [!WARNING] > > **Never hardcode credentials in production.** Use environment variables and encrypted secrets to keep sensitive values out of your connector definitions: > > ```lot wrap theme={"theme":{"light":"css-variables","dark":"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 **SOURCE\_TOPIC** `string` *required* MQTT topic pattern that triggers the INSERT. Supports wildcards. **QUERY** `string` *required* SQL INSERT statement with placeholders for dynamic values. > **Optional Settings** > > **DESTINATION\_TOPIC** `string` > > 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 **Basic Sensor Storage** Store temperature readings with timestamps: ```lot wrap theme={"theme":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} DEFINE CONNECTOR 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":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} {"value": 23.5, "unit": "celsius"} ``` **With SSL** Secure connection with SSL: ```lot wrap theme={"theme":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} DEFINE CONNECTOR 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}')" ``` **Multiple Events** Different tables for different data types: ```lot wrap theme={"theme":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} DEFINE CONNECTOR 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}')" ``` **JSON Column** Store full payload in JSONB column: ```lot wrap theme={"theme":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} DEFINE CONNECTOR 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":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} SELECT * FROM device_data WHERE payload->>'temperature' > '25'; ``` **UPSERT** 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":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} DEFINE CONNECTOR 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. **UPDATE** Modify existing records when a status change or update arrives: ```lot wrap theme={"theme":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} DEFINE CONNECTOR 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. **DELETE** Remove records when a decommission or cleanup event arrives: ```lot wrap theme={"theme":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} DEFINE CONNECTOR 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. > [!NOTE] > > **Alternative: STORE IN with Models** — Instead of writing EVENT queries, you can bind a [model](/latest/lot-language/models/overview) directly to this connector. Every `PUBLISH MODEL` call automatically inserts a row — no query needed: > > ```lot wrap theme={"theme":{"light":"css-variables","dark":"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":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} DEFINE CONNECTOR 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":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} DEFINE CONNECTOR 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":{"light":"css-variables","dark":"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 > **Connection Refused** > > * 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 > **Authentication Failed** > > * Verify USERNAME and PASSWORD are correct > * Check user has permissions on the DATABASE > * Verify authentication method in `pg_hba.conf` > **SSL Connection Errors** > > * Ensure USE\_SSL matches server requirements > * For self-signed certs, set TRUST\_SERVER\_CERTIFICATE "true" > * Verify SSL is enabled on PostgreSQL server > **Query Syntax Errors** > > * Check placeholder names match topic structure > * Verify table and column names exist > * Test query manually in psql first > * Ensure proper escaping for special characters > **Timeout Errors** > > * Increase CONNECTION\_TIMEOUT for slow networks > * Increase COMMAND\_TIMEOUT for complex queries > * Check server load and query performance *** ## Next Steps > **[Data Storage Connectors Overview](./overview)** > > Compare all storage options. > **[MySQL Connector](./mysql)** > > Configure MySQL database storage. # Snowflake Connector 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` connector stores MQTT messages in Snowflake, a cloud-native data warehouse. It supports high-volume data ingestion with automatic scaling and powerful SQL analytics. > [!NOTE] > > 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":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} DEFINE CONNECTOR 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 **CONNECTION\_STRING** `string` *required* Snowflake connection string with account, user, and password. > **Connection Options** > > **WAREHOUSE** `string` > > Snowflake compute warehouse name. > > **DATABASE** `string` > > Target database name. > > **SCHEMA** `string` > > Target schema name. Default: PUBLIC. > **Authentication** > > **ROLE** `string` > > Snowflake user role. > **Performance** > > **QUERY\_TIMEOUT** `integer` > > Query timeout in seconds. Default: 60. > > **POOL\_SIZE** `integer` > > Connection pool size. Default: 10. > [!WARNING] > > **Never hardcode credentials in production.** Use environment variables and encrypted secrets to keep sensitive values out of your connector definitions: > > ```lot wrap theme={"theme":{"light":"css-variables","dark":"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 **Basic Storage** Store sensor data in Snowflake: ```lot wrap theme={"theme":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} DEFINE CONNECTOR 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}'))" ``` **With Connection Pool** Optimized for high throughput: ```lot wrap theme={"theme":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} DEFINE CONNECTOR 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}'))" ``` **Multiple Tables** Connector to different tables: ```lot wrap theme={"theme":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} DEFINE CONNECTOR 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}'))" ``` **UPSERT** Snowflake uses `MERGE INTO` to insert or update depending on whether a matching row exists: ```lot wrap theme={"theme":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} DEFINE CONNECTOR 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())" ``` **UPDATE** Modify existing records when a status change arrives: ```lot wrap theme={"theme":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} DEFINE CONNECTOR 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}'" ``` **DELETE** Remove records on a decommission or cleanup event: ```lot wrap theme={"theme":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} DEFINE CONNECTOR 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" ``` > [!NOTE] > > **Alternative: STORE IN with Models** — Instead of writing EVENT queries, you can bind a [model](/latest/lot-language/models/overview) directly to this connector. Every `PUBLISH MODEL` call automatically inserts a row — no query needed: > > ```lot wrap theme={"theme":{"light":"css-variables","dark":"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":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} DEFINE CONNECTOR 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":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} DEFINE CONNECTOR 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":{"light":"css-variables","dark":"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 > **Connection Failed** > > * Verify account identifier is correct > * Check username and password > * Ensure warehouse exists and is running > * Verify network allows Snowflake connections > **Permission Errors** > > * Verify ROLE has INSERT permissions > * Check user has access to WAREHOUSE > * Ensure user can access DATABASE and SCHEMA > **Query Timeout** > > * Increase QUERY\_TIMEOUT value > * Check warehouse size is adequate > * Verify warehouse is not suspended *** ## Next Steps > **[Data Storage Connectors Overview](./overview)** > > Compare all storage options. > **[File Storage Connector](./file-storage)** > > Configure local file storage. # SQL Server Connector 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` connector stores MQTT messages in Microsoft SQL Server databases. It supports Windows and SQL authentication, SSL connections, and enterprise features. > [!NOTE] > > 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":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} DEFINE CONNECTOR 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 **SERVER** `string` *required* SQL Server hostname or IP address. Can include instance name (e.g., `server\instance`). **DATABASE** `string` *required* Target database name. **USERNAME** `string` *required* SQL Server username. **PASSWORD** `string` *required* SQL Server password. > **Optional Settings** > > **PORT** `integer` > > SQL Server port. Default: 1433. > > **USE\_SSL** `boolean` > > Enable SSL connection. Default: false. > > **TRUST\_SERVER\_CERTIFICATE** `boolean` > > Trust server certificate without validation. Default: false. > [!WARNING] > > **Never hardcode credentials in production.** Use environment variables and encrypted secrets to keep sensitive values out of your connector definitions: > > ```lot wrap theme={"theme":{"light":"css-variables","dark":"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 **Basic Storage** Store sensor data in SQL Server: ```lot wrap theme={"theme":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} DEFINE CONNECTOR 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}')" ``` **With SSL** Secure connection with trusted certificate: ```lot wrap theme={"theme":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} DEFINE CONNECTOR 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}')" ``` **Named Instance** Connect to a named SQL Server instance: ```lot wrap theme={"theme":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} DEFINE CONNECTOR 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}')" ``` **UPSERT** 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":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} DEFINE CONNECTOR 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());" ``` **UPDATE** Modify existing records when a status change arrives: ```lot wrap theme={"theme":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} DEFINE CONNECTOR 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}'" ``` **DELETE** Remove records on a decommission or cleanup event: ```lot wrap theme={"theme":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} DEFINE CONNECTOR 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" ``` > [!NOTE] > > **Alternative: STORE IN with Models** — Instead of writing EVENT queries, you can bind a [model](/latest/lot-language/models/overview) directly to this connector. Every `PUBLISH MODEL` call automatically inserts a row — no query needed: > > ```lot wrap theme={"theme":{"light":"css-variables","dark":"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":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} DEFINE CONNECTOR 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":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} DEFINE CONNECTOR 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":{"light":"css-variables","dark":"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 > **Connection Refused** > > * 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 > **Authentication Failed** > > * Verify USERNAME and PASSWORD are correct > * Ensure SQL Server authentication is enabled (mixed mode) > * Check user has permissions on the DATABASE > **SSL/Certificate Errors** > > * Set TRUST\_SERVER\_CERTIFICATE "true" for self-signed certs > * Verify SSL is configured on SQL Server *** ## Next Steps > **[Data Storage Connectors Overview](./overview)** > > Compare all storage options. > **[MongoDB Connector](./mongodb)** > > Configure MongoDB document storage. # ADS Connector (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` connector 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. > [!TIP] > > ADS provides direct, high-performance access to TwinCAT variables. The AutoDiscovery feature can automatically find and expose all PLC symbols as MQTT topics. > [!WARNING] > > The ADS connector is on beta testing. It is planned for a future release. ## Basic Syntax ```lot wrap theme={"theme":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} DEFINE CONNECTOR BeckhoffPLC WITH TYPE ADS ADD ADS_CONFIG WITH TARGET_NET_ID "192.168.1.100.1.1" WITH TARGET_IP "192.168.1.100" WITH TARGET_PORT 851 ADD MAPPING ProcessData WITH EVERY 500 MILLISECONDS ADD TAG Temperature WITH ADDRESS "GVL_Process.rTemperature" WITH DATA_TYPE "REAL" WITH SOURCE_TOPIC "plc/temperature" ``` Set the PLC's AMS Net ID, IP, and ADS port in `ADS_CONFIG`. The broker connects through the AMS router on the broker host, so create that route on the router or the PLC. See [AMS route setup](#ams-route-setup). *** ## AMS Net ID Basics ADS uses an AMS Net ID for the broker host and one for the PLC. Set the broker host's id on the AMS router. An 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) | > [!NOTE] > > You can find your TwinCAT system's AMS Net ID in TwinCAT XAE under **SYSTEM** → **Connectors** or by running `adsInfo` on the target. *** ## Connection Configuration ### ADS\_CONFIG Parameters **LOCAL\_NET\_ID** `string` Not required. Accepted and not used yet. **LOCAL\_ROUTE\_TO\_TARGET** `string` Not required. Accepted and not used yet. **LOCAL\_HOST\_NAME** `string` Local host name (optional). **TARGET\_NET\_ID** `string` *required* Target PLC's AMS Net ID (format: x.x.x.x.x.x). **TARGET\_IP** `string` *required* Target PLC's IP address. **TARGET\_PORT** `integer` ADS port. Typically 851 for TwinCAT 3, 801 for TwinCAT 2. **TARGET\_ROUTE\_TO\_LOCAL** `string` Not required. Accepted and not used yet. **TARGET\_USER** `string` Not required. Accepted and not used yet. **TARGET\_PASSWORD** `string` Not required. Accepted and not used yet. > [!NOTE] > > The broker connects through the AMS router on the broker host. Create the AMS route on the router or the PLC yourself. `LOCAL_NET_ID`, `LOCAL_ROUTE_TO_TARGET`, `TARGET_ROUTE_TO_LOCAL`, `TARGET_USER`, and `TARGET_PASSWORD` do not create that route. **TIMEOUT** `integer` Limit for each ADS request, in milliseconds. Default: 5000. A value of 0 or below falls back to 5000. > [!NOTE] > > A `SYMBOL` read is 1 request by name. A `SYMBOL` write takes 3 round trips (create handle, write, release handle), so it can take up to 3 × `TIMEOUT` plus a margin of at least 1 s. `INDEX` reads and writes are 1 request. When an ADS error means the PLC is unreachable, such as a timeout or target not found, the route goes to **Reconnecting** instead of staying Connected with BAD tags. See [Connection status](/latest/lot-language/routes/industrial/overview#connection-status). ### Connection Example ```lot wrap theme={"theme":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} ADD ADS_CONFIG WITH TARGET_NET_ID "192.168.1.100.1.1" WITH TARGET_IP "192.168.1.100" WITH TARGET_PORT 851 WITH TIMEOUT 5000 ``` *** ## AutoDiscovery AutoDiscovery automatically finds all PLC symbols and exposes them as MQTT topics. ### AutoDiscovery Parameters **AUTO\_DISCOVERY** `boolean` Enable automatic symbol discovery. Default: false. **DISCOVERY\_BASE\_TOPIC** `string` Base MQTT topic for discovered symbols. Default: ads/discovery. **DISCOVERY\_DEFAULT\_QOS** `integer` Default QoS for discovered symbols (0-2). Default: 0. **DISCOVERY\_DEFAULT\_RETAIN** `boolean` Default retain flag for discovered symbols. Default: false. **DISCOVERY\_DEFAULT\_TRIGGER** `integer` Default trigger mode: * `0` - OnStart (read once at connection) * `1` - OnChange (publish on value change) * `2` - Cyclic (poll at interval) Default: 2. **DISCOVERY\_DEFAULT\_CYCLE** `integer` Default polling cycle in milliseconds. Default: 1000. **DISCOVERY\_FILTER\_INCLUDES** `string` Include patterns (comma-separated, \* wildcard). Default: \* (all symbols). **DISCOVERY\_FILTER\_EXCLUDES** `string` Exclude patterns (comma-separated, \* wildcard). **DISCOVERY\_MAX\_DEPTH** `integer` Structure expansion depth. Default: 3. **DISCOVERY\_PARALLEL\_READING** `boolean` Enable parallel reading for large projects. Default: false. **DISCOVERY\_BATCH\_SIZE** `integer` Batch size for parallel reading. Default: 50. ### AutoDiscovery Example ```lot wrap theme={"theme":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} DEFINE CONNECTOR BeckhoffAutoDiscover WITH TYPE ADS ADD ADS_CONFIG WITH TARGET_NET_ID "192.168.1.100.1.1" WITH TARGET_IP "192.168.1.100" WITH TARGET_PORT 851 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":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} DEFINE CONNECTOR TwinCATStrings WITH TYPE ADS ADD ADS_CONFIG WITH TARGET_NET_ID "192.168.1.100.1.1" WITH TARGET_IP "192.168.1.100" WITH TARGET_PORT 851 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":{"light":"css-variables","dark":"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":{"light":"css-variables","dark":"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** `string` Array element type (required for ARRAY): BOOL, INT, REAL, STRING, STRUCT, etc. **ARRAY\_SIZE** `integer` Number of elements in the array. **ARRAY\_ELEMENT\_LENGTH** `integer` Length of each element in bytes (for STRING/STRUCT arrays). **ARRAY\_START\_OFFSET** `integer` Offset in bytes to skip before array data. Default: 0. **ARRAY\_ELEMENT\_HEADER\_SIZE** `integer` Header bytes to skip per element. Default: 0. ### Structure Handling **STRUCT\_FIELD\_DEFINITIONS** `string` Structure field definitions in format: `name:offset:type:size` (comma-separated). ### String sizing parameter **LENGTH** `integer` 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 connector 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":{"light":"css-variables","dark":"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":{"light":"css-variables","dark":"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 **Basic Symbol Reading** Read PLC variables using symbol names: ```lot wrap theme={"theme":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} DEFINE CONNECTOR TwinCATBasic WITH TYPE ADS ADD ADS_CONFIG WITH TARGET_NET_ID "192.168.1.100.1.1" WITH TARGET_IP "192.168.1.100" WITH TARGET_PORT 851 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" ``` **AutoDiscovery** Automatically discover and expose all GVL variables: ```lot wrap theme={"theme":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} DEFINE CONNECTOR TwinCATAutoDiscover WITH TYPE ADS ADD ADS_CONFIG WITH TARGET_NET_ID "192.168.1.100.1.1" WITH TARGET_IP "192.168.1.100" WITH TARGET_PORT 851 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 ``` **Bidirectional Control** Read process values and write setpoints: ```lot wrap theme={"theme":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} DEFINE CONNECTOR TwinCATControl WITH TYPE ADS ADD ADS_CONFIG WITH TARGET_NET_ID "192.168.1.100.1.1" WITH TARGET_IP "192.168.1.100" WITH TARGET_PORT 851 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" ``` **Array Data** Read array values: ```lot wrap theme={"theme":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} DEFINE CONNECTOR TwinCATArrays WITH TYPE ADS ADD ADS_CONFIG WITH TARGET_NET_ID "192.168.1.100.1.1" WITH TARGET_IP "192.168.1.100" WITH TARGET_PORT 851 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" ``` **Structure Data** Read structured data: ```lot wrap theme={"theme":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} DEFINE CONNECTOR TwinCATStruct WITH TYPE ADS ADD ADS_CONFIG WITH TARGET_NET_ID "192.168.1.100.1.1" WITH TARGET_IP "192.168.1.100" WITH TARGET_PORT 851 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" ``` **Combined (Cyclic + On-Demand)** Continuous monitoring with on-demand read and write events in the same connector: ```lot wrap theme={"theme":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} DEFINE CONNECTOR FullADSSetup WITH TYPE ADS ADD ADS_CONFIG WITH TARGET_NET_ID "192.168.1.100.1.1" WITH TARGET_IP "192.168.1.100" WITH TARGET_PORT 851 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`. *** ## AMS route setup The broker connects through the AMS router on the broker host. It does not create the route. Add the AMS route on the router or the PLC yourself. ### On the TwinCAT system 1. Open TwinCAT XAE 2. Go to **SYSTEM** → **Connectors** 3. Add a static route to the broker host: * **AMS Net ID**: the broker host's AMS Net ID * **Transport Type**: TCP/IP * **Address**: the broker host's IP address `LOCAL_NET_ID`, `LOCAL_ROUTE_TO_TARGET`, `TARGET_ROUTE_TO_LOCAL`, `TARGET_USER`, and `TARGET_PASSWORD` are not required and are not used yet. *** ## Troubleshooting > **Connection Failed** > > * Verify the target AMS Net ID and the route on the router or the PLC > * Ensure TARGET\_PORT is correct (851 for TC3, 801 for TC2) > * Verify the firewall allows the ADS port (typically 48898) > **Symbol Not Found** > > * 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 > **AutoDiscovery Issues** > > * 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 > **Write Operations Fail** > > * 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 > **Timeout Errors** > > * Increase TIMEOUT value > * Check network connectivity > * Verify TwinCAT runtime is running > * Reduce DISCOVERY\_BATCH\_SIZE if using AutoDiscovery *** ## Next Steps > **[Allen-Bradley](./allen-bradley)** > > Connect to Rockwell Automation PLCs. > **[OPC UA Client](./opcua-client)** > > Cross-platform OPC UA communication. # Allen-Bradley Connector 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` connector 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. > [!TIP] > > **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`. > [!NOTE] > > For **explicit CIP paths** (`4.100.3.0`), use the [EtherNet/IP connector](./ethernetip) instead. This connector is for symbolic tag-name access; EtherNet/IP is for Class/Instance/Attribute addressing. ### Connector 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 **Poll a controller tag** Read a controller-scoped DINT counter every second: ```lot wrap focus={9-13} theme={"theme":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} DEFINE CONNECTOR 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" ``` **Program-scoped tags** Read tags inside a specific program with units and deadband: ```lot wrap focus={9-16} theme={"theme":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} DEFINE CONNECTOR 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" ``` **Read and write** Poll a setpoint and accept MQTT writes to update it: ```lot wrap focus={9-18} theme={"theme":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} DEFINE CONNECTOR 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" ``` **Micro800** Read built-in I/O on a Micro800 controller: ```lot wrap focus={9-14} theme={"theme":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} DEFINE CONNECTOR 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. Honored for every device family. Default is `44818`. | | `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 (`backplane,slot`) for families that use a backplane. Micro800 (`DEVICE_FAMILY 4`) has no backplane: omit `PATH`. If you set one, the broker ignores it and logs a warning. | | `TIMEOUT` | No | `5000` | Per-tag read/write timeout in milliseconds. Default is `5000`. Minimum accepted value is `1000`; values below that fail route add validation. | > [!NOTE] > > The config block may be declared as `ADD ALLEN_BRADLEY_CONFIG` or `ADD AB_CONFIG` — both are equivalent. > [!NOTE] > > The connector reports **Connected** only after the PLC answers. With at least one tag, it opens one tag using the same settings a read uses. With no tags, it checks the EtherNet/IP session. If the controller later drops, the route moves to **Reconnecting** and recovers on its own. > > Each read or write opens the tag and then reads or writes it, and each step has its own `TIMEOUT`. The route waits 2 × `TIMEOUT` plus a margin, at least 1 s, before it gives up. That's about 5 s at `TIMEOUT 2000`. So a dead PLC is detected later than `TIMEOUT` alone suggests, and the first read after a reconnect can take up to twice as long. A write that times out publishes `"success": false` with `"outcome": "unknown"`, because the PLC may still apply it. See [Write results](/latest/lot-language/routes/industrial/overview#write-results). Publish `-checkConnectorConnection ` to `$SYS/Coreflux/Command` to run that same test. ### 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 **ControlLogix / CompactLogix** Standard backplane path to CPU in slot 0: ```lot wrap theme={"theme":{"light":"css-variables","dark":"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** MicroLogix 1100/1400: ```lot wrap theme={"theme":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} ADD ALLEN_BRADLEY_CONFIG WITH IP "192.168.1.50" WITH DEVICE_FAMILY 5 WITH TIMEOUT 5000 ``` **Through backplane** CPU in slot 2 on backplane 1: ```lot wrap theme={"theme":{"light":"css-variables","dark":"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** Micro800 has no backplane, so omit `PATH`. A `PATH` on this family is ignored and logged as a warning: ```lot wrap theme={"theme":{"light":"css-variables","dark":"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":{"light":"css-variables","dark":"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 | Published as the character | | `STRING` | STRING | CIP string with a length prefix — see [STRING encoding](#string-encoding) | | `TIME` | TIME | Duration in ms — published as `T#XdXhXmXsXms` | | `DATE` | DATE | Published as `yyyy-MM-dd` (the PLC stores seconds since 1970) | | `TOD` | TOD | Published as `hh:mm:ss.fff` | > [!NOTE] > > `DATE`, `TOD`, and `WCHAR` publish in these forms on every Allen-Bradley family. Measured on a Micro850: `1790553600` seconds publishes `2026-09-28`, and `65535` ms publishes `00:01:05.535`. Connected Components Workbench has no creatable `CHAR`, `WCHAR`, or `TOD` variable on Micro800, so those types apply to Logix families. > [!WARNING] > > A tag's `DATA_TYPE` must match the PLC tag's actual type. On a mismatch the tag is refused: no value is published, the tag goes BAD, and the log names both types. Route health leaves Green and goes Yellow. The check is strict. A PLC `INT` declared as `DINT` is refused too. That case used to read correctly by luck. ### STRING encoding Writes include the CIP length prefix. The layout depends on `DEVICE_FAMILY`. * **ControlLogix / CompactLogix (default):** fixed 88-byte CIP `STRING` — a 4-byte DINT length prefix plus up to 82 ASCII characters. Neither `LENGTH` nor `STRING_SIZE` resizes this layout. `STRING_SIZE` is accepted as an alias for the same field and has the same limitation. * **Micro800:** a 1-byte length prefix plus the characters. A write can be longer than the value currently stored in the tag. The broker sizes the tag so the new string fits, up to the String Size configured for that tag in Connected Components Workbench (for example, 80 characters). A longer string is rejected with an error instead of being truncated. ### TIME encoding Raw value is a signed 32-bit millisecond count. MQTT publishes Allen-Bradley-style duration strings (for example `T#1h1m1s1ms`). ### Write value formats Publish a payload to `DESTINATION_TOPIC` on a tag with `WRITABLE "true"`. Multi-byte values are written little-endian, the byte order CIP expects. | `DATA_TYPE` | Accepted write value | Example | | - | - | - | | `WORD`, `DWORD`, `LWORD` | Decimal, the same form a read publishes, or hex with a `16#` or `0x` prefix | `500` or `16#01F4` | | `TIME` | A `T#` duration, or a millisecond count. `T#10s` writes 10000 ms | `T#10s` | | `DATE` | An ISO date `yyyy-MM-dd`, written as seconds since 1970-01-01 UTC | `2025-01-01` | | `TOD` | Milliseconds since midnight. A read publishes `hh:mm:ss.fff`; a write does not accept that string | `65535` | | `STRING` | The characters to store. The broker sends the CIP length prefix. On Micro800, see [STRING encoding](#string-encoding) for the String Size limit | `Pump-01` | An on-demand `ADD EVENT` write accepts `T#` durations and sends the `STRING` length prefix. For `WORD`, `DWORD`, `LWORD`, and `DATE`, put the decimal or seconds form in the event `value` field. The `16#` / `0x` and `yyyy-MM-dd` forms apply to writable-tag payloads. *** ## TAG Configuration Every polled data point is an `ADD TAG` inside an `ADD MAPPING` block. ### Complete TAG Example ```lot wrap theme={"theme":{"light":"css-variables","dark":"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 > **Address and data type** > > **ADDRESS** `string` *required* > > PLC tag name — controller-scoped (`MainCounter`), program-scoped (`Program:MainProgram.Counter`), UDT member (`Valve1.Status`), or array (`Temps[0]`). > > **ADDRESS\_TYPE** `string` > > `SYMBOL` (default) or `PATH`. > > **DATA\_TYPE** `string` *required* > > Logix data type: `BOOL`, `SINT`, `INT`, `DINT`, `LINT`, `USINT`, `UINT`, `UDINT`, `ULINT`, `REAL`, `LREAL`, `STRING`, `TIME`, etc. It must match the PLC tag's actual type. A mismatch publishes no value, marks the tag BAD, and route health goes Yellow. See [Data Types](#data-types). > **Value transformation** > > **SCALING** `decimal` > > Multiply the parsed value after read. Alias: `SCALING_FACTOR`. Default: `1.0`. > > **OFFSET** `decimal` > > Add after scaling. Default: `0.0`. > > **UNIT** `string` > > Engineering unit (for example `°C`, `bar`, `RPM`). Included in JSON publish mode. > > **DECIMAL\_PLACES** `integer` > > Rounding precision for float MQTT payloads. Default: `2`. > **Filtering** > > **MIN\_VALUE** `double` > > Clamp minimum allowed value (especially for writable tags). > > **MAX\_VALUE** `double` > > Clamp maximum allowed value. > > **DEADBAND** `double` > > Suppress MQTT publish when the change is below this threshold. Default: `0.0`. > **Publishing and writes** > > **SOURCE\_TOPIC** `string` *required* > > MQTT topic where polled values are published. > > **PUBLISH\_MODE** `string` > > `VALUE_ONLY` (default) or `JSON` (value, unit, quality, timestamp, description). > > **WRITABLE** `boolean` > > When `"true"`, subscribe to `DESTINATION_TOPIC` and write incoming MQTT values to the PLC. > > **DESTINATION\_TOPIC** `string` > > MQTT topic to receive write commands. Required when `WRITABLE` is `"true"`. > > **DESCRIPTION** `string` > > 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":{"light":"css-variables","dark":"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":{"light":"css-variables","dark":"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":{"light":"css-variables","dark":"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 **Multiple data types** Monitor BOOL, DINT, and REAL tags in one mapping: ```lot wrap theme={"theme":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} DEFINE CONNECTOR 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" ``` **UDT members** Read User-Defined Type structure members: ```lot wrap theme={"theme":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} DEFINE CONNECTOR 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" ``` **Array elements** Read specific array indices: ```lot wrap theme={"theme":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} DEFINE CONNECTOR 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" ``` **Control loop** Read process value and write setpoint from MQTT: ```lot wrap theme={"theme":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} DEFINE CONNECTOR 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 ``` **Scaled analog** Scale a 4–20 mA sensor to engineering units: ```lot wrap theme={"theme":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} DEFINE CONNECTOR 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 > **Choose the correct device family** > > 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. > **Program scope** > > Controller-scoped tags use the name alone (`GlobalCounter`). Program-scoped tags need the full prefix: `Program:MainProgram.LocalCounter`. > **PATH vs SLOT** > > Set the CPU slot in `PATH` (`"1,0"` for backplane 1, slot 0). `SLOT` is kept for legacy parity, but routing is resolved from `PATH`. Micro800 (`DEVICE_FAMILY 4`) has no backplane: omit `PATH`. A `PATH` on a Micro800 route is ignored and logged as a warning. > **Writable tags** > > Use `WRITABLE "true"` with `MIN_VALUE` / `MAX_VALUE` on setpoints. Publish values in the forms under [Write value formats](#write-value-formats). Ensure the PLC is in Remote Run mode for writes to succeed. > **Deadbands and polling** > > 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 > **Connection failed** > > * Verify IP with `ping` > * Confirm the EtherNet/IP port is open (default `44818`, or the `PORT` you set) > * Ensure PLC is powered on and in **RUN** mode > * Check `DEVICE_FAMILY` matches your controller type > * Increase `TIMEOUT` on slow networks > * Publish `-checkConnectorConnection ` to test the connection: with tags, the broker opens one tag the way a read does; with no tags, it checks the EtherNet/IP session > **Tag not found** > > * 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` > **Path errors** > > * Use `PATH "1,0"` for standard backplane routing (backplane, slot) > * For Micro800, omit `PATH`. If you set one, the broker ignores it and logs a warning > * Multi-hop paths use comma-separated segments: `"1,2,2,1"` through bridge modules > **Micro800 issues** > > * Set `DEVICE_FAMILY 4` > * Omit `PATH`. Micro800 has no backplane; a `PATH` on this family is ignored and logged as a warning > * Use Micro800 I/O tag names (`_IO_EM_DI_00`, `_IO_EM_AI_00`) > * STRING uses a 1-byte length prefix (not the ControlLogix 4-byte format). A write can grow up to the tag's String Size in Connected Components Workbench; a longer string is rejected > **Write operations fail** > > * 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 > * For `TOD`, publish milliseconds since midnight. The read form `hh:mm:ss.fff` is not a write payload > **No value published, route Yellow** > > * `DATA_TYPE` must match the PLC tag's actual type. On a mismatch the tag is refused: no value is published, the tag goes BAD, the log names both types, and route health leaves Green and goes Yellow > * The check is strict. A PLC `INT` declared as `DINT` is refused too. That case used to read correctly by luck > * Match PLC types exactly: DINT → `DINT`, REAL → `REAL`, BOOL → `BOOL` > * `TIME` tags publish as `T#...` duration strings on MQTT > * `DATE` publishes as `yyyy-MM-dd`. `TOD` publishes as `hh:mm:ss.fff` > * For now, one failing tag can back off its whole mapping *** ## Next Steps > **[EtherNet/IP Connector](./ethernetip)** > > Explicit CIP Class/Instance/Attribute addressing for assembly objects and legacy devices. > **[Industrial Connectors Overview](./overview)** > > Compare OT connectors and choose the right protocol for your devices. # BACnet/IP Connector 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 connector 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. > [!TIP] > > BACnet is the dominant protocol for building management systems. If your controller supports BACnet/IP, this connector maps each data point to an MQTT topic using the same TAG model as every other Coreflux industrial connector. ### When to use this Use the BACnet connector 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`. **Poll sensor values** Connect to a controller and read a room temperature every five seconds: ```lot wrap theme={"theme":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} DEFINE CONNECTOR 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 and write** Read a setpoint and allow MQTT writes back to the device: ```lot wrap theme={"theme":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} DEFINE CONNECTOR 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" ``` **Updates on change** Subscribe to Change-of-Value so the device pushes updates only when values change — no polling interval needed: ```lot wrap theme={"theme":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} DEFINE CONNECTOR 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 ``` **Connect directly** When the controller does not answer Who-Is/I-Am, connect straight to its IP address: ```lot wrap theme={"theme":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} DEFINE CONNECTOR 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" ``` **On-demand read/write** Trigger a single BACnet read or write by publishing to an MQTT command topic: ```lot wrap theme={"theme":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} DEFINE CONNECTOR 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}" ``` **Per-tag alarms** Each alarming point gets its own MQTT alarm stream and auto-registered ACK topic: ```lot wrap theme={"theme":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} DEFINE CONNECTOR 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 | > [!NOTE] > > 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 connector, also set `IP` and add at least one `ADD MAPPING` with tags. ```lot wrap focus={2-4} theme={"theme":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} DEFINE CONNECTOR 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":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} DEFINE CONNECTOR 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` | > [!NOTE] > > After 3 failed read cycles in a row on the current connection, the route goes to **Reconnecting**. Reads and writes still queued on a replaced connection give way at once and don't count against the new one. See [Connection status](/latest/lot-language/routes/industrial/overview#connection-status). ### 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-connector-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-connector-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` | > [!TIP] > > `WITH IP` accepts hostnames and Docker service names (for example `coreflux-connector-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":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} DEFINE CONNECTOR 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 Commandable objects (analog, binary and multi-state outputs and values) keep a 16-slot priority array. The device uses the value in the highest slot that holds one (1 = highest, 16 = lowest). If every slot is empty, it falls back to `RELINQUISH_DEFAULT`. * **Default:** every write from the connector goes to slot **8**. * **Set it:** `WITH WRITE_PRIORITY <1-16>` in the connector config. It applies to every write on that connector, and there is no per-tag override. A value outside 1–16 is refused when the connector is added. * **Higher slots win:** a write at 8 doesn't change the present value while a BMS or operator holds slot 1–7. * **Relinquish isn't supported:** the connector can't write `NULL` to free its slot. Its last value stays in that slot until you overwrite it, or until another tool relinquishes it. * **Inspect it:** set a tag's `ADDRESS_TYPE` to `PRIORITY_ARRAY` or `RELINQUISH_DEFAULT` to see which slot is active. The IOTech sandbox profile seeds `AO:0` at priority **1**. Use `WITH WRITE_PRIORITY 1` for that test device. > [!WARNING] > > 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":{"light":"css-variables","dark":"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":{"light":"css-variables","dark":"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 | connector `COV_LIFETIME` | Per-tag COV subscription lifetime in seconds | | `COV_CONFIRMED` | No | connector `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` | > [!NOTE] > > `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. Connector-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":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} DEFINE CONNECTOR 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. > [!NOTE] > > 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":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} { "connector_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 connector 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. **Device-wide** 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":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} DEFINE CONNECTOR 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`. **Per-tag** Each alarming point gets its own MQTT alarm stream and ACK topic: ```lot wrap focus={12,17} theme={"theme":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} DEFINE CONNECTOR 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. > [!WARNING] > > 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":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} { "connector_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 | **Recommended — echo the alarm** 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":{"light":"css-variables","dark":"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":{"light":"css-variables","dark":"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":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} { "success": true, "connector_name": "BMS", "operation": "ACK", "address": "AI:0", "event_state": "HIGH_LIMIT", "timestamp": "2026-06-10T16:30:00.000Z" } ``` > [!TIP] > > If `success` is `true`, the device accepted the acknowledgement. The alarm is removed from `{alarm_topic}/active`. **Explicit command** 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":{"light":"css-variables","dark":"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":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} { "success": true, "connector_name": "BMS", "operation": "ACK", "address": "AI:0", "event_state": "HIGH_LIMIT", "timestamp": "2026-06-10T16:30:00.000Z" } ``` > [!WARNING] > > 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`. > **Field device prerequisites** > > 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":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} DEFINE CONNECTOR 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":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} { "success": true, "connector_name": "BMS", "operation": "READ", "address": "AI:0", "property": "PRESENT_VALUE", "value": 22.5, "timestamp": "2026-05-31T10:00:00.000Z" } ``` > [!WARNING] > > The connector 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 **Simple polling** Reads two analog inputs and one analog value every five seconds: ```lot wrap theme={"theme":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} DEFINE CONNECTOR 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`. **COV subscriptions** Subscribe to Change-of-Value for fast-changing points — no polling interval needed: ```lot wrap theme={"theme":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} DEFINE CONNECTOR 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 ``` **Device-wide alarms** ```lot wrap theme={"theme":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} DEFINE CONNECTOR 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`. **Sandbox (Docker)** Connects to the IOTech BACnet Docker simulator when the broker runs in another container: ```lot wrap theme={"theme":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} DEFINE CONNECTOR Sandbox WITH TYPE BACNET ADD BACNET_CONFIG WITH IP "coreflux-connector-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 connector publishes each point to its own MQTT topic, so the rest of LoT can use that data like any other topic. **Aggregate into a model** First, the connector publishes each point under `bacnet/bms`: ```lot wrap theme={"theme":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} DEFINE CONNECTOR 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":{"light":"css-variables","dark":"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" ``` **Monitor alarm streams** An action can watch per-tag alarm topics and forward events: ```lot wrap theme={"theme":{"light":"css-variables","dark":"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 > **Always set WITH IP explicitly** > > 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. > **Use Direct IP for unresponsive controllers** > > 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 connector waits on discovery timeout every connect. > **Use COV for fast-changing points** > > Polling at one-second intervals wastes bandwidth on BACnet/IP. Use `WITH COV true` for occupancy sensors, fast process variables, and alarm-related points. > **Set LOCAL\_DEVICE\_ID before enabling alarms** > > The broker must announce itself as a BACnet device via directed I-Am so the field device knows where to send Event-Notifications. > **Choose write priority deliberately** > > 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. > **Omit WITH EVERY from COV-only mappings** > > COV-only mappings do not need a polling interval. Adding `WITH EVERY` on a COV-only mapping adds confusion without benefit. > **Use per-tag alarms for production points** > > 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. > **Echo alarm JSON when acknowledging** > > 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 `-checkConnectorConnection BMS` to `$SYS/Coreflux/Command`. The broker returns a JSON status for the named connector indicating whether the BACnet device was reached. > **Discovery and connection issues** > > | Symptom | Check | Solution | > | - | - | - | > | Connector 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: connector cannot reach device | `WITH IP "127.0.0.1"` in Docker? | Use the container hostname (e.g. `coreflux-connector-bacnet`) | > | Discovery timeout on every connect | `DISCOVERY_TIMEOUT` too low? | Increase to 10+ seconds for slow devices | > **Data issues** > > | 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 | > **Alarm issues** > > | 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 | > **Networking issues** > > | 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` | Connector 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 > **[Industrial Overview](./overview)** > > Learn the patterns shared across all OT connectors. > **[OPC UA Client](./opcua-client)** > > Connect to cross-platform industrial servers with OPC UA. # EtherNet/IP Connector 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` connector 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. > [!TIP] > > **Like reading labeled drawers in a filing cabinet.** Each CIP address (`Class.Instance.Attribute.Index`) points to a specific property on the device. The connector 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 connector 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`. > [!NOTE] > > For **symbolic tag names** (for example `Program:MainProgram.Temperature`), use the dedicated [Allen-Bradley connector](./allen-bradley) instead. The EtherNet/IP connector 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`, and a result on `/result` * **Engineering units** — `SCALING` / `OFFSET` / `DEADBAND` per tag *** ## Quick Start **Poll a CIP attribute** Read an analog input point every 500 ms and publish to MQTT: ```lot wrap focus={9-12} theme={"theme":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} DEFINE CONNECTOR 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" ``` **Read and write** Poll an assembly output word and accept MQTT writes to update it: ```lot wrap focus={9-15} theme={"theme":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} DEFINE CONNECTOR 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" ``` **Bit from a status word** 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":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} DEFINE CONNECTOR 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" ``` **ARRAY from assembly** Decode a packed INT32 array from an assembly buffer: ```lot wrap focus={9-14} theme={"theme":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} DEFINE CONNECTOR 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` | Longest wait for any reply, in milliseconds: session registration, reads and writes. A device slower than this is treated as disconnected and reconnected. | | `RETRY_ATTEMPTS` | No | `3` | Retry count on failure (0–10) | | `RETRY_TIME_SECONDS` | No | `5` | Delay between retries in seconds | > [!NOTE] > > Raise `READ_WRITE_TIMEOUT` for slow devices or gateways. When a reply doesn't arrive in time, or the device closes the connection, the route goes to **Reconnecting**. See [Connection status](/latest/lot-language/routes/industrial/overview#connection-status). > [!NOTE] > > The config block may be declared as `ADD ETHERNETIP_CONFIG` or `ADD EIP_CONFIG` — both are equivalent. ### Connection Example ```lot wrap theme={"theme":{"light":"css-variables","dark":"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) > [!WARNING] > > **Not implemented in release 2.2.** The connector 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":{"light":"css-variables","dark":"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 connector** | | 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 (unsigned) | | `SINT` | — | 8 bits (signed) | | `INT16` | `INT` | 16 bits | | `UINT16` | `UINT` | 16 bits | | `WORD` | — | 16 bits, published as IEC hex text (for example `16#ABCD`) | | `INT32` | `DINT` | 32 bits | | `UINT32` | `UDINT` | 32 bits | | `DWORD` | — | 32 bits, published as IEC hex text (for example `16#DEAD_BEEF`) | | `INT64` | `LINT` | 64 bits. Above `2^53`, every digit is published when no scaling, offset, minimum, or maximum is set | | `UINT64` | `ULINT` | 64 bits. Same full-digit publishing as `INT64` when no scaling, offset, minimum, or maximum is set | | `LWORD` | — | 64 bits, published as IEC hex text (for example `16#0123_4567_89AB_CDEF`) | | `FLOAT32` | `REAL`, `FLOAT` | 32 bits | | `FLOAT64` | `LREAL`, `DOUBLE` | 64 bits — MQTT values round to `DECIMAL_PLACES` (default `2`) | | `CHAR` | — | Single ASCII character | | `ASCII` | — | Read as `CHAR` (single-byte character) | | `STRING` | — | CIP string: 2-byte `UINT16` length prefix + ASCII payload | | `TIME` | — | Duration. Width and unit depend on the device | | `TOD` | `TIME_OF_DAY` | Time of day. Width and unit depend on the device | | `DATE` | — | Date. Width and unit depend on the device | | `WCHAR` | — | 16-bit wide character, published as the character | | `ARRAY` | — | Packed elements — requires `ARRAY_ELEMENT_TYPE` + `ARRAY_SIZE` | | `STRUCT` | — | Packed fields — requires `LENGTH` + `STRUCT_FIELD_DEFINITIONS` | > [!WARNING] > > `TIME`, `TOD`, and `DATE` width and unit are device-specific. Omron NJ/NX controllers store these values as 8 bytes in nanoseconds, which this connector does not decode. The published values come back wrong or as `0`. > [!WARNING] > > `DATETIME`, `DTL`, and `DATE_AND_TIME` are rejected when the route is defined, with the message `DATETIME is not supported on ETHERNETIP`. The connector does not publish `0` for these types. ### 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 | `SINT` | | USINT | `BYTE` | | INT | `INT16` | | DINT | `INT32` | | LINT | `INT64` | | UINT | `UINT16` | | UDINT | `UINT32` | | ULINT | `UINT64` | | REAL | `FLOAT32` | | LREAL | `FLOAT64` | | STRING | `STRING` | `SINT` is signed. `BYTE` (alias `USINT`) is unsigned. *** ## 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":{"light":"css-variables","dark":"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 > **Address and data type** > > **ADDRESS** `string` *required* > > CIP path: `ClassID.InstanceID.AttributeID.AttributeIndex` > > **DATA\_TYPE** `string` *required* > > CIP data type (see table above). > > **LENGTH** `integer` > > 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`). > > **BIT\_ADDRESS** `integer` > > 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. > **Joined attribute reads** > > **ENABLE\_JOIN\_ATTRIBUTE** `string` > > 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`. > > **JOIN\_CLASS\_ID** `integer` > > CIP Class ID of the joined attribute. Required when `ENABLE_JOIN_ATTRIBUTE` is `true` (must be > 0). > > **JOIN\_INSTANCE\_ID** `integer` > > CIP Instance ID of the joined attribute. Required when join is enabled (must be >= 0; instance `0` is valid on some devices). > > **JOIN\_ATTRIBUTE\_ID** `integer` > > CIP Attribute ID of the joined attribute. Required when join is enabled (must be > 0). > > **JOIN\_INDEX** `integer` > > Byte offset within the joined CIP response where the slice starts. Default: `0`. > > **JOIN\_LENGTH** `integer` > > Bytes to take from the joined attribute starting at `JOIN_INDEX`. Required when join is enabled (must be > 0). **Not** related to `SCALING_FACTOR`. > **Value transformation** > > **SCALING** `decimal` > > Multiply the parsed numeric value after byte parsing. Alias: `SCALING_FACTOR`. Default: `1.0`. **Unrelated to byte layout** — does not replace `JOIN_LENGTH`. > > **OFFSET** `decimal` > > Add after scaling. Default: `0.0`. > > **DEADBAND** `decimal` > > Suppress MQTT publish when the change is below this threshold. Default: `0.0`. > > **DECIMAL\_PLACES** `integer` > > Decimal places for `FLOAT32` / `FLOAT64` MQTT payloads. Default: `2`. > **Publishing and writes** > > **SOURCE\_TOPIC** `string` > > MQTT topic where polled values are published. > > **PUBLISH\_MODE** `string` > > `VALUE_ONLY` (default — publish just the value) or `JSON` (value, quality, and timestamp). Aliases: `VALUE`, `SIMPLE` for value-only; `FULL`, `METADATA` for JSON. > > **WRITABLE** `boolean` > > When `true`, subscribe to `DESTINATION_TOPIC` and write incoming MQTT values to the device. > > **DESTINATION\_TOPIC** `string` > > MQTT topic to receive write commands. Required when `WRITABLE` is `true`. Each write publishes its outcome on `/result`. ### 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":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} { "tag_name": "AnalogIn1", "value": 75.5, "quality": "GOOD", "timestamp": "2025-12-17T10:30:45.123Z" } ``` ### Writes Publish a value to `DESTINATION_TOPIC` on a tag with `WRITABLE true`. The connector sends a CIP `Set_Attribute_Single` and publishes the outcome on `/result`. The connection stays up whether the device accepts the write or rejects it. When the device accepts the write, the result has `"success": true`. When the device rejects the write, the result has `"success": false`, an `error` string, and a numeric `cip_status`: ```json wrap theme={"theme":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} { "success": false, "error": "CIP status 0x13 (Insufficient command data)", "cip_status": 19 } ``` | CIP status | Meaning | | - | - | | `0x13` | Not enough data (`cip_status` 19) | | `0x15` | Too much data | | `0x0E` | Attribute not settable | `WORD`, `DWORD`, and `LWORD` write payloads are parsed as hexadecimal (`16#XXXX`, `0xXXXX`, or plain hex). A payload of `99` is written as `0x99` (153). ### Using an Omron NJ/NX Through this connector, variables are reached as Tag Data Link Assemblies, not by name. A write to an input or output Assembly can be answered OK and ignored by the controller (measured on an NX1P2). Writing a variable by name needs CIP services this connector does not implement. ### 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":{"light":"css-variables","dark":"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":{"light":"css-variables","dark":"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). Field types accept the same aliases as scalar `DATA_TYPE`, including `FLOAT32` and `FLOAT64`. An unknown field type publishes `null` for that field and the broker logs a warning. **MQTT payload:** JSON object, e.g. `{"temp":25.5,"pressure":101.325,"status":1}`. > [!WARNING] > > 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. > [!WARNING] > > 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). > [!WARNING] > > `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 Connector, map each asset field to the LoT parameter below. | Asset field (HUB) | Connector 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 **Device identity** Read identity object attributes on a slow interval: ```lot wrap theme={"theme":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} DEFINE CONNECTOR 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" ``` **Scaled analog input** Read a 4–20 mA value with engineering-unit scaling and deadband: ```lot wrap theme={"theme":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} DEFINE CONNECTOR 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" ``` **Kaeser compressor status** Three boolean status bits from one INT16 word with joined attributes (legacy asset parity): ```lot wrap focus={12-28} theme={"theme":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} DEFINE CONNECTOR 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" ``` **Multi-rate polling** Fast critical data and slow diagnostics in one connector: ```lot wrap theme={"theme":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} DEFINE CONNECTOR 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" ``` **ARRAY and STRUCT** Decode packed assembly data as JSON: ```lot wrap theme={"theme":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} DEFINE CONNECTOR 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 > **Identity Object (Class 1)** > > | 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 | > **Analog Input Point (Class 102)** > > | Attribute | Description | Data Type | > | - | - | - | > | 3 | Value | FLOAT32 / INT16 | > | 4 | Status | UINT16 | > | 5 | Scaling Minimum | FLOAT32 | > | 6 | Scaling Maximum | FLOAT32 | > **Discrete Input Point (Class 104)** > > | Attribute | Description | Data Type | > | - | - | - | > | 3 | Value | BOOLEAN | > | 4 | Status | UINT16 | > **Assembly Object (Class 4)** > > | Attribute | Description | Data Type | > | - | - | - | > | 3 | Data | Variable | > | 4 | Size | UINT16 | *** ## Best Practices > **Polling intervals** > > 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. > **Deadbands** > > Use `DEADBAND` on noisy analog values to reduce MQTT traffic. Set `0.0` on digital status tags so every change publishes. > **Timeouts on slow networks** > > Increase `CONNECTION_TIMEOUT` and `READ_WRITE_TIMEOUT` on congested or remote networks. Raise `RETRY_ATTEMPTS` when connections are intermittent. > **Join attribute layout** > > Always set `JOIN_LENGTH` explicitly when `ENABLE_JOIN_ATTRIBUTE` is `"true"`. Verify `LENGTH + JOIN_LENGTH` matches the numeric type width before deploying to production. > **Symbolic vs CIP paths** > > Use numeric CIP paths on this connector. For Rockwell tag-name access (`Program:MainProgram.Counter`), use the [Allen-Bradley connector](./allen-bradley). *** ## Troubleshooting > **Connection failed** > > * 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` > **CIP path error** > > * 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) > **Wrong or stale values** > > * 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 > * `WORD`, `DWORD`, and `LWORD` publish IEC hex text (for example `16#ABCD`) > * `INT64` and `UINT64` above `2^53` publish every digit when no scaling, offset, minimum, or maximum is set > * On an Omron NJ/NX, `TIME`, `TOD`, and `DATE` are 8-byte nanosecond values this connector does not decode, so they come back wrong or as `0` > **Route definition rejected** > > * `DATETIME`, `DTL`, and `DATE_AND_TIME` are rejected with the message `DATETIME is not supported on ETHERNETIP` > **Write did not change the device** > > * Read `/result`. `"success": false` means the device rejected the CIP write. The connection stays up. `cip_status` 19 (`0x13`) is not enough data, `0x15` is too much data, and `0x0E` means the attribute is not settable > * `WORD`, `DWORD`, and `LWORD` payloads are hexadecimal. `99` is written as `0x99` (153) > * On an Omron NJ/NX, a write to an input or output Assembly can be answered OK and ignored by the controller > **Join attribute rejected at startup** > > * `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 > **[Allen-Bradley Connector](./allen-bradley)** > > Symbolic tag-name access for ControlLogix, CompactLogix, and MicroLogix PLCs. > **[Industrial Connectors Overview](./overview)** > > Compare OT connectors and choose the right protocol for your devices. # FINS Connector (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` connector 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. > [!TIP] > > 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":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} DEFINE CONNECTOR 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 **IP** `string` *required* Target Omron device IP address. **PORT** `integer` FINS protocol port. Default: 9600. **PROTOCOL** `string` Protocol type: `TCP` or `UDP`. Default: TCP. **LOCAL\_NODE** `integer` Local node address (0-255). Default: 0. **REMOTE\_NODE** `integer` Remote node address - target PLC (0-255). Default: 0. **TIMEOUT** `integer` Reply timeout per attempt, in seconds. Default: 5. A read or write can take up to (`RETRIES` + 1) × `TIMEOUT`, plus backoff and 2 s, before it fails (33.5 s with the defaults). **RETRIES** `integer` Number of retries for FINS communication. Default: 5. **POLLING** `integer` Polling interval in milliseconds. Default: 200. > [!NOTE] > > On FINS/UDP the route goes to **Reconnecting** after 2 commands in a row get no reply. On FINS/TCP a connection with no answered attempt is closed and reopened. See [Connection status](/latest/lot-language/routes/industrial/overview#connection-status). ### Connection Examples **TCP Connection** Standard TCP connection (recommended): ```lot wrap theme={"theme":{"light":"css-variables","dark":"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 ``` > [!NOTE] > > TCP transport for FINS is currently in development. Use UDP for production deployments. **UDP Connection** UDP for lower latency: ```lot wrap theme={"theme":{"light":"css-variables","dark":"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 ``` > [!NOTE] > > 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 | > [!NOTE] > > 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 connector 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":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} DEFINE CONNECTOR 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":{"light":"css-variables","dark":"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 > **Address Configuration** > > **ADDRESS** `string` *required* > > Word address in PLC memory (e.g., 100, 200, 500). > > **BIT\_ADDRESS** `integer` > > Bit address for bit-specific data types (0-15). Used for BOOL values within a word. > > **MEMORY\_TYPE** `string` > > Memory area: `CIO`, `WR`, `HR`, `AR`, `DM`, `CNT`, `TIM`, `EM`. Default: DM. > > **DATA\_TYPE** `string` *required* > > FINS data type: BOOL, BYTE, INT, UINT, DINT, UDINT, LINT, ULINT, REAL, LREAL, ASCII, STRING. > > **LENGTH** `integer` > > String payload size in bytes; FINS adds two header bytes (`LENGTH + 2` on the wire). Default: **256**. Alias: `STRING_SIZE`. > **Value Transformation** > > **SCALING** `double` > > Multiplier for value transformation. Default: 1.0. > > **OFFSET** `double` > > Offset added after scaling. Default: 0.0. > > **DECIMAL\_PLACES** `integer` > > Decimal places in output. Default: 2. > **Filtering** > > **MIN\_VALUE** `double` > > Minimum allowed value. > > **MAX\_VALUE** `double` > > Maximum allowed value. > > **DEADBAND** `double` > > Minimum change to trigger publish. Default: 0.0. > **Publishing** > > **SOURCE\_TOPIC** `string` *required* > > Topic where PLC values are published. Subscribe here to receive sensor data. > > **DESTINATION\_TOPIC** `string` > > Topic to send write commands. Publish a value here to write it to the PLC. > > **PUBLISH\_MODE** `string` > > Output format: `VALUE_ONLY` or `JSON`. Default: VALUE\_ONLY. > > **UNIT** `string` > > Engineering unit for documentation. > > **DESCRIPTION** `string` > > Human-readable description. > **Write Configuration** > > **WRITABLE** `boolean` > > Allow writing to this address. Default: false. *** ## Addressing Examples ### Word Addressing Access data at word level: ```lot wrap theme={"theme":{"light":"css-variables","dark":"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":{"light":"css-variables","dark":"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":{"light":"css-variables","dark":"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" ``` > [!NOTE] > > 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 connector 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":{"light":"css-variables","dark":"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":{"light":"css-variables","dark":"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 **Basic Data Memory Reading** Read values from DM area: ```lot wrap theme={"theme":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} DEFINE CONNECTOR 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" ``` **I/O and Work Areas** Read CIO and Work areas: ```lot wrap theme={"theme":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} DEFINE CONNECTOR 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" ``` **Bit-Level Access** Access individual bits: ```lot wrap theme={"theme":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} DEFINE CONNECTOR 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" ``` **Timers and Counters** Read timer and counter values: ```lot wrap theme={"theme":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} DEFINE CONNECTOR 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" ``` **Bidirectional Control** Read and write process values: ```lot wrap theme={"theme":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} DEFINE CONNECTOR 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" ``` **Holding Area** Read retentive data from HR area: ```lot wrap theme={"theme":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} DEFINE CONNECTOR 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" ``` **Combined (Cyclic + On-Demand)** Continuous monitoring with on-demand read and write events in the same connector: ```lot wrap theme={"theme":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} DEFINE CONNECTOR 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 > **CIO - Core I/O** > > * 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 > **DM - Data Memory** > > * Main user data storage > * Typically DM0-DM32767 > * Best for process data, setpoints, configuration > * Retentive (battery-backed) > **HR - Holding Area** > > * Always retentive data > * Survives power cycles > * Use for counters, totals, settings > * Typically HR0-HR511 > **WR - Work Area** > > * General purpose work bits > * Non-retentive (cleared on power cycle) > * Fast access for temporary data > **TIM/CNT - Timers and Counters** > > * TIM: Timer present values > * CNT: Counter present values > * Values are 16-bit unsigned > * Timer values often in 0.1s units *** ## Troubleshooting > **Connection Failed** > > * 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 > **Node Address Errors** > > * 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 > **Memory Address Errors** > > * 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 > **Timeouts with node mismatch** > > * The broker only accepts a reply that matches the request's service ID and comes from the node it addressed. > * Some PLCs and gateways swap the client and server node in the FINS/TCP handshake reply. Their replies are dropped, so the route times out. The broker logs a warning naming the mismatched nodes. > * Check `LOCAL_NODE` and `REMOTE_NODE`, or update the gateway firmware. > **Timeout Errors** > > * Increase TIMEOUT value > * Check network connectivity > * Verify PLC is in RUN mode > * Reduce POLLING rate if reading many tags > * Try TCP instead of UDP > **Write Operations Fail** > > * 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 > **[Modbus TCP Client](./modbus-tcp-client)** > > Universal industrial protocol support. > **[Industrial Overview](./overview)** > > Common patterns for all industrial connectors. # Modbus Serial Client Connector 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 > [!NOTE] > > This page documents the **client** connector (`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` connector 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. `WITH CONNECTION_TYPE "SERIAL"` is optional: the type already means serial. An explicit `CONNECTION_TYPE` still wins. > [!TIP] > > 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":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} DEFINE CONNECTOR 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 **CONNECTION\_TYPE** `string` Optional. A connector with `TYPE MODBUS_SERIAL` already implies `SERIAL`, so you can omit this. If you set it, that value is the one the connector uses. **COM\_PORT** `string` *required* Serial port name. Windows: `COM1`, `COM2`, etc. Linux: `/dev/ttyUSB0`, `/dev/ttyS0`, etc. **BAUD\_RATE** `integer` *required* Communication speed in bits per second. Common values: 9600, 19200, 38400, 57600, 115200. **PARITY** `string` Parity checking: `NONE`, `ODD`, `EVEN`, `MARK`, or `SPACE`. Default: NONE. **DATA\_BITS** `integer` Number of data bits per character: 7 or 8. Default: 8. **STOP\_BITS** `string` Number of stop bits: `ONE`, `TWO`, or `ONEPOINTFIVE`. Default: ONE. **SLAVE\_ID** `integer` *required* Modbus slave/unit ID (1-247). Each device on the bus must have a unique ID. **READ\_TIMEOUT** `integer` Read operation timeout in milliseconds. Default: 1000. **WRITE\_TIMEOUT** `integer` Write operation timeout in milliseconds. Default: 1000. **RETRY\_COUNT** `integer` Number of retry attempts on failure. Default: 3. **RETRY\_DELAY** `integer` Delay between retries in milliseconds. Default: 100. *** ## Common Serial Settings Different devices use different serial settings. Here are common configurations: **Standard (Most Common)** ```lot wrap theme={"theme":{"light":"css-variables","dark":"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 (9600 baud, 8 data bits, No parity, 1 stop bit) **Even Parity** ```lot wrap theme={"theme":{"light":"css-variables","dark":"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 (9600 baud, 8 data bits, Even parity, 1 stop bit. Common in older equipment.) **High Speed** ```lot wrap theme={"theme":{"light":"css-variables","dark":"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 (115200 baud, 8 data bits, No parity, 1 stop bit. For modern devices.) **Linux USB Adapter** ```lot wrap theme={"theme":{"light":"css-variables","dark":"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 ``` > [!NOTE] > > 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":{"light":"css-variables","dark":"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 connectors or use different slave IDs: ```lot wrap theme={"theme":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} DEFINE CONNECTOR 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 CONNECTOR 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" ``` > [!WARNING] > > When sharing a serial port between multiple slave IDs, ensure polling intervals are set to avoid bus collisions. The connector 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 connector reads enough **consecutive registers** from the tag's starting `ADDRESS` to cover the configured size. ```lot wrap focus={16} theme={"theme":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} DEFINE CONNECTOR 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 Connector](./overview#string-sizing-by-connector) on the industrial overview. ### TAG parameters **LENGTH** `integer` (default: 256) **STRING only.** Maximum string size in bytes. The connector allocates consecutive registers from the starting `ADDRESS` (2 bytes per register). Alias: `STRING_SIZE`. ### Quick Reference ```lot wrap theme={"theme":{"light":"css-variables","dark":"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 connector 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":{"light":"css-variables","dark":"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":{"light":"css-variables","dark":"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 **Single Device** Basic connection to a single Modbus RTU device: ```lot wrap theme={"theme":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} DEFINE CONNECTOR 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" ``` **Temperature Controller** Read process value and write setpoint: ```lot wrap theme={"theme":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} DEFINE CONNECTOR 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 "%" ``` **VFD (Variable Frequency Drive)** Control a motor drive: ```lot wrap theme={"theme":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} DEFINE CONNECTOR 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" ``` **Combined (Cyclic + On-Demand)** Continuous monitoring with on-demand read and write events in the same connector: ```lot wrap theme={"theme":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} DEFINE CONNECTOR 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 > **No Communication** > > * 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** > > * 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 > **Timeout Errors** > > * Increase `READ_TIMEOUT` value > * Verify serial settings match device exactly > * Check for bus conflicts (multiple masters) > * For RS-485, ensure proper biasing > **Incorrect Values** > > * 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) > **Port Not Found** > > * 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 > [!NOTE] > > 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 > **[Modbus TCP Client](./modbus-tcp-client)** > > Connect to Modbus devices over Ethernet. > **[Siemens S7](./siemens-s7)** > > Connect directly to Siemens PLCs. # Modbus TCP Client Connector 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 > [!NOTE] > > This page documents the **client** connector (`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` connector 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. > [!TIP] > > Modbus TCP is one of the most widely supported industrial protocols. If your device supports Modbus, this connector provides a reliable way to integrate it with MQTT. ## Basic Syntax ```lot wrap theme={"theme":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} DEFINE CONNECTOR 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 **HOST** `string` *required* Modbus TCP server hostname or IP address. **PORT** `integer` Modbus TCP port. Default: 502. **SLAVE\_ID** `integer` Modbus slave/unit ID (1-247). Default: 1. **CONNECTION\_TIMEOUT** `integer` Connection timeout in milliseconds. Default: 5000. **READ\_TIMEOUT** `integer` Read operation timeout in milliseconds. Default: 1000. **WRITE\_TIMEOUT** `integer` Write operation timeout in milliseconds. Default: 1000. **RETRY\_COUNT** `integer` Number of retry attempts on failure. Default: 3. **RETRY\_DELAY** `integer` Delay between retries in milliseconds. Default: 100. **ONE\_BASED\_ADDRESSES** `boolean` (default: false) 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). ### One request at a time Each connection carries one request at a time. Mappings, events, and timers on that connector share the connection, so a poll of many tags runs one read after another. A device that is merely slow fails that read and keeps the connection. Raise `READ_TIMEOUT` and set the poll interval long enough for those reads to finish. A connection that stops answering altogether is closed and reopened, so a stale reply never lands on the next request. See [Connection status](/latest/lot-language/routes/industrial/overview#connection-status). ### Connection Example ```lot wrap theme={"theme":{"light":"css-variables","dark":"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 | > [!NOTE] > > 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":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} DEFINE CONNECTOR 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":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} DEFINE CONNECTOR 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" ``` > [!NOTE] > > 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 **DATA\_TYPE** `string` *required* 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 connector reads enough **consecutive registers** starting at the tag's `ADDRESS` to cover the configured size. ```lot wrap focus={12} theme={"theme":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} DEFINE CONNECTOR 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 Connector](./overview#string-sizing-by-connector) on the industrial overview. *** ## TAG Configuration ### Complete TAG Parameters ```lot wrap theme={"theme":{"light":"css-variables","dark":"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 > **Address Configuration** > > **ADDRESS** `string` *required* > > 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). > > **ADDRESS\_TYPE** `string` *required* > > Type of Modbus address: `HOLDING_REGISTER`, `INPUT_REGISTER`, `COIL`, or `DISCRETE_INPUT`. > > **LENGTH** `integer` (default: 256) > > **STRING only.** Maximum string size in bytes. The connector allocates consecutive registers from the starting `ADDRESS` (2 bytes per register). Alias: `STRING_SIZE`. > **Value Transformation** > > **SCALING** `double` > > Multiplier applied to raw value. Default: 1.0. Example: Raw value 245 with SCALING 0.1 becomes 24.5. > > **OFFSET** `double` > > Value added after scaling. Default: 0.0. Formula: `result = (raw * scaling) + offset`. > > **DECIMAL\_PLACES** `integer` > > Number of decimal places in published value. Default: 2. > **Filtering** > > **MIN\_VALUE** `double` > > Minimum allowed value. Values below this threshold are not published. > > **MAX\_VALUE** `double` > > Maximum allowed value. Values above this threshold are not published. > > **DEADBAND** `double` > > Minimum change required to publish a new value. Default: 0.0. > **Publishing** > > **SOURCE\_TOPIC** `string` > > Topic where PLC values are published. Subscribe here to receive sensor data. > > **PUBLISH\_MODE** `string` > > Output format: `VALUE_ONLY` or `JSON`. Default: VALUE\_ONLY. > > **UNIT** `string` > > Engineering unit for documentation (e.g., °C, bar, RPM). > > **DESCRIPTION** `string` > > Human-readable description of the TAG. > **Write Configuration** > > **WRITABLE** `boolean` > > Allow writing to this register/coil. Default: false. > > **DESTINATION\_TOPIC** `string` > > Topic to send write commands. Publish a value here to write it to the PLC. > **Byte Ordering** > > **BYTE\_ORDER** `string` > > Byte order for multi-byte values: `BIGENDIAN` or `LITTLEENDIAN`. Default: BIGENDIAN. > > **WORD\_ORDER** `string` > > Word order for 32/64-bit values: `BIGENDIAN` or `LITTLEENDIAN`. Default: BIGENDIAN. > > > [!WARNING] > > > > 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 connector executes it and publishes the result to DESTINATION\_TOPIC. ```lot wrap theme={"theme":{"light":"css-variables","dark":"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 connector executes the write and publishes the result to DESTINATION\_TOPIC: ```lot wrap theme={"theme":{"light":"css-variables","dark":"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 **Basic Sensor Reading** Read temperature and pressure from a Modbus device: ```lot wrap theme={"theme":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} DEFINE CONNECTOR 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" ``` **Multiple Polling Rates** Group TAGs by update frequency: ```lot wrap theme={"theme":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} DEFINE CONNECTOR 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" ``` **Bidirectional Control** Read values and write setpoints: ```lot wrap theme={"theme":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} DEFINE CONNECTOR 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" ``` **Coils and Digital I/O** Read digital inputs and control outputs: ```lot wrap theme={"theme":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} DEFINE CONNECTOR 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" ``` **Combined (Cyclic + On-Demand)** Continuous monitoring with on-demand read and write events in the same connector: ```lot wrap theme={"theme":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} DEFINE CONNECTOR 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 connector reads registers 0-9 and publishes the result to `modbus/responses/read`. To write, publish to `modbus/commands/write`. *** ## Troubleshooting > **Connection Timeout** > > * 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` > **Mismatched responses** > > * Tags go BAD with "Response was not of expected transaction ID" or "Unexpected byte count" when replies get mixed on one connection. Upgrade to a newer broker. > * Polling many tags is now serialized. Raise `READ_TIMEOUT` and the poll interval so each read can finish. > **Incorrect Values** > > * 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 > **Write Operations Fail** > > * 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` > **Slave ID Errors** > > * 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 > **[Modbus Serial Client](./modbus-serial-client)** > > Connect to RS-232/RS-485 Modbus RTU devices. > **[Modbus TCP Server](./modbus-tcp-server)** > > Expose MQTT topics as Modbus registers for SCADA and HMIs. # Modbus TCP Server Connector 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 > [!NOTE] > > This page documents the **server** connector (`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` connector 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. > [!TIP] > > **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 Connector * **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 **Expose sensor values** Map an MQTT topic to a holding register masters can poll: ```lot wrap theme={"theme":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} DEFINE CONNECTOR 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`. **Writable setpoint** Allow a master to write a register; the broker publishes the value to MQTT within one `WITH EVERY` interval: ```lot wrap theme={"theme":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} DEFINE CONNECTOR 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. **Full plant server** Listen on port 502 as unit 1 with multiple tags and connection settings: ```lot wrap theme={"theme":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} DEFINE CONNECTOR 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. **Crash-resilient setpoints** Persist master-written setpoints across broker restarts: ```lot wrap theme={"theme":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} DEFINE CONNECTOR 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":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} graph LR subgraph mqtt [MQTT] Topics[Topics] end subgraph broker [Coreflux Broker] Connector[MODBUS_SERVER Connector] Registers[Register / Coil Space] end subgraph ot [OT Masters] Master[SCADA / HMI / PLC] end Topics -->|publish sets value| Connector Connector --> Registers Registers -->|read / write| Master Master -->|write writable tag| Connector Connector -->|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 connectors, connector 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 connector 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":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} DEFINE CONNECTOR 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 **HOST** `string` (default: 0.0.0.0) Bind interface to listen on (`0.0.0.0` = all interfaces). `IP` is accepted as an alias. **PORT** `integer` (default: 502) TCP listen port. Each server connector claims its port exclusively—a second connector on the same port is refused. **MAX\_CONNECTIONS** `integer` (default: 64) Best-effort soft cap on concurrent master connections. See [Connection-limit behavior](#connection-limit-behavior). **UNIT\_ID** `integer` (default: 1) Modbus unit ID the broker answers as (1–247). `SLAVE_ID` is accepted as an alias. **PERSISTENCE** `boolean` (default: false) Persist served values across broker restarts. See [Data persistence & recovery](#data-persistence--recovery). **ONE\_BASED\_ADDRESSES** `boolean` (default: false) 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). > [!NOTE] > > 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":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} DEFINE CONNECTOR 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) connector): | 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":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} DEFINE CONNECTOR 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" ``` > [!NOTE] > > Modicon-style addresses are decoded the same way regardless of `ONE_BASED_ADDRESSES`. The flag only affects plain numeric addresses. ### Tag parameters **ADDRESS** `string` *required* 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). **ADDRESS\_TYPE** `string` *required* Which bank the tag lives in: `HOLDING_REGISTER`, `INPUT_REGISTER`, `COIL`, or `DISCRETE_INPUT`. **DATA\_TYPE** `string` (default: INT16) Wire type: `BOOL`, `INT16`, `UINT16`, `INT32`, `UINT32`, `INT64`, `UINT64`, `FLOAT`, `DOUBLE`, or `STRING`. `INT32`/`UINT32`/`FLOAT` span 2 registers; `INT64`/`UINT64`/`DOUBLE` span 4. **LENGTH** `integer` (default: 40) **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 connector validation. **SOURCE\_TOPIC** `string` MQTT topic whose publishes set this register or coil. Defaults to `{mapping SOURCE_TOPIC}/{TagName}`. **WRITABLE** `boolean` (default: false) Allow masters to write the address. Only `HOLDING_REGISTER` and `COIL` are master-writable. **DESTINATION\_TOPIC** `string` Topic published to when a master writes this (writable) address. **BYTE\_ORDER** `string` (default: BIGENDIAN) Byte order within each register for multi-byte types: `BIGENDIAN` or `LITTLEENDIAN`. **WORD\_ORDER** `string` (default: BIGENDIAN) Word order across registers for 32-/64-bit types (high word first by default): `BIGENDIAN` or `LITTLEENDIAN`. **DECIMAL\_PLACES** `integer` (default: 2) Rounding applied to `FLOAT`/`DOUBLE` values published to `DESTINATION_TOPIC`. **PERSISTENCE** `boolean` Per-tag override of the connector persistence default. Precedence: tag → mapping → connector. See [Data persistence & recovery](#data-persistence--recovery). **DESCRIPTION** `string` Free-text documentation for the tag. **EVERY** `duration` (default: 1 second) 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":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} DEFINE CONNECTOR 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":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} DEFINE CONNECTOR 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)**. > [!NOTE] > > 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 connector 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 connector default: ```lot wrap theme={"theme":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} DEFINE CONNECTOR 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":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} DEFINE CONNECTOR 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 connector'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 **1. Deploy the connector** Add the connector definition to your broker and confirm connector status shows **Connected** (the endpoint is listening). **2. Connect with a Modbus master** Use any standards-compliant Modbus TCP master. Connect to `:` and address the connector's `UNIT_ID` (default 1). **3. Confirm MQTT → register** Publish a test value to a bound `SOURCE_TOPIC`. Read the corresponding address from the master and confirm the decoded value matches. **4. Confirm register → MQTT** 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 > [!WARNING] > > Each `MODBUS_SERVER` connector binds exclusively to its `PORT`. A second connector on the same port fails to start. Give each connector its own port. *** ## Troubleshooting Publish `-checkConnectorConnection ` to `$SYS/Coreflux/Command`. For a server connector this reports whether the hosted endpoint is **listening**, along with the endpoint, port, unit ID, and current connection count. > **Connector status never Connected** > > * **Port in use** — Another connector 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. > **Master cannot connect** > > Confirm `HOST`/`PORT`, network path, and firewall rules. The master must target the connector's `UNIT_ID` (default 1). > **Master reads zero or stale values** > > Registers default to zero until first set. Verify `SOURCE_TOPIC` has published; retained MQTT values are seeded at startup. > **Master write not appearing on MQTT** > > * 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. > **Write rejected (Modbus exception)** > > 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). > **Master FC16 write fails on a STRING tag (Illegal Data Address)** > > **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.` > **Master activity logging** > > The broker logs master activity for this connector 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 connector. > **Wrong numeric value** > > Align `BYTE_ORDER` and `WORD_ORDER` with the master's expectation. Default is big-endian, high word first. > **Address mismatch (Illegal Data Address)** > > 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. > **Connection-limit behavior** > > `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 > **Bind to a specific interface in production** > > Prefer `WITH HOST "192.168.10.5"` over `0.0.0.0` when only one network should reach the server. > **Keep environment values out of connector text** > > Use `WITH PORT GET ENV "MODBUS_PORT"` instead of hard-coding environment-specific ports. > **Declare each exposed address explicitly** > > Only declared addresses are served. One `ADD TAG` per register or coil a master needs. > **Make tags WRITABLE only when needed** > > Leave `WRITABLE` unset for read-only points. Non-writable holding registers and read-only banks reject writes with a Modbus exception. > **Match byte and word order to the master** > > Set `WORD_ORDER "LITTLEENDIAN"` when the master expects low word first. Default is big-endian, high word first. > **Size WITH EVERY to outbound latency** > > Use a shorter interval for responsive control loops; a longer one to reduce churn for slowly-changing values. > **Reserve ports per connector** > > Document which connector owns which port to avoid startup conflicts when adding connectors. > **Restrict access at the network layer** > > Modbus TCP has no authentication. Place the server behind a firewall or VLAN and rely on HUB topic permissions for MQTT-side security. > **Monitor connector status** > > Watch `$SYS/Coreflux/Routes/{connector_name}/status`. `Connected` means the server is listening—investigate `Error` immediately (usually a port conflict). *** ## Next Steps > **[Modbus TCP Client](./modbus-tcp-client)** > > Connect to remote Modbus devices and poll register data into MQTT. > **[Modbus Serial Client](./modbus-serial-client)** > > Connect to remote Modbus RTU devices over serial. # OPC UA Client Connector 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 > [!NOTE] > > This page documents the **client** connector (`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` connector 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. > [!TIP] > > 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":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} DEFINE CONNECTOR 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 **ENDPOINT\_URL** `string` *required* OPC UA server endpoint URL (e.g., `opc.tcp://localhost:4840`). **AUTH\_MODE** `integer` Authentication mode: * `0` - Anonymous (no authentication) * `1` - Username/Password * `2` - Certificate Default: 0. **USERNAME** `string` Username for authentication (required if AUTH\_MODE is 1). **PASSWORD** `string` Password for authentication (required if AUTH\_MODE is 1). **USE\_SECURITY** `boolean` Enable security mode. Default: false. **SECURITY\_POLICY** `integer` Security policy level: * `0` - None * `1` - Basic128Rsa15 * `2` - Basic256 * `3` - Basic256Sha256 * `4` - Aes128\_Sha256\_RsaOaep * `5` - Aes256\_Sha256\_RsaPss Default: 0. **MESSAGE\_SECURITY** `integer` Message security mode: * `0` - None * `1` - Sign * `2` - SignAndEncrypt Default: 0. **PKI\_DIR** `string` 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). **CLIENT\_CERT\_PATH** `string` 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. **CLIENT\_KEY\_PATH** `string` Path to the private key PEM file when `CLIENT_CERT_PATH` points to a certificate-only PEM. Not needed for PKCS#12 bundles. **CLIENT\_CERT\_PASSWORD** `string` Password for the PKCS#12 bundle or encrypted private key. Use `GET SECRET`—never hard-code passwords in connector definitions. **AUTO\_ACCEPT\_UNTRUSTED\_CERTIFICATES** `boolean` 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** `boolean` Suppress nonce validation errors. Default: false. **TIMEOUT** `integer` Connection timeout in milliseconds. Default: 90000. *** ## Security Configuration **Anonymous (No Security)** For development or trusted networks: ```lot wrap theme={"theme":{"light":"css-variables","dark":"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" ``` **Username/Password** Basic authentication: ```lot wrap theme={"theme":{"light":"css-variables","dark":"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 ``` **Certificate Authentication** 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":{"light":"css-variables","dark":"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). > [!TIP] > > **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 connectors 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":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} ADD OPCUA_CONFIG WITH ENDPOINT_URL "opc.tcp://server:4840" WITH PKI_DIR "/data/opcua/pki" ``` > [!WARNING] > > 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":{"light":"css-variables","dark":"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":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} DEFINE CONNECTOR 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. > [!NOTE] > > 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: **Generate certificate** ```bash wrap theme={"theme":{"light":"css-variables","dark":"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 ``` **Reference in connector (PKCS#12)** ```lot wrap focus={6-9} theme={"theme":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} DEFINE CONNECTOR 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" ``` **Reference in connector (PEM)** 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":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} DEFINE CONNECTOR 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`: **1. Deploy the connector** The first handshake fails and the server's certificate is written to `PKI_DIR/rejected/certs`. **2. Move to trusted** ```bash wrap theme={"theme":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} mv "$PKI_DIR"/rejected/certs/.der "$PKI_DIR"/trusted/certs/ ``` **3. Reconnect** The connector 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: **1. Export the broker certificate** ```bash wrap theme={"theme":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} openssl x509 -inform der -in "$PKI_DIR"/own/certs/.der -out broker-client.pem ``` **2. Add to the server's trust list** Import `broker-client.pem` (or the `.der`) into the server's trusted certificate store—via its PKI `trusted` folder or vendor certificate manager. **3. Reconnect** 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 connector connects: ```bash wrap theme={"theme":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} -checkConnectorConnection 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 | > [!WARNING] > > An `ADDRESS` that contains a slash (`/`) is treated as a browse path, even with `USE_BROWSE_PATH` off. So a string NodeId with a slash, such as the default `ns=2;s=PlantServer/Temperature` node of a Coreflux [OPC UA server](/latest/lot-language/routes/industrial/opcua-server), can't be read. Give the server tag an explicit `ADDRESS` without slashes, such as `"Line1.Oee"`, and read `ns=2;s=Line1.Oee`. ### 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: **USE\_BROWSE\_PATH** `boolean` Enable BrowsePath mode - treat all TAG addresses as browse paths instead of NodeIds. **BROWSE\_ROOT\_PATH** `string` Root path for browsing when USE\_BROWSE\_PATH is enabled. Default: Objects. #### BrowsePath Example ```lot wrap theme={"theme":{"light":"css-variables","dark":"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":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} DEFINE CONNECTOR 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 Connector](./overview#string-sizing-by-connector) on the industrial overview. *** ## TAG Configuration ### Complete TAG Example ```lot wrap theme={"theme":{"light":"css-variables","dark":"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 > **Address Configuration** > > **ADDRESS** `string` *required* > > OPC UA NodeId (e.g., `ns=2;s=MyVariable` or `ns=2;i=1234`) or BrowsePath if USE\_BROWSE\_PATH is enabled. > > **DATA\_TYPE** `string` *required* > > OPC UA data type: `BOOLEAN`, `BYTE`, `INT16`, `UINT16`, `INT32`, `UINT32`, `INT64`, `UINT64`, `FLOAT`, `DOUBLE`, `STRING`. > > **IS\_ARRAY** `boolean` > > Indicates if the value is an array. Default: false. > > **LENGTH** `integer` (default: 256) > > **STRING only.** Maximum string size in bytes. Alias: `STRING_SIZE`. > **Value Transformation** > > **SCALING** `double` > > Multiplier for value transformation. Default: 1.0. > > **OFFSET** `double` > > Offset added after scaling. Default: 0.0. > > **DECIMAL\_PLACES** `integer` > > Decimal places in output. Default: 2. > **Filtering** > > **MIN\_VALUE** `double` > > Minimum allowed value. > > **MAX\_VALUE** `double` > > Maximum allowed value. > > **DEADBAND** `double` > > Minimum change to trigger publish. Default: 0.0. > **Publishing** > > **SOURCE\_TOPIC** `string` > > Topic where PLC values are published. Subscribe here to receive sensor data. > > **PUBLISH\_MODE** `string` > > Output format: `VALUE_ONLY` or `JSON`. Default: VALUE\_ONLY. > > **UNIT** `string` > > Engineering unit for documentation. > > **DESCRIPTION** `string` > > Human-readable description. > **Write Configuration** > > **WRITABLE** `boolean` > > Allow writing to this node. Default: false. > > **DESTINATION\_TOPIC** `string` > > 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 connector 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":{"light":"css-variables","dark":"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":{"light":"css-variables","dark":"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 **Basic Connection** Simple OPC UA connection with anonymous access: ```lot wrap theme={"theme":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} DEFINE CONNECTOR 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" ``` **Secure Connection** Production setup with encryption: ```lot wrap theme={"theme":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} DEFINE CONNECTOR 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" ``` **BrowsePath Mode** Using symbolic paths: ```lot wrap theme={"theme":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} DEFINE CONNECTOR 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" ``` **Array Values** Reading array data: ```lot wrap theme={"theme":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} DEFINE CONNECTOR 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" ``` **Kepware Connection** Connecting to Kepware OPC UA server: ```lot wrap theme={"theme":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} DEFINE CONNECTOR 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" ``` **Combined (Cyclic + On-Demand)** Continuous monitoring with on-demand read and write events in the same connector: ```lot wrap theme={"theme":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} DEFINE CONNECTOR 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 > **Use Encryption in Production** > > Always enable security for production deployments: > > ```lot wrap theme={"theme":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} > WITH USE_SECURITY "true" > WITH SECURITY_POLICY 3 // Basic256Sha256 > WITH MESSAGE_SECURITY 2 // SignAndEncrypt > ``` > **Don't Auto-Accept Certificates** > > In production, properly configure certificate trust: > > ```lot wrap theme={"theme":{"light":"css-variables","dark":"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). > **Use Strong Authentication** > > Prefer username/password or certificate authentication over anonymous: > > ```lot wrap theme={"theme":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} > WITH AUTH_MODE 1 // or '2' for certificate > WITH USERNAME "operator" > WITH PASSWORD "strong_password_here" > ``` *** ## Troubleshooting > **Connection Timeout** > > * Verify endpoint URL is correct > * Check firewall allows TCP port (usually 4840) > * Ensure server is running and accessible > * Try increasing TIMEOUT value > **Security Rejection** > > * 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 > **Connector with CLIENT\_CERT\_PATH fails to start** > > * 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 > **NodeId Not Found** > > * 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 > **Authentication Failed** > > * Verify username and password > * Check user has appropriate permissions > * Ensure AUTH\_MODE matches server requirements > **Incorrect Values** > > * Verify DATA\_TYPE matches server variable type > * Check IS\_ARRAY setting for array values > * Verify SCALING and OFFSET calculations *** ## Next Steps > **[OPC UA Server Connector](./opcua-server)** > > Expose MQTT topics as OPC UA nodes for SCADA and OT clients. > **[ADS (Beckhoff)](./ads)** > > Connect to TwinCAT systems. # OPC UA Server Connector 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 > [!NOTE] > > This page documents the **server** connector (`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` connector 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. > [!TIP] > > **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 Connector * **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 **Expose static tags** 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":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} DEFINE CONNECTOR 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`. **Writable setpoints** 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":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} DEFINE CONNECTOR 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). **Dynamic wildcard topics** 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":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} DEFINE CONNECTOR 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 connector edit required. **Secure production server** Encrypt traffic, require credentials, and combine static tags with dynamic discovery: ```lot wrap theme={"theme":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} DEFINE CONNECTOR 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. **Crash-resilient setpoints** Persist client-written setpoints across broker restarts: ```lot wrap theme={"theme":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} DEFINE CONNECTOR 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":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} graph LR subgraph mqtt [MQTT] Topics[Topics] end subgraph broker [Coreflux Broker] Connector[OPCUA_SERVER Connector] Nodes[OPC UA Nodes] end subgraph ot [OT Clients] Client[SCADA / UaExpert / PLC] end Topics -->|publish sets value| Connector Connector --> Nodes Nodes -->|browse / read| Client Client -->|write writable tag| Connector Connector -->|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 connector 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":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} DEFINE CONNECTOR 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 **SERVER\_PORT** `integer` (default: 4840) TCP port the server listens on (`opc.tcp`). Each server connector claims its port process-wide—two connectors cannot share the same port. **ENDPOINT\_PATH** `string` Optional path appended to the base address, for example `/coreflux`. Default: empty. **APPLICATION\_NAME** `string` (default: CorefluxBroker\_OpcUaServer) Application display name and certificate common name (CN). **APPLICATION\_URI** `string` (default: urn:\{host}:Coreflux:\{connector}) Application URI. Must match the certificate SAN URI. Changing this regenerates the certificate. **NAMESPACE\_URI** `string` (default: urn:coreflux:opcua:\{connector}) Namespace URI for exposed nodes. **ROOT\_FOLDER** `string` (default: connector name) Root folder under `Objects` where tags are organized. **PKI\_DIR** `string` 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 **USE\_SECURITY** `boolean` (default: true) Offer a signed or encrypted endpoint. > [!NOTE] > > 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** `integer` (default: 0) Security policy offered when `USE_SECURITY` is true: | Value | Policy | | - | - | | `0` | None | | `3` | Basic256Sha256 | | `4` | Aes128\_Sha256\_RsaOaep | | `5` | Aes256\_Sha256\_RsaPss | **MESSAGE\_SECURITY** `integer` (default: 0) Message security mode. Requires `SECURITY_POLICY` ≥ 1 when using Sign or SignAndEncrypt. | Value | Mode | | - | - | | `0` | None | | `1` | Sign | | `2` | SignAndEncrypt | **ALLOW\_NO\_SECURITY** `boolean` (default: false) When `USE_SECURITY` is true, also offer a None/None endpoint. Use for testing only. ### Authentication **ALLOW\_ANONYMOUS** `boolean` (default: true) Offer anonymous login. Set to `false` for production and configure `USERNAME`/`PASSWORD` or `ALLOW_CERTIFICATE_AUTH`. **USERNAME** `string` Username for password-based login. Use `GET ENV` or `GET SECRET`. **PASSWORD** `string` Password for password-based login. Use `GET ENV` or `GET SECRET`. **ALLOW\_CERTIFICATE\_AUTH** `boolean` (default: false) Offer X.509 certificate user authentication. **AUTO\_ACCEPT\_UNTRUSTED\_CERTIFICATES** `boolean` (default: false) Automatically trust connecting client certificates. When false, untrusted client certs are placed in the `rejected` store. **PERSISTENCE** `boolean` (default: false) Persist served node values across broker restarts. See [Data persistence & recovery](#data-persistence--recovery). > [!NOTE] > > 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 connector](./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 connectors | | `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 | | `ADD EVENT` / triggers | Supported on client connectors | Not supported | On server connectors, connector status **Connected** means the endpoint is **listening**—not that a remote session is established. ### Explicit NodeId By default, a tag named `Temperature` under connector `PlantServer` with `ROOT_FOLDER` defaulting to the connector name maps to `ns=2;s=PlantServer/Temperature`. Override it with `ADDRESS`. Give the string identifier only, without the `ns=2;s=` prefix: ```lot wrap focus={6,7} theme={"theme":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} DEFINE CONNECTOR 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 "Line1.Temp" WITH DATA_TYPE "FLOAT" ``` The node is then `ns=2;s=Line1.Temp`. > [!WARNING] > > The Coreflux [OPC UA client](/latest/lot-language/routes/industrial/opcua-client) treats any address with a slash (`/`) as a browse path, not a NodeId. It can't read back a default node such as `ns=2;s=PlantServer/Temperature`, or a dynamic node named after its topic. If a Coreflux OPC UA client reads this server, set an explicit `ADDRESS` without slashes, such as `"Line1.Oee"`. Other OPC UA clients, such as UaExpert, are not affected. ### Writable tag parameters For writable tags, configure outbound formatting the same way as on the client connector: **WRITABLE** `boolean` (default: false) Allow OPC UA clients to write this node. Writes publish to the tag's `SOURCE_TOPIC`. **DECIMAL\_PLACES** `integer` (default: 2) Decimal places applied to `FLOAT`/`DOUBLE` values when a client write is published to MQTT. **MIN\_VALUE** `double` Minimum value after a client write. Values below this are clamped. **MAX\_VALUE** `double` Maximum value after a client write. Values above this are clamped. **PERSISTENCE** `boolean` Per-tag override of the connector persistence default. Precedence: tag → mapping → connector. See [Data persistence & recovery](#data-persistence--recovery). ### Supported data types Use the same `DATA_TYPE` values as the [OPC UA client connector](./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). > [!NOTE] > > **String sizing:** `STRING` nodes are natively variable-length in OPC UA. The `OPCUA_SERVER` connector 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 connector 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 connector 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` | > [!NOTE] > > Combine static and dynamic mappings in the same connector. 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 connector 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":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} DEFINE CONNECTOR SensorGateway WITH TYPE OPCUA_SERVER ADD OPCUA_SERVER_CONFIG WITH SERVER_PORT 4840 ADD MAPPING Sensors WITH SOURCE_TOPIC "sensors/+/value" ``` **Sequence:** 1. Connector 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 connector starts are seeded the same way as for static tags—matching topics create nodes at startup. ### Dynamic node limits > [!WARNING] > > Dynamic nodes are not removed while the connector is running. They persist until the connector 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. **DATA\_TYPE** `string` Set to `"ARRAY"` for list values. **ARRAY\_ELEMENT\_TYPE** `string` (default: INT16) Type of each element: `BOOL`, `INT16`, `UINT16`, `INT32`, `UINT32`, `INT64`, `UINT64`, `FLOAT`, `DOUBLE`, or `STRING`. **ARRAY\_SIZE** `integer` 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":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} DEFINE CONNECTOR 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`. **DATA\_TYPE** `string` Set to `"STRUCT"` for object payloads. **STRUCT\_FIELD\_DEFINITIONS** `string` 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 connector (`name:offset:type:size`), the server uses the name and type only; offset and size are ignored. ```lot wrap focus={4-9} theme={"theme":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} DEFINE CONNECTOR 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. > [!NOTE] > > `DECIMAL_PLACES`, `MIN_VALUE`, and `MAX_VALUE` apply to scalar writable tags only—not to individual struct fields. *** ## Security Examples **Anonymous (development)** For lab or trusted networks where encryption is not required: ```lot wrap focus={3-4} theme={"theme":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} DEFINE CONNECTOR 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" ``` **Username and encryption** Require credentials and encrypt traffic: ```lot wrap focus={4-9} theme={"theme":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} DEFINE CONNECTOR 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 ``` **Certificate user auth** Accept X.509 certificates for client login: ```lot wrap focus={4-9} theme={"theme":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} DEFINE CONNECTOR 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 connectors 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":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} ADD OPCUA_SERVER_CONFIG WITH SERVER_PORT 4840 WITH PKI_DIR "/data/opcua/pki" ``` > [!WARNING] > > 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":{"light":"css-variables","dark":"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:{connector}`): ```lot wrap focus={4-8} theme={"theme":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} DEFINE CONNECTOR 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 connector](./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":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} DEFINE CONNECTOR 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: **1. First connect** The client's certificate lands in `PKI_DIR/rejected/certs`. **2. Move to trusted** ```bash wrap theme={"theme":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} mv "$PKI_DIR"/rejected/certs/.der "$PKI_DIR"/trusted/certs/ ``` **3. Reconnect** 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 connector 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":{"light":"css-variables","dark":"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 connector; 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 connector 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":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} DEFINE CONNECTOR 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) connector 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 connector default: ```lot wrap theme={"theme":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} DEFINE CONNECTOR 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":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} DEFINE CONNECTOR 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. > [!WARNING] > > 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 connector'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 **1. Deploy the connector** Add the connector definition to your broker and confirm connector status shows **Connected** (the endpoint is listening). **2. Connect with an OPC UA client** 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). **3. Confirm MQTT → node** Publish a test value to a bound topic—for example `factory/line1/Temperature`. Refresh the client and confirm the node value updates. **4. Confirm node → MQTT** 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 > [!WARNING] > > Each `OPCUA_SERVER` connector binds exclusively to its `SERVER_PORT`. A second connector on the same port fails to start and reports a red connector status. *** ## Troubleshooting > **Connector status Red — failed to start server** > > * **Port in use** — Another process or server connector 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). > **Client rejects the server certificate on every reconnect** > > 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 certificate rejected** > > * **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 > **Strict client refuses the security handshake** > > `APPLICATION_URI` must match the certificate SAN URI. Leave both at their defaults, or change them together so they stay in sync. > **Client write has no effect** > > * 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. > **Client activity logging** > > The broker logs client activity for this connector 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. > **Anonymous connection rejected** > > `ALLOW_ANONYMOUS` is `false`. Connect with the configured username and password, or use certificate authentication if enabled. > **Node reads BadWaitingForInitialData** > > The `SOURCE_TOPIC` has not published yet (or no retained value). Publish the topic, enable connector 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 > **Persist PKI in production** > > 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). > **Disable anonymous access** > > Set `ALLOW_ANONYMOUS false` on plant networks and require username/password or certificate authentication. Anonymous endpoints are appropriate for lab or isolated development only. > **Reserve ports** > > Each server connector binds exclusively to its `SERVER_PORT`. Document which connector owns which port to avoid startup conflicts when adding connectors. > **Use secrets for credentials** > > Prefer `GET ENV` and `GET SECRET` over literals in connector definitions. Never commit passwords or client credentials to version control. > **Match security to the client** > > 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. > **Enable persistence for client-written setpoints** > > 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 > **[OPC UA Client Connector](./opcua-client)** > > Connect to remote OPC UA servers and poll device data into MQTT. > **[Industrial Connectors Overview](./overview)** > > Compare protocols and choose the right industrial connector for your architecture. # Industrial Connectors 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 connectors ## Why Industrial Connectors? 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 connectors 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. > [!TIP] > > **Like a translator at an international meeting.** Industrial connectors 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 > **[Modbus TCP Client](./modbus-tcp-client)** > > Poll PLCs and industrial devices over Modbus TCP. > **[Modbus Serial Client](./modbus-serial-client)** > > RS-232/RS-485 serial communication with Modbus RTU devices. > **[Modbus TCP Server](./modbus-tcp-server)** > > Expose MQTT topics as Modbus registers and coils for SCADA and HMIs. > **[Siemens S7](./siemens-s7)** > > Native S7 protocol for Siemens S7-200/300/400/1200/1500 PLCs. > **[OPC UA Client](./opcua-client)** > > Connect to remote OPC UA servers and poll device data into MQTT. > **[OPC UA Server](./opcua-server)** > > Expose MQTT topics as OPC UA nodes for SCADA and OT clients. > **[ADS (Beckhoff)](./ads)** > > TwinCAT ADS protocol for Beckhoff PLCs with AutoDiscovery support. > **[Allen-Bradley](./allen-bradley)** > > ControlLogix, CompactLogix, and other Rockwell Automation PLCs. > **[EtherNet/IP](./ethernetip)** > > CIP-based communication for Allen-Bradley and compatible devices. > **[FINS (Omron)](./fins)** > > Omron FINS protocol for CJ/CS/CP series PLCs. > **[BACnet](./bacnet)** > > 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 connectors share a common architecture with three key components: ```mermaid theme={"theme":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} graph LR subgraph broker [Coreflux Broker] Connector[Industrial Connector] 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 Connector --> 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":{"light":"css-variables","dark":"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":{"light":"css-variables","dark":"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: > **SOURCE\_TOPIC** > > **SOURCE\_TOPIC** `string` > > Base MQTT topic for publishing tag data. Each TAG publishes to `{SOURCE_TOPIC}/{TAG_NAME}`. > **EVERY** > > **EVERY** `string` *required* > > 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 > **TAG capabilities** > > * **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":{"light":"css-variables","dark":"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: > **Addressing** > > **ADDRESS** `string` *required* > > Protocol-specific address (register number, variable path, NodeId, etc.). > > **ADDRESS\_TYPE** `string` > > Address type varies by protocol (e.g., HOLDING\_REGISTER, DATABLOCK, SYMBOL). > **Data Types** > > **DATA\_TYPE** `string` *required* > > Data type for the value. Common types: `BOOL`, `INT16`, `UINT16`, `INT32`, `UINT32`, `FLOAT`, `DOUBLE`, `STRING`. > > **LENGTH** `integer` > > Maximum string size when `DATA_TYPE` is `STRING` on connectors that support configurable sizing. Units and defaults vary—see [String sizing by Connector](#string-sizing-by-connector). Preferred keyword; `STRING_SIZE` is accepted as an alias where sizing is supported. > **Topics** > > **SOURCE\_TOPIC** `string` > > Topic where PLC values are published. Subscribe here to receive sensor data. > > **DESTINATION\_TOPIC** `string` > > Topic to send write commands. Publish a value here to write it to the PLC (requires WRITABLE true). > **Value Transformation** > > **SCALING** `double` > > Multiplier applied to raw value. Formula: `published_value = (raw_value * SCALING) + OFFSET`. Default: 1.0. > > **OFFSET** `double` > > Value added after scaling. Default: 0.0. > > **DECIMAL\_PLACES** `integer` > > Number of decimal places in published value. Default: 2. > **Filtering** > > **MIN\_VALUE** `double` > > Minimum allowed value. Values below this are filtered out. > > **MAX\_VALUE** `double` > > Maximum allowed value. Values above this are filtered out. > > **DEADBAND** `double` > > Minimum change required to publish a new value. Reduces network traffic for slowly changing values. Default: 0.0. > **Publishing Options** > > **PUBLISH\_MODE** `string` > > Output format: `VALUE_ONLY` (just the value) or `JSON` (structured object with metadata). Default: VALUE\_ONLY. > > **UNIT** `string` > > Engineering unit for documentation and JSON output (e.g., °C, bar, RPM). > > **DESCRIPTION** `string` > > Human-readable description of the TAG. > **Write Control** > > **WRITABLE** `boolean` > > Allow writing to this TAG via DESTINATION\_TOPIC. Default: false. > **Byte Ordering** > > **BYTE\_ORDER** `string` > > Byte order for multi-byte values: `BIGENDIAN` or `LITTLEENDIAN`. Default: BIGENDIAN. > > **WORD\_ORDER** `string` > > Word order for 32-bit values: `BIGENDIAN` or `LITTLEENDIAN`. Default: BIGENDIAN. ### String sizing by Connector When `DATA_TYPE` is `STRING`, connectors 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. | Connector | 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 connector 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). **Group-level (Recommended)** Define `SOURCE_TOPIC` at the MAPPING level. Each TAG automatically publishes to `{SOURCE_TOPIC}/{TAG_NAME}`. ```lot wrap theme={"theme":{"light":"css-variables","dark":"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`. **Individual TAG** Define `SOURCE_TOPIC` at each TAG level for custom topic structure. ```lot wrap theme={"theme":{"light":"css-variables","dark":"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 connectors. **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":{"light":"css-variables","dark":"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: > **SOURCE\_TOPIC** > > **SOURCE\_TOPIC** `string` *required* > > MQTT topic where the EVENT listens for trigger messages. Publish a message here to execute the operation. > **DESTINATION\_TOPIC** > > **DESTINATION\_TOPIC** `string` *required* > > MQTT topic where the EVENT publishes the result of the operation. > **QUERY** > > **QUERY** `string` *required* > > 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 | > [!NOTE] > > Most real-world connectors combine both patterns: MAPPING for continuous monitoring and EVENT for on-demand operations within the same `DEFINE CONNECTOR`. *** ## Basic Example Pattern ### Cyclic Only This minimal pattern works across all industrial protocols for continuous data acquisition: ```lot wrap theme={"theme":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} DEFINE CONNECTOR 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 connector 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":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} DEFINE CONNECTOR 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 connectors. ### 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":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} DEFINE CONNECTOR 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":{"light":"css-variables","dark":"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 ``` *** ## Connection and Writes ### Connection status Every OT connector reports one of these states: * **Connected**: the device has answered. A route doesn't show Connected just because its socket is open. * **Reconnecting**: reads keep failing with connection errors. The route drops the connection and rebuilds it with backoff (up to 30 s between attempts). The first successful read sets it back to Connected. By default, 3 read cycles in a row that fail with a connection error move a route to Reconnecting. Data errors, such as an invalid address, don't change the state. A route restarted after `RECONNECT_MAX_ATTEMPTS` ran out starts retrying again. | Connector | When it reports a lost device | | - | - | | [FINS](/latest/lot-language/routes/industrial/fins) | UDP: after 2 commands in a row get no reply after all `RETRIES`. TCP: when no attempt of a command is answered, the connection is closed and reopened. Connected only after the PLC answers a probe (UDP) or the node handshake succeeds (TCP). | | [EtherNet/IP](/latest/lot-language/routes/industrial/ethernetip) | When the device doesn't reply within `READ_WRITE_TIMEOUT` or closes the connection. | | [Allen-Bradley](/latest/lot-language/routes/industrial/allen-bradley) | When a read or write outlives the safety timeout: 2 × `TIMEOUT` plus a margin. | | [BACnet](/latest/lot-language/routes/industrial/bacnet) | Default rule (3 failures). Only failures on the current connection count. | | [Siemens S7](/latest/lot-language/routes/industrial/siemens-s7) | When the connect doesn't finish within `READ_TIMEOUT`, or a read fails with "Plc is not connected". Connected only once the PLC has answered. | | [ADS](/latest/lot-language/routes/industrial/ads) | When a request times out or fails with an ADS error that means the PLC is unreachable (for example timeout, or target port or machine not found). | | [OPC UA client](/latest/lot-language/routes/industrial/opcua-client) | A read interrupted by a reconnect or certificate rotation marks the tags BAD and backs off like a lost connection. | | [Modbus TCP client](/latest/lot-language/routes/industrial/modbus-tcp-client) | When the connection stops answering altogether, it is closed and reopened. A slow read fails and the connection stays open. | | Other OT connectors | Default rule. | ### Write results A write to a writable tag with a `DESTINATION_TOPIC` publishes its result on `{DESTINATION_TOPIC}/result`: | Case | Payload | | - | - | | Written | `{"success": true, "route_name", "tag", "address", "value", "timestamp"}` | | Failed | `{"success": false, "route_name", "error", "timestamp"}`, plus `cip_status` on EtherNet/IP CIP errors | | Outcome unknown | `{"success": false, "outcome": "unknown", "route_name", "tag", "address", "error", "timestamp"}` | `"outcome": "unknown"` means the device didn't answer in time but may still apply the write. `success` stays `false`, so existing flows still treat the write as unconfirmed. Read the tag back before you retry, or the value may be written twice. | Connector | Publishes `outcome: "unknown"` | | - | - | | Allen-Bradley | Yes, when a write outlives the safety timeout | | FINS, EtherNet/IP, ADS, BACnet, Siemens S7, others | Not yet. A timed-out write is reported as a plain failure. | *** ## Best Practices > **Optimize Polling Intervals** > > 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":{"light":"css-variables","dark":"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 > ``` > **Use Deadband for Analog Values** > > Reduce network traffic for slowly changing values: > > ```lot wrap theme={"theme":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} > ADD TAG Temperature > WITH DEADBAND 0.5 // Only publish if change > 0.5 > ``` > **Apply Scaling at the Edge** > > Transform raw PLC values to engineering units in the connector: > > ```lot wrap theme={"theme":{"light":"css-variables","dark":"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" > ``` > **Use JSON Mode for Rich Data** > > Include metadata in published messages: > > ```lot wrap theme={"theme":{"light":"css-variables","dark":"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":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} > { > "value": 24.5, > "unit": "°C", > "description": "Main reactor temperature", > "timestamp": "2024-01-15T10:30:00Z" > } > ``` > **Secure Write Operations** > > Only enable WRITABLE for TAGs that need it, and use specific topics: > > ```lot wrap theme={"theme":{"light":"css-variables","dark":"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: > **[Modbus TCP Client](./modbus-tcp-client)** > > Start with the most common industrial protocol. > **[Siemens S7](./siemens-s7)** > > Connect to Siemens PLCs directly. # Siemens S7 Connector 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` connector 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. > [!TIP] > > The S7 connector uses native protocol communication, which is typically faster and more efficient than OPC UA for Siemens PLCs. ## Basic Syntax ```lot wrap theme={"theme":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} DEFINE CONNECTOR 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 | > [!NOTE] > > 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 **IP** `string` *required* S7 PLC IP address. **CPU\_TYPE** `string` *required* CPU type: `S7200`, `S7300`, `S7400`, `S71200`, or `S71500`. **RACK** `integer` PLC rack number. Default: 0. **SLOT** `integer` PLC slot number. Default: 1 for S7-1200/1500, 2 for S7-300/400. **CONNECTION\_TYPE** `string` Connection type: `PG` (Programming device), `OP` (Operator Panel), or `S7BASIC`. Default: PG. **RETRY\_ATTEMPTS** `integer` Number of retry attempts on failure. Default: 3. **RETRY\_TIME\_SECONDS** `integer` Seconds to wait between retries. Default: 5. **REFRESH\_TIME\_MS** `integer` Refresh time in milliseconds. Default: 1000. **READ\_TIMEOUT** `integer` Read timeout in milliseconds. Default: 5000. It also limits the connection attempt: a PLC that accepts TCP but doesn't answer within this time is reported as not connected, and the route retries. The PLC socket also uses `READ_TIMEOUT` and `WRITE_TIMEOUT`, not the library's 10 s default, so a PLC or gateway that is slow to accept connections needs a higher `READ_TIMEOUT`. **WRITE\_TIMEOUT** `integer` Write timeout in milliseconds. Default: 5000. ### Connection Examples **S7-1500** ```lot wrap theme={"theme":{"light":"css-variables","dark":"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" ``` **S7-1200** ```lot wrap theme={"theme":{"light":"css-variables","dark":"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 ``` **S7-300** ```lot wrap theme={"theme":{"light":"css-variables","dark":"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 ``` **S7-400** ```lot wrap theme={"theme":{"light":"css-variables","dark":"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 **ADDRESS\_TYPE** `string` Memory area type: `DATABLOCK`, `MEMORY`, `INPUT`, `OUTPUT`, `TIMER`, `COUNTER`. > [!NOTE] > > 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 connector 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":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} DEFINE CONNECTOR 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 Connector](./overview#string-sizing-by-connector) on the industrial overview. *** ## TAG Configuration ### Complete TAG Example ```lot wrap theme={"theme":{"light":"css-variables","dark":"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 > **Address Configuration** > > **ADDRESS** `string` *required* > > S7 address in standard notation (e.g., `DB1.DBW100`, `M0.0`, `I0.0`, `Q0.0`). > > **ADDRESS\_TYPE** `string` > > Address type: `DATABLOCK`, `MEMORY`, `INPUT`, `OUTPUT`, `TIMER`, `COUNTER`. > > **DATA\_TYPE** `string` *required* > > S7 data type: `BOOL`, `BYTE`, `WORD`, `DWORD`, `INT`, `DINT`, `REAL`, `STRING`. > > **LENGTH** `integer` (default: 256) > > **STRING only.** Total string buffer size in bytes, including the two-byte S7 header. Alias: `STRING_SIZE`. > **Value Transformation** > > **SCALING** `double` > > Multiplier applied to raw value. Default: 1.0. > > **OFFSET** `double` > > Value added after scaling. Default: 0.0. > > **DECIMAL\_PLACES** `integer` > > Number of decimal places in output. Default: 2. > **Filtering** > > **MIN\_VALUE** `double` > > Minimum allowed value. > > **MAX\_VALUE** `double` > > Maximum allowed value. > > **DEADBAND** `double` > > Minimum change to trigger publish. Default: 0.0. > **Publishing** > > **SOURCE\_TOPIC** `string` > > Topic where PLC values are published. Subscribe here to receive sensor data. > > **PUBLISH\_MODE** `string` > > Output format: `VALUE_ONLY` or `JSON`. Default: VALUE\_ONLY. > > **UNIT** `string` > > Engineering unit (e.g., °C, bar, RPM). > **Write Configuration** > > **WRITABLE** `boolean` > > Allow writing to this address. Default: false. > > **DESTINATION\_TOPIC** `string` > > 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 connector 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":{"light":"css-variables","dark":"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":{"light":"css-variables","dark":"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 **Basic Data Block Reading** Read values from a data block: ```lot wrap theme={"theme":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} DEFINE CONNECTOR 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" ``` **Memory and I/O** Read markers, inputs, and outputs: ```lot wrap theme={"theme":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} DEFINE CONNECTOR 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" ``` **Bidirectional Control** Read process values and write setpoints: ```lot wrap theme={"theme":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} DEFINE CONNECTOR 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" ``` **Multiple Polling Rates** Optimize with different update frequencies: ```lot wrap theme={"theme":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} DEFINE CONNECTOR 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" ``` **Combined (Cyclic + On-Demand)** Continuous monitoring with on-demand read and write events in the same connector: ```lot wrap theme={"theme":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} DEFINE CONNECTOR 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 > [!WARNING] > > 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 > **Connection Refused** > > * 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 > **Wrong Slot Error** > > Use correct slot for your CPU: > > * S7-1200/1500: Slot 1 > * S7-300: Slot 2 > * S7-400: Slot 2 or 3 (check hardware config) > **Optimized Block Access Error** > > For S7-1500: Disable optimized block access in TIA Portal for data blocks you want to access. > **Incorrect Values** > > * 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 > **Write Fails** > > * 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 > **[OPC UA Client](./opcua-client)** > > Cross-platform industrial communication. > **[ADS (Beckhoff)](./ads)** > > Connect to TwinCAT systems. # Email Connector 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` connector 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. > [!TIP] > > **Like an auto-reply rule in your mailbox.** When a message lands on a topic you're watching, the connector 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 connector, starting with the most common scenarios. ## Quick Start **Critical alert** Send an immediate alert to a fixed team address when a critical event arrives: ```lot wrap theme={"theme":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} DEFINE CONNECTOR 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" ``` **Dynamic recipient** Send to an address taken from the message payload: ```lot wrap theme={"theme":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} DEFINE CONNECTOR 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}" ``` **Report with images** Embed images (a logo, a chart) directly in the email: ```lot wrap theme={"theme":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} DEFINE CONNECTOR 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" ``` > [!NOTE] > > 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 connector which mail server to send through and how to authenticate. > **Server Settings** > > **HOST** `string` *required* > > SMTP server address (e.g., `smtp.gmail.com`, `smtp.office365.com`). > > **PORT** `integer` *required* > > SMTP port (typically 587 for TLS, 465 for SSL). > > **USE\_TLS** `boolean` > > Enable TLS encryption. Recommended: true. > **Authentication** > > **USERNAME** `string` *required* > > Email account to send from. > > **PASSWORD** `string` *required* > > Email password or app-specific password. *** ## Event Configuration Each `ADD EVENT` block defines a trigger topic and the content of the email it sends. > **Trigger & Routing** > > **SOURCE\_TOPIC** `string` > > MQTT topic to trigger email sending. > > **DESTINATION\_TOPIC** `string` > > MQTT topic to publish send status. > **Email Content** > > **TEMPLATE\_PATH** `string` > > Path to HTML email template file. > > **SUBJECT** `string` > > Email subject line (supports placeholders). > > **RECIPIENT** `string` > > 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 > **Email Not Sending** > > * 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 > **Email Authentication Failed** > > * 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 > **[LoT Actions](../../actions/overview)** > > Publish the MQTT events that trigger notifications. > **[Data Storage Connectors](../data-storage/overview)** > > Store MQTT data in databases. # Connectors 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 Connectors ## What are Connectors? Connectors 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. > [!NOTE] > > Write `DEFINE CONNECTOR` and deploy it with `-addConnector`. `DEFINE ROUTE` and `-addRoute` remain supported aliases, as do the other Route command spellings. The broker stores each definition exactly as you typed it. Overload settings use `ADD CONNECTOR_CONFIG`; `ADD ROUTE_CONFIG` remains a supported alias. Older brokers accept only `DEFINE ROUTE` and `-addRoute`. See [Connector command spellings](/latest/mqtt-broker/commands#connector-command-spellings). > [!TIP] > > **Like plugging a cable into a device.** A Connector is the cable between your broker and an external system — a database, a PLC, or another broker. ![Architecture diagram showing the Coreflux MQTT broker at the center connected to databases, PLCs, REST APIs, cloud brokers, email, and AI through Connectors](https://mintcdn.com/coreflux/7tBTxEEkDVX4IRGH/images/product-overview-diagram.png?fit=max\&auto=format\&n=7tBTxEEkDVX4IRGH\&q=85\&s=8e40e7ae59a9ca0254a45a115bc4f448) Connectors connect your Coreflux MQTT broker to databases, industrial equipment, APIs, cloud services, and more ## Connector Categories > **[System Connectors](./system-routes)** > > Configure MQTT bridges and Sparkplug roles. > **[Streaming Connectors](./streaming/kafka)** > > Connect MQTT topics to Apache Kafka. > **[gRPC Connectors](./streaming/grpc)** > > Call an external gRPC service from MQTT (`TYPE GRPC`), with one bidirectional call per device topic by default. > **[Notification Connectors](./notifications/email)** > > Send alerts and reports to people from MQTT events. > **[Data Storage Connectors](./data-storage/overview)** > > Store MQTT data in PostgreSQL, MySQL, MongoDB, OpenSearch, Snowflake, and more. > **[REST API Connectors](./rest-api-routes)** > > HTTP client/server for external API integration and webhook endpoints. > **[Industrial Connectors](./industrial/overview)** > > Connect to PLCs via Modbus, Siemens S7, OPC UA, ADS, Allen-Bradley, and FINS. > **[MCP Connectors](./mcp-routes)** > > Connect *out* to external MCP servers (`TYPE MCP` / `CALL MCP`). To expose this broker as an MCP server, see [MCP Server](/latest/mqtt-broker/mcp-server). > **[AI Connectors and Agents](./ai-routes)** > > Call a model with a TYPE LLM connector, or define an agent that uses it. > **[Media Connectors](./media/overview)** > > Decode barcodes, capture camera frames, and publish microphone audio. > **[Overload configuration](./store-and-forward)** > > Choose wait or drop when a connector cannot keep up with incoming MQTT. This page covers the fundamentals of connector configuration. Choose a specific connector category above for detailed syntax and examples. *** ## Basic Syntax ```lot wrap theme={"theme":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} DEFINE CONNECTOR WITH TYPE ADD WITH "" ADD MAPPING WITH SOURCE_TOPIC "" WITH DESTINATION_TOPIC "" ``` ### Components **connector\_name** `string` *required* Unique identifier for the connector. Use descriptive names like `CloudBridge`, `SensorDataIndexer`, `AlertEmailer`. **connector\_type** `type` *required* The type of connector (see Available Connector Types below). **CONFIG\_SECTION** `block` Configuration blocks specific to the connector type (e.g., `MQTT_CONFIG`, `MONGODB_CONFIG`, `REST_API_CONFIG`). **CONNECTOR\_CONFIG** `block` Optional. Controls wait-or-drop behavior when this connector cannot keep up with incoming MQTT. Works on every connector type. `ADD ROUTE_CONFIG` remains a supported alias. See [Store and Forward Configuration](./store-and-forward). **MAPPING** `block` Defines topic mappings for data flow between source and destination. ### Disable a connector Add `DISABLE true` on its own line to keep a connector defined but stopped: ```lot wrap theme={"theme":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} DEFINE CONNECTOR PlantHistorian WITH TYPE POSTGRESQL DISABLE true ADD SQL_CONFIG WITH SERVER "historian.plant.local" ``` A disabled connector stays listed and its definition is still published, but it isn't started. It opens no connection, maps no topics, and uses no action slots. `DISABLE false`, or no `DISABLE` line, means enabled. The state survives a restart. To switch it without editing the code, use `-disableConnector ` and `-enableConnector ` (aliases `-disableRoute` / `-enableRoute`). `-disableConnector` refuses a `TYPE LLM` connector that agents still use: disable those agents first. See [Disable and enable](/latest/mqtt-broker/commands#disable-and-enable). *** ## Available Connector Types **System** | Connector Type | Description | | - | - | | `MQTT_BRIDGE` | Broker-to-broker topic synchronization | | `SPARKPLUG_B` | Sparkplug decoding and edge-node publishing | | `SPARKPLUG_HOST` | Sparkplug Primary Host Application | **Streaming** | Connector Type | Description | | - | - | | `KAFKA` | Stream data both ways with Apache Kafka | | `GRPC` | Call an external gRPC service from MQTT topics (unary and streaming) | **Notification** | Connector Type | Description | | - | - | | `EMAIL` | Send templated emails from MQTT messages | **Data Storage** | Connector 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 | **Industrial** | Connector 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 | **Other** | Connector 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 and decoding | | `VIDEO_INPUT` | JPEG capture from a camera, stream, or file | | `VIDEO_OUTPUT` | HTTP MJPEG from MQTT frames | | `AUDIO_INPUT` | Raw PCM capture from a microphone, tone, silence, or file | *** ## Quick Examples **Email Alerts** Send email notifications from MQTT events: ```lot wrap theme={"theme":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} DEFINE CONNECTOR 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" ``` **Modbus TCP** Read data from PLCs via Modbus: ```lot wrap theme={"theme":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} DEFINE CONNECTOR 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" ``` **REST API** Connect to external REST APIs: ```lot wrap theme={"theme":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} DEFINE CONNECTOR 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}" ``` **PostgreSQL Storage** Store MQTT data in PostgreSQL: ```lot wrap theme={"theme":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} DEFINE CONNECTOR 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}')" ``` **MQTT Bridge** Connect two brokers and sync topics: ```lot wrap theme={"theme":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} DEFINE CONNECTOR 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 Connector Configuration Connectors share two common building blocks: **MQTT configuration** and **topic mappings**. You'll see these across most connector types, so understanding them here saves time when setting up any specific connector. ### Connecting to a Broker Connectors that communicate with MQTT brokers — like bridges — need to know which broker to talk to. The simplest option is `BROKER SELF`, which tells the connector to use your local Coreflux broker: ```lot wrap theme={"theme":{"light":"css-variables","dark":"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":{"light":"css-variables","dark":"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" ``` > [!NOTE] > > Not every connector type requires an `MQTT_CONFIG` block. Database connectors, email connectors, and REST connectors have their own configuration sections. Check the specific connector page for details. ### Controlling Data Flow with Mappings Mappings define **which MQTT topics** flow through a connector and **in which direction**. For example, to push local sensor readings to a cloud broker: ```lot wrap theme={"theme":{"light":"css-variables","dark":"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 connector cannot keep up Incoming MQTT can outpace the destination. Add `ADD CONNECTOR_CONFIG` on any connector to choose whether that connector waits or drops the newest message (`ADD ROUTE_CONFIG` remains a supported alias). Connectors without the block use the broker default (`balanced`). See [Store and Forward Configuration](./store-and-forward). *** ## Connector Status Monitoring Every connector 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 connector is defined. > [!NOTE] > > 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 connector 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":{"light":"css-variables","dark":"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 connector name as defined in LoT | | `type` | string | The connector 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 connector 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 connector has been running (`HH:MM:SS`) | | `connectedSince` | string | ISO 8601 timestamp of when the connector established its current connection | | `timestamp` | string | ISO 8601 timestamp of when this status update was generated | ### Checking Connector Status You can monitor any connector'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 connectors at once. *** ## Use Cases **Edge-to-Cloud Sync** Bridge local edge data to cloud brokers: ```lot wrap theme={"theme":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} DEFINE CONNECTOR 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" ``` **Industrial Integration** Read data from PLCs via multiple protocols: ```lot wrap theme={"theme":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} DEFINE CONNECTOR 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" ``` **Real-Time Analytics** Store sensor data for analysis: ```lot wrap theme={"theme":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} DEFINE CONNECTOR 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}}}" ``` **Alert Notifications** Email stakeholders when critical events occur: ```lot wrap theme={"theme":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} DEFINE CONNECTOR 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 > **[Industrial Connectors](./industrial/overview)** > > Connect to PLCs and industrial equipment. > **[Data Storage](./data-storage)** > > Store MQTT data in databases. # Acumatica Cloud ERP REST API Source: https://docs.coreflux.org/latest/lot-language/routes/rest-api-apps/acumatica-cloud-erp Client-mode REST API connector for Acumatica contract-based REST (Production Orders, Stock Items, Adjustments) Technical Preview ## Overview Client-mode REST API connector for Acumatica contract-based REST (Production Orders, Stock Items, Adjustments). `BASE_ADDRESS` defaults to `https://YOUR_ACUMATICA_HOST`. Auth: **Session cookie (POST /entity/auth/login) or OAuth 2.0 Bearer**. Pass secrets only at runtime via MQTT JSON placeholders or Hub `GET SECRET` — never commit real credentials. ## Auth Session cookie (POST /entity/auth/login) or OAuth 2.0 Bearer. Prefer Hub secrets / `GET SECRET`. Never hard-code tokens into the `.lot`. ## Basic Syntax ```lot wrap theme={"theme":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} DEFINE CONNECTOR AcumaticaCloudErpRestApi WITH TYPE REST_API ADD REST_API_CONFIG WITH BASE_ADDRESS "https://YOUR_ACUMATICA_HOST" WITH ENABLE_CLIENT "true" WITH USE_SSL "true" WITH REQUEST_TIMEOUT '30' WITH DEFAULT_CONTENT_TYPE "application/json" # Auth: Authorization Bearer {value.json.access_token} — prefer Hub secrets / GET SECRET. # Optional Basic: WITH USERNAME "YOUR_USER" / WITH PASSWORD GET SECRET "APP_PASSWORD" ADD EVENT Login WITH SOURCE_TOPIC "integrations/acumatica-cloud-erp/request/Login" WITH DESTINATION_TOPIC "integrations/acumatica-cloud-erp/response/Login" WITH METHOD "POST" WITH ENDPOINT "/entity/auth/login" WITH HEADERS ADD "Content-Type" "application/json" ADD "Authorization" "Bearer {value.json.access_token}" WITH BODY "{payload}" ADD EVENT Logout WITH SOURCE_TOPIC "integrations/acumatica-cloud-erp/request/Logout" WITH DESTINATION_TOPIC "integrations/acumatica-cloud-erp/response/Logout" WITH METHOD "POST" WITH ENDPOINT "/entity/auth/logout" WITH HEADERS ADD "Content-Type" "application/json" ADD "Authorization" "Bearer {value.json.access_token}" WITH BODY "{payload}" ADD EVENT Listproductionorders WITH SOURCE_TOPIC "integrations/acumatica-cloud-erp/request/Listproductionorders" WITH DESTINATION_TOPIC "integrations/acumatica-cloud-erp/response/Listproductionorders" WITH METHOD "GET" WITH ENDPOINT "/entity/Default/24.200.001/ProductionOrder" WITH HEADERS ADD "Accept" "application/json" # ... additional EVENTs in package .lot ... ``` ## MQTT topics Request topics under `integrations/acumatica-cloud-erp/request/...`; responses on `integrations/acumatica-cloud-erp/response/...`. ## Template reference Generated from the shipped broker template `acumatica-cloud-erp` (`-connectorCode ACUMATICA_CLOUD_ERP`). It lists every event the template defines. ### Auth Session cookie (POST /entity/auth/login) or OAuth 2.0 Bearer Auth headers used by the template: `Authorization: Bearer {value.json.access_token}`. ### Template parameters | Placeholder | Type | Description | Required | | - | - | - | - | | `RouteName` | string | Name of the Acumatica Cloud ERP connector | no | | `BaseUrl` | string | Acumatica Cloud ERP base URL without trailing slash (e.g. https\://YOUR\_ACUMATICA\_HOST) | yes | `BASE_ADDRESS` default: `https://YOUR_ACUMATICA_HOST`. ### Events | Event | Method | Endpoint | Request topic | Response topic | Payload fields | | - | - | - | - | - | - | | `Login` | POST | `/entity/auth/login` | `integrations/acumatica-cloud-erp/request/Login` | `integrations/acumatica-cloud-erp/response/Login` | `access_token`, `body` | | `Logout` | POST | `/entity/auth/logout` | `integrations/acumatica-cloud-erp/request/Logout` | `integrations/acumatica-cloud-erp/response/Logout` | `access_token`, `body` | | `Listproductionorders` | GET | `/entity/MANUFACTURING/24.200.001/ProductionOrder` | `integrations/acumatica-cloud-erp/request/Listproductionorders` | `integrations/acumatica-cloud-erp/response/Listproductionorders` | `access_token` | | `Getproductionorder` | GET | `/entity/MANUFACTURING/24.200.001/ProductionOrder/{value.json.id}` | `integrations/acumatica-cloud-erp/request/Getproductionorder` | `integrations/acumatica-cloud-erp/response/Getproductionorder` | `access_token`, `id` | | `Putmoveentry` | PUT | `/entity/MANUFACTURING/24.200.001/MoveEntry` | `integrations/acumatica-cloud-erp/request/Putmoveentry` | `integrations/acumatica-cloud-erp/response/Putmoveentry` | `access_token`, `body` | | `Liststockitems` | GET | `/entity/Default/24.200.001/StockItem` | `integrations/acumatica-cloud-erp/request/Liststockitems` | `integrations/acumatica-cloud-erp/response/Liststockitems` | `access_token` | | `Getstockitem` | GET | `/entity/Default/24.200.001/StockItem/{value.json.id}` | `integrations/acumatica-cloud-erp/request/Getstockitem` | `integrations/acumatica-cloud-erp/response/Getstockitem` | `access_token`, `id` | | `Putinventoryadjustment` | PUT | `/entity/Default/24.200.001/InventoryAdjustment` | `integrations/acumatica-cloud-erp/request/Putinventoryadjustment` | `integrations/acumatica-cloud-erp/response/Putinventoryadjustment` | `access_token`, `body` | ### Deploy Get the template card (JSON with `template` and `metadata`) from the broker, then deploy the connector: ```bash theme={"theme":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} mosquitto_pub -t '$SYS/Coreflux/Command' -m "-connectorCode ACUMATICA_CLOUD_ERP" ``` The deployable connector from the REST API app, with `BASE_ADDRESS` already filled in: ```lot theme={"theme":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} DEFINE CONNECTOR AcumaticaCloudErpRestApi WITH TYPE REST_API ADD REST_API_CONFIG WITH BASE_ADDRESS "https://YOUR_ACUMATICA_HOST" WITH ENABLE_CLIENT "true" WITH USE_SSL "true" WITH REQUEST_TIMEOUT '30' WITH DEFAULT_CONTENT_TYPE "application/json" ADD EVENT Login WITH SOURCE_TOPIC "integrations/acumatica-cloud-erp/request/Login" WITH DESTINATION_TOPIC "integrations/acumatica-cloud-erp/response/Login" WITH METHOD "POST" WITH ENDPOINT "/entity/auth/login" WITH HEADERS ADD "Content-Type" "application/json" ADD "Authorization" "Bearer {value.json.access_token}" WITH BODY "{value.json.body}" ADD EVENT Logout WITH SOURCE_TOPIC "integrations/acumatica-cloud-erp/request/Logout" WITH DESTINATION_TOPIC "integrations/acumatica-cloud-erp/response/Logout" WITH METHOD "POST" WITH ENDPOINT "/entity/auth/logout" WITH HEADERS ADD "Content-Type" "application/json" ADD "Authorization" "Bearer {value.json.access_token}" WITH BODY "{value.json.body}" ADD EVENT Listproductionorders WITH SOURCE_TOPIC "integrations/acumatica-cloud-erp/request/Listproductionorders" WITH DESTINATION_TOPIC "integrations/acumatica-cloud-erp/response/Listproductionorders" WITH METHOD "GET" WITH ENDPOINT "/entity/MANUFACTURING/24.200.001/ProductionOrder" WITH HEADERS ADD "Accept" "application/json" ADD "Authorization" "Bearer {value.json.access_token}" ADD EVENT Getproductionorder WITH SOURCE_TOPIC "integrations/acumatica-cloud-erp/request/Getproductionorder" WITH DESTINATION_TOPIC "integrations/acumatica-cloud-erp/response/Getproductionorder" WITH METHOD "GET" WITH ENDPOINT "/entity/MANUFACTURING/24.200.001/ProductionOrder/{value.json.id}" WITH HEADERS ADD "Accept" "application/json" ADD "Authorization" "Bearer {value.json.access_token}" ADD EVENT Putmoveentry WITH SOURCE_TOPIC "integrations/acumatica-cloud-erp/request/Putmoveentry" WITH DESTINATION_TOPIC "integrations/acumatica-cloud-erp/response/Putmoveentry" WITH METHOD "PUT" WITH ENDPOINT "/entity/MANUFACTURING/24.200.001/MoveEntry" WITH HEADERS ADD "Content-Type" "application/json" ADD "Authorization" "Bearer {value.json.access_token}" WITH BODY "{value.json.body}" ADD EVENT Liststockitems WITH SOURCE_TOPIC "integrations/acumatica-cloud-erp/request/Liststockitems" WITH DESTINATION_TOPIC "integrations/acumatica-cloud-erp/response/Liststockitems" WITH METHOD "GET" WITH ENDPOINT "/entity/Default/24.200.001/StockItem" WITH HEADERS ADD "Accept" "application/json" ADD "Authorization" "Bearer {value.json.access_token}" ADD EVENT Getstockitem WITH SOURCE_TOPIC "integrations/acumatica-cloud-erp/request/Getstockitem" WITH DESTINATION_TOPIC "integrations/acumatica-cloud-erp/response/Getstockitem" WITH METHOD "GET" WITH ENDPOINT "/entity/Default/24.200.001/StockItem/{value.json.id}" WITH HEADERS ADD "Accept" "application/json" ADD "Authorization" "Bearer {value.json.access_token}" ADD EVENT Putinventoryadjustment WITH SOURCE_TOPIC "integrations/acumatica-cloud-erp/request/Putinventoryadjustment" WITH DESTINATION_TOPIC "integrations/acumatica-cloud-erp/response/Putinventoryadjustment" WITH METHOD "PUT" WITH ENDPOINT "/entity/Default/24.200.001/InventoryAdjustment" WITH HEADERS ADD "Content-Type" "application/json" ADD "Authorization" "Bearer {value.json.access_token}" WITH BODY "{value.json.body}" ``` Deploy it with `-addConnector` (do not include `ADD METADATA`). ### Usage example Call `Login` by publishing a JSON payload to its request topic, and read the reply on the response topic: ```bash theme={"theme":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} mosquitto_sub -t 'integrations/acumatica-cloud-erp/response/Login' & mosquitto_pub -t 'integrations/acumatica-cloud-erp/request/Login' -m '{"access_token": "", "body": {}}' ``` ## Related * [REST API Connectors](/latest/lot-language/routes/rest-api-routes) * [REST API apps index](/latest/lot-language/routes/rest-api-apps) # Airtable Source: https://docs.coreflux.org/latest/lot-language/routes/rest-api-apps/airtable Client-mode REST API connector for a curated Airtable Web API v0 slice: create records, filterByFormula list, upsert by device id, and webhook create Technical Preview ## Overview A client-mode connector for a **curated IoT / low-code** slice of the [Airtable Web API](https://airtable.com/developers/web/api/introduction) under `https://api.airtable.com`: | Catalog connector event | Entity / path | Notes | | - | - | - | | | `GET /v0/meta/bases` | Bases visible to the token | | POST \{table} records | `POST /v0/{baseId}/{tableIdOrName}` | Device publishes state into a table | | GET \{table}?filterByFormula= | `GET /v0/{baseId}/{tableIdOrName}?filterByFormula=` | URL-encode the formula | | PATCH \{table} upsert by device id | `PATCH /v0/{baseId}/{tableIdOrName}` | `performUpsert.fieldsToMergeOn` | | (optional) webhooks | `POST /v0/bases/{baseId}/webhooks` | Client create; inbound ping is separate | `BASE_ADDRESS` is `https://api.airtable.com` (no trailing slash; endpoints already include `/v0/...`). Topics use `devops/airtable//`. Pass `baseId` and `tableIdOrName` on each MQTT payload (prefer table IDs). > [!NOTE] > > Upsert: set `performUpsert.fieldsToMergeOn` (for example `["Device Id"]`). Zero matches create; one match updates; multiple matches fail. > [!NOTE] > > Webhooks: this package ships **client create** only. Airtable POSTs a lightweight ping to `notificationUrl`. Inbound receive would need a separate `ENABLE_SERVER` connector. > [!NOTE] > > Prefer Hub secrets for tokens. ## Basic Syntax ```lot wrap theme={"theme":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} # Coreflux REST_API template — Airtable curated IoT / low-code slice # Auth: Bearer {value.json.api_token} (PAT or OAuth) # BASE_ADDRESS: https://api.airtable.com DEFINE CONNECTOR AirtableRestApi WITH TYPE REST_API ADD REST_API_CONFIG WITH BASE_ADDRESS "https://api.airtable.com" WITH ENABLE_CLIENT "true" WITH USE_SSL "true" WITH REQUEST_TIMEOUT '60' WITH DEFAULT_CONTENT_TYPE "application/json" ADD EVENT Listbases WITH SOURCE_TOPIC "devops/airtable/bases/list" WITH DESTINATION_TOPIC "devops/airtable/bases/list/response" WITH METHOD "GET" WITH ENDPOINT "/v0/meta/bases" WITH HEADERS ADD "Accept" "application/json" ADD "Authorization" "Bearer {value.json.api_token}" ADD EVENT Createrecords WITH SOURCE_TOPIC "devops/airtable/records/create" WITH DESTINATION_TOPIC "devops/airtable/records/create/response" WITH METHOD "POST" WITH ENDPOINT "/v0/{value.json.baseId}/{value.json.tableIdOrName}" WITH HEADERS ADD "Accept" "application/json" ADD "Content-Type" "application/json" ADD "Authorization" "Bearer {value.json.api_token}" WITH BODY "{payload}" ADD EVENT Listrecords WITH SOURCE_TOPIC "devops/airtable/records/list" WITH DESTINATION_TOPIC "devops/airtable/records/list/response" WITH METHOD "GET" WITH ENDPOINT "/v0/{value.json.baseId}/{value.json.tableIdOrName}?filterByFormula={value.json.filterByFormula}&maxRecords={value.json.maxRecords}" WITH HEADERS ADD "Accept" "application/json" ADD "Authorization" "Bearer {value.json.api_token}" ADD EVENT Upsertrecords WITH SOURCE_TOPIC "devops/airtable/records/upsert" WITH DESTINATION_TOPIC "devops/airtable/records/upsert/response" WITH METHOD "PATCH" WITH ENDPOINT "/v0/{value.json.baseId}/{value.json.tableIdOrName}" WITH HEADERS ADD "Accept" "application/json" ADD "Content-Type" "application/json" ADD "Authorization" "Bearer {value.json.api_token}" WITH BODY "{payload}" ADD EVENT Createwebhook WITH SOURCE_TOPIC "devops/airtable/webhooks/create" WITH DESTINATION_TOPIC "devops/airtable/webhooks/create/response" WITH METHOD "POST" WITH ENDPOINT "/v0/bases/{value.json.baseId}/webhooks" WITH HEADERS ADD "Accept" "application/json" ADD "Content-Type" "application/json" ADD "Authorization" "Bearer {value.json.api_token}" WITH BODY "{payload}" ``` ## Events | Event | Method | Endpoint | Request topic | Response topic | | - | - | - | - | - | | Listbases | GET | `/v0/meta/bases` | `devops/airtable/bases/list` | `devops/airtable/bases/list/response` | | Createrecords | POST | `/v0/{baseId}/{tableIdOrName}` | `devops/airtable/records/create` | `devops/airtable/records/create/response` | | Listrecords | GET | `/v0/{baseId}/{tableIdOrName}?filterByFormula=&maxRecords=` | `devops/airtable/records/list` | `devops/airtable/records/list/response` | | Upsertrecords | PATCH | `/v0/{baseId}/{tableIdOrName}` | `devops/airtable/records/upsert` | `devops/airtable/records/upsert/response` | | Createwebhook | POST | `/v0/bases/{baseId}/webhooks` | `devops/airtable/webhooks/create` | `devops/airtable/webhooks/create/response` | ## Auth **Personal access token or OAuth 2.0 Bearer — default in the template:** 1. Create a PAT at [airtable.com/create/tokens](https://airtable.com/create/tokens) (or an OAuth app) with scopes `data.records:read`, `data.records:write`, plus `schema.bases:read` for ListBases and `webhook:manage` for CreateWebhook. Add the target base as a resource. 2. Pass `api_token` on the MQTT request payload so headers resolve: * `Authorization: Bearer {value.json.api_token}` 3. Store the token in Hub secrets / `GET SECRET "AIRTABLE_TOKEN"`. Never commit real credentials. 4. Pass `baseId` and `tableIdOrName` on each call (prefer table IDs so renames do not break paths). ## Example payloads **List bases:** ```json theme={"theme":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} { "api_token": "YOUR_PAT_OR_OAUTH_TOKEN" } ``` **Create records:** ```json theme={"theme":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} { "api_token": "YOUR_PAT_OR_OAUTH_TOKEN", "baseId": "appXXXXXXXXXXXXXX", "tableIdOrName": "tblXXXXXXXXXXXXXX", "records": [ { "fields": { "Device Id": "line-a-sensor-01", "Temperature": 72.5, "Status": "OK" } } ] } ``` **List with filterByFormula (URL-encoded):** ```json theme={"theme":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} { "api_token": "YOUR_PAT_OR_OAUTH_TOKEN", "baseId": "appXXXXXXXXXXXXXX", "tableIdOrName": "tblXXXXXXXXXXXXXX", "filterByFormula": "%7BDevice%20Id%7D%3D%27line-a-sensor-01%27", "maxRecords": "50" } ``` **Upsert by device id:** ```json theme={"theme":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} { "api_token": "YOUR_PAT_OR_OAUTH_TOKEN", "baseId": "appXXXXXXXXXXXXXX", "tableIdOrName": "tblXXXXXXXXXXXXXX", "performUpsert": { "fieldsToMergeOn": ["Device Id"] }, "records": [ { "fields": { "Device Id": "line-a-sensor-01", "Temperature": 73.1, "Status": "WARN" } } ] } ``` **Create webhook:** ```json theme={"theme":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} { "api_token": "YOUR_PAT_OR_OAUTH_TOKEN", "baseId": "appXXXXXXXXXXXXXX", "notificationUrl": "https://example.com/airtable-ping", "specification": { "options": { "filters": { "dataTypes": ["tableData"], "recordChangeScope": "tblXXXXXXXXXXXXXX" } } } } ``` ## Rate limits and notes * Airtable enforces per-base rate limits; back off on HTTP 429. * Prefer table IDs over names. * `filterByFormula` must be URL-encoded; keep URLs under \~16k characters. * Deployable `.lot` must not include `ADD METADATA` (Hub uses `hub-metadata.lot` separately). ## Sources * [API introduction](https://airtable.com/developers/web/api/introduction) * [Authentication](https://airtable.com/developers/web/api/authentication) * [Create records](https://airtable.com/developers/web/api/create-records) * [List records](https://airtable.com/developers/web/api/list-records) * [Update multiple records (upsert)](https://airtable.com/developers/web/api/update-multiple-records) * [Create a webhook](https://airtable.com/developers/web/api/create-a-webhook) * [Webhooks overview](https://airtable.com/developers/web/api/webhooks-overview) ## Related * [REST API Connectors](/latest/lot-language/routes/rest-api-routes) * [REST API Apps](/latest/lot-language/routes/rest-api-apps) # Amazon Bedrock Control Source: https://docs.coreflux.org/latest/lot-language/routes/rest-api-apps/amazon-bedrock-control Connect Coreflux LoT to the Amazon Bedrock control plane to list and inspect foundation models and list inference profiles with a Bedrock API key Technical Preview ## Overview Client-mode REST API connectors for Amazon Bedrock: triage alarms and summarize shift logs with the Converse API, create Titan embeddings with InvokeModel, count tokens, screen text with a guardrail, and list foundation models and inference profiles, all with a Bedrock API key. This is one of the templates in the Amazon Bedrock REST API app (`amazon-bedrock`). Deploy it alongside the other connectors of that app when they depend on each other (for example a token connector plus an API connector). ## Auth Amazon Bedrock API key as Authorization: Bearer \{value.json.api\_key} (long-term ABSK..., short-term bedrock-api-key-...). Hub secret suggested AWS\_BEARER\_TOKEN\_BEDROCK + GET SECRET. Alternative: AWS SigV4 (service bedrock) computed outside the broker, documented in comments. Never commit keys. Auth headers used by the template: `Authorization: Bearer {value.json.api_key}`. ## Template parameters | Placeholder | Type | Description | Required | | - | - | - | - | | `RouteName` | string | Name of the Amazon Bedrock Control connector | no | | `BaseUrl` | string | Amazon Bedrock Control base URL without trailing slash (e.g. [https://bedrock.eu-west-1.amazonaws.com](https://bedrock.eu-west-1.amazonaws.com)) | yes | `BASE_ADDRESS` default: `https://bedrock.eu-west-1.amazonaws.com`. ## Events | Event | Method | Endpoint | Request topic | Response topic | Payload fields | | - | - | - | - | - | - | | `ListFoundationModels` | GET | `/foundation-models?byOutputModality={value.json.output_modality}&byProvider={value.json.provider}` | `ai/bedrock/foundation-model/list` | `ai/bedrock/foundation-model/list/response` | `api_key`, `output_modality`, `provider` | | `GetFoundationModel` | GET | `/foundation-models/{value.json.model_id}` | `ai/bedrock/foundation-model/get` | `ai/bedrock/foundation-model/get/response` | `api_key`, `model_id` | | `ListInferenceProfiles` | GET | `/inference-profiles?maxResults={value.json.max_results}&type={value.json.type}` | `ai/bedrock/inference-profile/list` | `ai/bedrock/inference-profile/list/response` | `api_key`, `max_results`, `type` | ## Deploy Get the template card (JSON with `template` and `metadata`) from the broker, then deploy the connector: ```bash theme={"theme":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} mosquitto_pub -t '$SYS/Coreflux/Command' -m "-connectorCode AMAZON_BEDROCK_CONTROL" ``` The deployable connector from the REST API app, with `BASE_ADDRESS` already filled in: ```lot theme={"theme":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} DEFINE CONNECTOR AmazonBedrockControl WITH TYPE REST_API ADD REST_API_CONFIG WITH BASE_ADDRESS "https://bedrock.eu-west-1.amazonaws.com" WITH ENABLE_CLIENT "true" WITH USE_SSL "true" WITH REQUEST_TIMEOUT '60' WITH DEFAULT_CONTENT_TYPE "application/json" ADD EVENT ListFoundationModels WITH SOURCE_TOPIC "ai/bedrock/foundation-model/list" WITH DESTINATION_TOPIC "ai/bedrock/foundation-model/list/response" WITH METHOD "GET" WITH ENDPOINT "/foundation-models?byOutputModality={value.json.output_modality}&byProvider={value.json.provider}" WITH HEADERS ADD "Accept" "application/json" ADD "Authorization" "Bearer {value.json.api_key}" ADD EVENT GetFoundationModel WITH SOURCE_TOPIC "ai/bedrock/foundation-model/get" WITH DESTINATION_TOPIC "ai/bedrock/foundation-model/get/response" WITH METHOD "GET" WITH ENDPOINT "/foundation-models/{value.json.model_id}" WITH HEADERS ADD "Accept" "application/json" ADD "Authorization" "Bearer {value.json.api_key}" ADD EVENT ListInferenceProfiles WITH SOURCE_TOPIC "ai/bedrock/inference-profile/list" WITH DESTINATION_TOPIC "ai/bedrock/inference-profile/list/response" WITH METHOD "GET" WITH ENDPOINT "/inference-profiles?maxResults={value.json.max_results}&type={value.json.type}" WITH HEADERS ADD "Accept" "application/json" ADD "Authorization" "Bearer {value.json.api_key}" ``` Deploy it with `-addConnector` (do not include `ADD METADATA`). ## Usage example Call `ListFoundationModels` by publishing a JSON payload to its request topic, and read the reply on the response topic: ```bash theme={"theme":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} mosquitto_sub -t 'ai/bedrock/foundation-model/list/response' & mosquitto_pub -t 'ai/bedrock/foundation-model/list' -m '{"api_key": "", "output_modality": "", "provider": ""}' ``` ## Related * [REST API Connectors](/latest/lot-language/routes/rest-api-routes) * [REST API Apps catalog](/latest/lot-language/routes/rest-api-apps) # Amazon Bedrock Runtime Source: https://docs.coreflux.org/latest/lot-language/routes/rest-api-apps/amazon-bedrock-runtime Use the LoT Amazon Bedrock Runtime connector to call Converse and InvokeModel, count tokens and apply guardrails from MQTT with a Bedrock API key Technical Preview ## Overview Client-mode REST API connectors for Amazon Bedrock: triage alarms and summarize shift logs with the Converse API, create Titan embeddings with InvokeModel, count tokens, screen text with a guardrail, and list foundation models and inference profiles, all with a Bedrock API key. This is one of the templates in the Amazon Bedrock REST API app (`amazon-bedrock`). Deploy it alongside the other connectors of that app when they depend on each other (for example a token connector plus an API connector). ## Auth Amazon Bedrock API key as Authorization: Bearer \{value.json.api\_key} (long-term ABSK..., short-term bedrock-api-key-...). Hub secret suggested AWS\_BEARER\_TOKEN\_BEDROCK + GET SECRET. Alternative: AWS SigV4 (service bedrock) computed outside the broker, documented in comments. Never commit keys. Auth headers used by the template: `Authorization: Bearer {value.json.api_key}`. ## Template parameters | Placeholder | Type | Description | Required | | - | - | - | - | | `RouteName` | string | Name of the Amazon Bedrock Runtime connector | no | | `BaseUrl` | string | Amazon Bedrock Runtime base URL without trailing slash (e.g. [https://bedrock-runtime.eu-west-1.amazonaws.com](https://bedrock-runtime.eu-west-1.amazonaws.com)) | yes | `BASE_ADDRESS` default: `https://bedrock-runtime.eu-west-1.amazonaws.com`. ## Events | Event | Method | Endpoint | Request topic | Response topic | Payload fields | | - | - | - | - | - | - | | `Converse` | POST | `/model/{value.json.model_id}/converse` | `ai/bedrock/converse/send` | `ai/bedrock/converse/send/response` | `api_key`, `body`, `model_id` | | `InvokeModel` | POST | `/model/{value.json.model_id}/invoke` | `ai/bedrock/model/invoke` | `ai/bedrock/model/invoke/response` | `api_key`, `body`, `model_id` | | `CountTokens` | POST | `/model/{value.json.model_id}/count-tokens` | `ai/bedrock/tokens/count` | `ai/bedrock/tokens/count/response` | `api_key`, `body`, `model_id` | | `ApplyGuardrail` | POST | `/guardrail/{value.json.guardrail_id}/version/{value.json.guardrail_version}/apply` | `ai/bedrock/guardrail/apply` | `ai/bedrock/guardrail/apply/response` | `api_key`, `body`, `guardrail_id`, `guardrail_version` | ## Deploy Get the template card (JSON with `template` and `metadata`) from the broker, then deploy the connector: ```bash theme={"theme":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} mosquitto_pub -t '$SYS/Coreflux/Command' -m "-connectorCode AMAZON_BEDROCK_RUNTIME" ``` The deployable connector from the REST API app, with `BASE_ADDRESS` already filled in: ```lot theme={"theme":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} DEFINE CONNECTOR AmazonBedrockRuntime WITH TYPE REST_API ADD REST_API_CONFIG WITH BASE_ADDRESS "https://bedrock-runtime.eu-west-1.amazonaws.com" WITH ENABLE_CLIENT "true" WITH USE_SSL "true" WITH REQUEST_TIMEOUT '120' WITH DEFAULT_CONTENT_TYPE "application/json" ADD EVENT Converse WITH SOURCE_TOPIC "ai/bedrock/converse/send" WITH DESTINATION_TOPIC "ai/bedrock/converse/send/response" WITH METHOD "POST" WITH ENDPOINT "/model/{value.json.model_id}/converse" WITH HEADERS ADD "Accept" "application/json" ADD "Content-Type" "application/json" ADD "Authorization" "Bearer {value.json.api_key}" WITH BODY "{value.json.body}" ADD EVENT InvokeModel WITH SOURCE_TOPIC "ai/bedrock/model/invoke" WITH DESTINATION_TOPIC "ai/bedrock/model/invoke/response" WITH METHOD "POST" WITH ENDPOINT "/model/{value.json.model_id}/invoke" WITH HEADERS ADD "Accept" "application/json" ADD "Content-Type" "application/json" ADD "Authorization" "Bearer {value.json.api_key}" WITH BODY "{value.json.body}" ADD EVENT CountTokens WITH SOURCE_TOPIC "ai/bedrock/tokens/count" WITH DESTINATION_TOPIC "ai/bedrock/tokens/count/response" WITH METHOD "POST" WITH ENDPOINT "/model/{value.json.model_id}/count-tokens" WITH HEADERS ADD "Accept" "application/json" ADD "Content-Type" "application/json" ADD "Authorization" "Bearer {value.json.api_key}" WITH BODY "{value.json.body}" ADD EVENT ApplyGuardrail WITH SOURCE_TOPIC "ai/bedrock/guardrail/apply" WITH DESTINATION_TOPIC "ai/bedrock/guardrail/apply/response" WITH METHOD "POST" WITH ENDPOINT "/guardrail/{value.json.guardrail_id}/version/{value.json.guardrail_version}/apply" WITH HEADERS ADD "Accept" "application/json" ADD "Content-Type" "application/json" ADD "Authorization" "Bearer {value.json.api_key}" WITH BODY "{value.json.body}" ``` Deploy it with `-addConnector` (do not include `ADD METADATA`). ## Usage example Call `Converse` by publishing a JSON payload to its request topic, and read the reply on the response topic: ```bash theme={"theme":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} mosquitto_sub -t 'ai/bedrock/converse/send/response' & mosquitto_pub -t 'ai/bedrock/converse/send' -m '{"api_key": "", "body": {}, "model_id": ""}' ``` ## Related * [REST API Connectors](/latest/lot-language/routes/rest-api-routes) * [REST API Apps catalog](/latest/lot-language/routes/rest-api-apps) # Amazon CloudWatch Source: https://docs.coreflux.org/latest/lot-language/routes/rest-api-apps/amazon-cloudwatch LoT connector for Amazon CloudWatch: send PutMetricData, GetMetricData, CreateLogStream and PutLogEvents from MQTT using upstream SigV4 headers Technical Preview ## Overview A client-mode connector for a **curated observability / ITSM** slice of [Amazon CloudWatch](https://docs.aws.amazon.com/AmazonCloudWatch/latest/monitoring/WhatIsCloudWatch.html) and [CloudWatch Logs](https://docs.aws.amazon.com/AmazonCloudWatch/latest/logs/WhatIsCloudWatchLogs.html): | Catalog connector event | Entity / path | Notes | | - | - | - | | POST PutMetricData | `POST /` on `monitoring.{region}.amazonaws.com` | JSON + `X-Amz-Target: GraniteServiceVersion20100801.PutMetricData`; SigV4 service `monitoring`; expect **200** | | GetMetricData | `POST /` (same host) | Catalog said GET; official JSON protocol is POST `GetMetricData`; preferred credential check → **200** | | POST PutLogEvents | `POST /` on `logs.{region}.amazonaws.com` | `Content-Type: application/x-amz-json-1.1`; `X-Amz-Target: Logs_20140328.PutLogEvents`; SigV4 service `logs` | | CreateLogStream | `POST /` (logs host) | Helper so PutLogEvents is usable | | SNS alarm notification | inbound HTTPS | **ENABLE\_SERVER companion** (docs only — not in this client `.lot`) | Default `BASE_ADDRESS` is `https://monitoring.eu-west-1.amazonaws.com`. Topics use `itsm/amazon-cloudwatch//`. Switch BASE to `https://logs.{region}.amazonaws.com` for Logs events. > [!NOTE] > > Prefer Hub secrets for `AWS_ACCESS_KEY_ID` / `AWS_SECRET_ACCESS_KEY` / `AWS_REGION` used by an upstream SigV4 signer. Pass `Authorization`, `x-amz-date`, and `x-amz-content-sha256` on each MQTT payload — this is **not** Bearer auth. ## Basic Syntax ```lot wrap theme={"theme":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} # Coreflux REST_API template — Amazon CloudWatch (metrics + logs) # Auth: SigV4 headers from MQTT payload (NOT Bearer) # BASE_ADDRESS: https://monitoring.eu-west-1.amazonaws.com DEFINE CONNECTOR AmazonCloudWatchRestApi WITH TYPE REST_API ADD REST_API_CONFIG WITH BASE_ADDRESS "https://monitoring.eu-west-1.amazonaws.com" WITH ENABLE_CLIENT "true" WITH USE_SSL "true" WITH REQUEST_TIMEOUT '60' WITH DEFAULT_CONTENT_TYPE "application/json" ADD EVENT Putmetricdata WITH SOURCE_TOPIC "itsm/amazon-cloudwatch/metric/put" WITH DESTINATION_TOPIC "itsm/amazon-cloudwatch/metric/put/response" WITH METHOD "POST" WITH ENDPOINT "/" WITH HEADERS ADD "Accept" "application/json" ADD "Content-Type" "application/json" ADD "Content-Encoding" "amz-1.0" ADD "X-Amz-Target" "GraniteServiceVersion20100801.PutMetricData" ADD "Authorization" "{value.json.authorization}" ADD "x-amz-date" "{value.json.x_amz_date}" ADD "x-amz-content-sha256" "{value.json.x_amz_content_sha256}" WITH BODY "{payload}" ADD EVENT Getmetricdata WITH SOURCE_TOPIC "itsm/amazon-cloudwatch/metric/get" WITH DESTINATION_TOPIC "itsm/amazon-cloudwatch/metric/get/response" WITH METHOD "POST" WITH ENDPOINT "/" WITH HEADERS ADD "Accept" "application/json" ADD "Content-Type" "application/json" ADD "Content-Encoding" "amz-1.0" ADD "X-Amz-Target" "GraniteServiceVersion20100801.GetMetricData" ADD "Authorization" "{value.json.authorization}" ADD "x-amz-date" "{value.json.x_amz_date}" ADD "x-amz-content-sha256" "{value.json.x_amz_content_sha256}" WITH BODY "{payload}" ADD EVENT Putlogevents WITH SOURCE_TOPIC "itsm/amazon-cloudwatch/log/put" WITH DESTINATION_TOPIC "itsm/amazon-cloudwatch/log/put/response" WITH METHOD "POST" WITH ENDPOINT "/" WITH HEADERS ADD "Accept" "application/json" ADD "Content-Type" "application/x-amz-json-1.1" ADD "X-Amz-Target" "Logs_20140328.PutLogEvents" ADD "Authorization" "{value.json.authorization}" ADD "x-amz-date" "{value.json.x_amz_date}" ADD "x-amz-content-sha256" "{value.json.x_amz_content_sha256}" WITH BODY "{payload}" ADD EVENT Createlogstream WITH SOURCE_TOPIC "itsm/amazon-cloudwatch/log/create-stream" WITH DESTINATION_TOPIC "itsm/amazon-cloudwatch/log/create-stream/response" WITH METHOD "POST" WITH ENDPOINT "/" WITH HEADERS ADD "Accept" "application/json" ADD "Content-Type" "application/x-amz-json-1.1" ADD "X-Amz-Target" "Logs_20140328.CreateLogStream" ADD "Authorization" "{value.json.authorization}" ADD "x-amz-date" "{value.json.x_amz_date}" ADD "x-amz-content-sha256" "{value.json.x_amz_content_sha256}" WITH BODY "{payload}" ``` ## Authentication | Mode | Header | Notes | | - | - | - | | AWS Signature Version 4 | `Authorization: {value.json.authorization}` (+ `x-amz-date`, `x-amz-content-sha256`) | Default in this template — **not** Bearer | Sign with service name `monitoring` (metrics host) or `logs` (Logs host). Store `AWS_ACCESS_KEY_ID` / `AWS_SECRET_ACCESS_KEY` / `AWS_REGION` (optional `AWS_SESSION_TOKEN`) in Hub secrets and resolve via an upstream signer + `GET SECRET` — never commit credentials. ## MQTT examples **PutMetricData** (BASE = monitoring): ```json theme={"theme":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} { "Namespace": "Coreflux/Plant", "MetricData": [ { "MetricName": "vibration_rms", "Value": 12.4, "Unit": "None", "Dimensions": [{ "Name": "line", "Value": "a" }] } ] } ``` Publish with SigV4 headers. Expect **HTTP 200**. **GetMetricData** (credential check; BASE = monitoring): ```json theme={"theme":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} { "StartTime": "2026-10-04T16:00:00Z", "EndTime": "2026-10-04T17:00:00Z", "MetricDataQueries": [ { "Id": "m1", "MetricStat": { "Metric": { "Namespace": "AWS/EC2", "MetricName": "CPUUtilization", "Dimensions": [{ "Name": "InstanceId", "Value": "i-0123456789abcdef0" }] }, "Period": 300, "Stat": "Average" } } ] } ``` Expect **HTTP 200**. **PutLogEvents** (BASE = `logs.{region}.amazonaws.com`): ```json theme={"theme":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} { "logGroupName": "coreflux/plant", "logStreamName": "line-a", "logEvents": [ { "timestamp": 1728057600000, "message": "vibration_rms=12.4" } ] } ``` Expect **HTTP 200** with `nextSequenceToken`. Create the stream first via `Createlogstream` if needed. ## ENABLE\_SERVER companion CloudWatch Alarms → SNS → HTTPS subscription can POST notification JSON to an inbound endpoint. Package that as a separate `ENABLE_SERVER` connector — not included in the client `.lot`. ## Sources * [https://docs.aws.amazon.com/AmazonCloudWatch/latest/APIReference/API\_PutMetricData.html](https://docs.aws.amazon.com/AmazonCloudWatch/latest/APIReference/API_PutMetricData.html) * [https://docs.aws.amazon.com/AmazonCloudWatch/latest/APIReference/API\_GetMetricData.html](https://docs.aws.amazon.com/AmazonCloudWatch/latest/APIReference/API_GetMetricData.html) * [https://docs.aws.amazon.com/AmazonCloudWatch/latest/APIReference/making-api-requests.html](https://docs.aws.amazon.com/AmazonCloudWatch/latest/APIReference/making-api-requests.html) * [https://docs.aws.amazon.com/AmazonCloudWatchLogs/latest/APIReference/API\_PutLogEvents.html](https://docs.aws.amazon.com/AmazonCloudWatchLogs/latest/APIReference/API_PutLogEvents.html) * [https://docs.aws.amazon.com/AmazonCloudWatchLogs/latest/APIReference/API\_CreateLogStream.html](https://docs.aws.amazon.com/AmazonCloudWatchLogs/latest/APIReference/API_CreateLogStream.html) * [https://docs.aws.amazon.com/sns/latest/dg/sns-http-https-endpoint-as-subscriber.html](https://docs.aws.amazon.com/sns/latest/dg/sns-http-https-endpoint-as-subscriber.html) ## Related * [REST API connectors overview](/latest/lot-language/routes/rest-api-routes) * [Azure Monitor](/latest/lot-language/routes/rest-api-apps/azure-monitor) * [Datadog](/latest/lot-language/routes/rest-api-apps/datadog) * [AWS IoT Core](/latest/lot-language/routes/rest-api-apps/aws-iot-core) # Amazon DynamoDB Source: https://docs.coreflux.org/latest/lot-language/routes/rest-api-apps/amazon-dynamodb LoT connector for Amazon DynamoDB: put, update, get and query device state items, batch writes and list tables from MQTT with SigV4-signed JSON Technical Preview ## Overview A client-mode connector for the [Amazon DynamoDB API](https://docs.aws.amazon.com/amazondynamodb/latest/APIReference/) that keeps per-device state and history in a DynamoDB table straight from MQTT. DynamoDB uses the AWS JSON 1.0 protocol: every operation is `POST /` with `Content-Type: application/x-amz-json-1.0` and an `X-Amz-Target` header naming the operation. | Connector event | X-Amz-Target | Notes | | - | - | - | | `PutItem` | `DynamoDB_20120810.PutItem` | Write or replace a device state item. [API\_PutItem](https://docs.aws.amazon.com/amazondynamodb/latest/APIReference/API_PutItem.html) | | `UpdateItem` | `DynamoDB_20120810.UpdateItem` | Set `lastSeen` or other attributes with an `UpdateExpression`. [API\_UpdateItem](https://docs.aws.amazon.com/amazondynamodb/latest/APIReference/API_UpdateItem.html) | | `QueryByDevice` | `DynamoDB_20120810.Query` | Items for one partition key (for example `deviceId`), newest first. [API\_Query](https://docs.aws.amazon.com/amazondynamodb/latest/APIReference/API_Query.html) | | `GetItem` | `DynamoDB_20120810.GetItem` | Read one item by primary key. | | `BatchWriteItem` | `DynamoDB_20120810.BatchWriteItem` | Up to 25 puts or deletes per call. | | `ListTables` | `DynamoDB_20120810.ListTables` | Credential check. | | `DescribeTable` / `CreateTable` | `…DescribeTable`, `…CreateTable` | Table status, key schema, and one-time setup. | Default `BASE_ADDRESS` is `https://dynamodb.eu-west-1.amazonaws.com`; change the region to match your table. Topics use `data/amazon-dynamodb//`, and replies arrive on `/response`. ## Auth DynamoDB requires [AWS Signature Version 4](https://docs.aws.amazon.com/IAM/latest/UserGuide/reference_sigv4.html) with service name `dynamodb`. It is not a Bearer token. The connector does not sign requests itself; an upstream signer (a Python action, edge service, or SDK) signs each request and publishes the signed headers with the body: * `authorization`: the `AWS4-HMAC-SHA256 Credential=…, SignedHeaders=…, Signature=…` value * `x_amz_date`: the `X-Amz-Date` timestamp used in the signature * `x_amz_content_sha256`: hex SHA-256 of the body * `body`: the DynamoDB request JSON. Only this field is sent to AWS. Keep `AWS_ACCESS_KEY_ID`, `AWS_SECRET_ACCESS_KEY` and `AWS_REGION` in Hub secrets for the signer, and never commit keys. For temporary STS credentials, add an `x-amz-security-token` header to the events. > [!WARNING] > > **The signature covers the exact body bytes.** Sign compact JSON (no spaces) and make sure the connector sends the same bytes. If they differ, AWS returns `400 InvalidSignatureException`. This has not been verified on the broker runtime yet. The IAM policy needs `dynamodb:PutItem`, `UpdateItem`, `Query`, `GetItem`, `BatchWriteItem`, `ListTables` and `DescribeTable` on the table, plus `CreateTable` if the connector creates it. ## Basic Syntax ```lot wrap theme={"theme":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} DEFINE CONNECTOR AmazonDynamodbRestApi WITH TYPE REST_API ADD REST_API_CONFIG WITH BASE_ADDRESS "https://dynamodb.eu-west-1.amazonaws.com" WITH ENABLE_CLIENT "true" WITH USE_SSL "true" WITH REQUEST_TIMEOUT '60' WITH DEFAULT_CONTENT_TYPE "application/x-amz-json-1.0" # Errors come back as HTTP 400 JSON {"__type":"...#ValidationException","message":"..."} ADD EVENT PutItem WITH SOURCE_TOPIC "data/amazon-dynamodb/item/put" WITH DESTINATION_TOPIC "data/amazon-dynamodb/item/put/response" WITH METHOD "POST" WITH ENDPOINT "/" WITH HEADERS ADD "Accept" "application/json" ADD "Content-Type" "application/x-amz-json-1.0" ADD "X-Amz-Target" "DynamoDB_20120810.PutItem" ADD "Authorization" "{value.json.authorization}" ADD "x-amz-date" "{value.json.x_amz_date}" ADD "x-amz-content-sha256" "{value.json.x_amz_content_sha256}" WITH BODY "{value.json.body}" ADD EVENT UpdateItem WITH SOURCE_TOPIC "data/amazon-dynamodb/item/update" WITH DESTINATION_TOPIC "data/amazon-dynamodb/item/update/response" WITH METHOD "POST" WITH ENDPOINT "/" WITH HEADERS ADD "Accept" "application/json" ADD "Content-Type" "application/x-amz-json-1.0" ADD "X-Amz-Target" "DynamoDB_20120810.UpdateItem" ADD "Authorization" "{value.json.authorization}" ADD "x-amz-date" "{value.json.x_amz_date}" ADD "x-amz-content-sha256" "{value.json.x_amz_content_sha256}" WITH BODY "{value.json.body}" ADD EVENT QueryByDevice WITH SOURCE_TOPIC "data/amazon-dynamodb/items/query" WITH DESTINATION_TOPIC "data/amazon-dynamodb/items/query/response" WITH METHOD "POST" WITH ENDPOINT "/" WITH HEADERS ADD "Accept" "application/json" ADD "Content-Type" "application/x-amz-json-1.0" ADD "X-Amz-Target" "DynamoDB_20120810.Query" ADD "Authorization" "{value.json.authorization}" ADD "x-amz-date" "{value.json.x_amz_date}" ADD "x-amz-content-sha256" "{value.json.x_amz_content_sha256}" WITH BODY "{value.json.body}" ``` The full template also includes `GetItem`, `BatchWriteItem`, `ListTables`, `DescribeTable` and `CreateTable`, which use the same headers with their own `X-Amz-Target`. ## Payload examples DynamoDB values use attribute-value JSON: strings are `{"S": "…"}`, and numbers are `{"N": "…"}` written as strings. **Write device state (topic `data/amazon-dynamodb/item/put`):** ```json theme={"theme":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} { "authorization": "AWS4-HMAC-SHA256 Credential=AKIA…/20261004/eu-west-1/dynamodb/aws4_request, SignedHeaders=…, Signature=…", "x_amz_date": "20261004T220500Z", "x_amz_content_sha256": "", "body": { "TableName": "cf_device_state", "Item": { "deviceId": {"S": "press-01"}, "ts": {"N": "1759615200"}, "temp": {"N": "21.5"}, "state": {"S": "RUN"} } } } ``` **Update last-seen (topic `data/amazon-dynamodb/item/update`), body only:** ```json theme={"theme":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} { "TableName": "cf_device_state", "Key": {"deviceId": {"S": "press-01"}, "ts": {"N": "1759615200"}}, "UpdateExpression": "SET lastSeen = :t, #s = :s", "ExpressionAttributeNames": {"#s": "state"}, "ExpressionAttributeValues": {":t": {"S": "2026-10-04T22:05:00Z"}, ":s": {"S": "IDLE"}}, "ReturnValues": "ALL_NEW" } ``` **Query a device, newest first (topic `data/amazon-dynamodb/items/query`), body only:** ```json theme={"theme":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} { "TableName": "cf_device_state", "KeyConditionExpression": "deviceId = :d", "ExpressionAttributeValues": {":d": {"S": "press-01"}}, "ScanIndexForward": false, "Limit": 10 } ``` > [!TIP] > > `state`, `name`, `status` and many other common words are DynamoDB reserved words. Alias them with `ExpressionAttributeNames` (`#s`) in update and query expressions. ## Errors and limits * Errors are `HTTP 400` JSON with `__type` and `message`, for example `ValidationException`, `ResourceNotFoundException`, `ConditionalCheckFailedException`, or `ProvisionedThroughputExceededException` (back off and retry). * Items are limited to 400 KB. `BatchWriteItem` accepts up to 25 requests (16 MB) and may return `UnprocessedItems` with `HTTP 200`; resend those. * `Query` returns at most 1 MB per call; page with `LastEvaluatedKey` → `ExclusiveStartKey`. * Scan, deletes, PartiQL, transactions and DynamoDB Streams (a separate `streams.dynamodb` endpoint) are not included. ## Related * [REST API Connectors](/latest/lot-language/routes/rest-api-routes) * [REST API Apps](/latest/lot-language/routes/rest-api-apps/index) * [Amazon S3](/latest/lot-language/routes/rest-api-apps/amazon-s3) * [Amazon CloudWatch](/latest/lot-language/routes/rest-api-apps/amazon-cloudwatch) # Amazon S3 Source: https://docs.coreflux.org/latest/lot-language/routes/rest-api-apps/amazon-s3 Client-mode REST API connector for a curated Amazon S3 slice: PutObject, GetObject, HeadObject, ListBuckets, and PUT to a presigned URL Technical Preview ## Overview A client-mode connector for a **curated IIoT object-storage** slice of the [Amazon S3 REST API](https://docs.aws.amazon.com/AmazonS3/latest/API/Welcome.html) under path-style `https://s3.amazonaws.com`: | Catalog connector event | Entity / path | Notes | | - | - | - | | | `GET /` | ListBuckets for the signing identity | | PUT object (snapshot, CSV rollup) | `PUT /{bucket}/{key}` | SigV4 headers required | | GET object | `GET /{bucket}/{key}` | SigV4 headers required | | (optional) HeadObject | `HEAD /{bucket}/{key}` | Metadata without body | | PUT to presigned URL | `PUT {presigned_path}` | No Authorization; switch BASE to URL origin | | S3 → SNS event | — | Docs-only; future `ENABLE_SERVER` | `BASE_ADDRESS` defaults to `https://s3.amazonaws.com` (no trailing slash; path-style). Prefer regional `https://s3.{region}.amazonaws.com` when required. Topics use `storage/amazon-s3//`. > [!NOTE] > > **Virtual-hosted style:** set `BASE_ADDRESS` to `https://{bucket}.s3.{region}.amazonaws.com` and use endpoints like `/{key}` (bucket is already in the host). > [!NOTE] > > **Presigned PUT:** set `BASE_ADDRESS` to the presigned URL origin only, put path+query in `presigned_path`, and omit SigV4 headers — the query string carries the signature. Ideal for edge devices that must not hold AWS keys. > [!NOTE] > > **S3 event notifications** (SNS / EventBridge / Lambda) are not inbound HTTP on this client connector. Receiving notification POSTs needs a separate `ENABLE_SERVER` REST\_API template (not packaged). > [!NOTE] > > Prefer Hub secrets for AWS keys used by an upstream SigV4 signer. ## Basic Syntax ```lot wrap theme={"theme":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} # Coreflux REST_API template — Amazon S3 curated slice # Auth: SigV4 via {value.json.authorization}, {value.json.x_amz_date}, # {value.json.x_amz_content_sha256} — or PutPresigned (no Authorization) # BASE_ADDRESS: https://s3.amazonaws.com (path-style) DEFINE CONNECTOR AmazonS3RestApi WITH TYPE REST_API ADD REST_API_CONFIG WITH BASE_ADDRESS "https://s3.amazonaws.com" WITH ENABLE_CLIENT "true" WITH USE_SSL "true" WITH REQUEST_TIMEOUT '60' WITH DEFAULT_CONTENT_TYPE "application/octet-stream" ADD EVENT Listbuckets WITH SOURCE_TOPIC "storage/amazon-s3/buckets/list" WITH DESTINATION_TOPIC "storage/amazon-s3/buckets/list/response" WITH METHOD "GET" WITH ENDPOINT "/" WITH HEADERS ADD "Accept" "application/xml" ADD "Authorization" "{value.json.authorization}" ADD "x-amz-date" "{value.json.x_amz_date}" ADD "x-amz-content-sha256" "{value.json.x_amz_content_sha256}" ADD EVENT Putobject WITH SOURCE_TOPIC "storage/amazon-s3/object/put" WITH DESTINATION_TOPIC "storage/amazon-s3/object/put/response" WITH METHOD "PUT" WITH ENDPOINT "/{value.json.bucket}/{value.json.key}" WITH HEADERS ADD "Accept" "application/xml" ADD "Content-Type" "{value.json.content_type}" ADD "Authorization" "{value.json.authorization}" ADD "x-amz-date" "{value.json.x_amz_date}" ADD "x-amz-content-sha256" "{value.json.x_amz_content_sha256}" WITH BODY "{payload}" ADD EVENT Getobject WITH SOURCE_TOPIC "storage/amazon-s3/object/get" WITH DESTINATION_TOPIC "storage/amazon-s3/object/get/response" WITH METHOD "GET" WITH ENDPOINT "/{value.json.bucket}/{value.json.key}" WITH HEADERS ADD "Accept" "*/*" ADD "Authorization" "{value.json.authorization}" ADD "x-amz-date" "{value.json.x_amz_date}" ADD "x-amz-content-sha256" "{value.json.x_amz_content_sha256}" ADD EVENT Headobject WITH SOURCE_TOPIC "storage/amazon-s3/object/head" WITH DESTINATION_TOPIC "storage/amazon-s3/object/head/response" WITH METHOD "HEAD" WITH ENDPOINT "/{value.json.bucket}/{value.json.key}" WITH HEADERS ADD "Authorization" "{value.json.authorization}" ADD "x-amz-date" "{value.json.x_amz_date}" ADD "x-amz-content-sha256" "{value.json.x_amz_content_sha256}" ADD EVENT Putpresigned WITH SOURCE_TOPIC "storage/amazon-s3/object/put-presigned" WITH DESTINATION_TOPIC "storage/amazon-s3/object/put-presigned/response" WITH METHOD "PUT" WITH ENDPOINT "{value.json.presigned_path}" WITH HEADERS ADD "Content-Type" "{value.json.content_type}" WITH BODY "{payload}" ``` ## Configuration | Setting | Value | | - | - | | Connector name | `AmazonS3RestApi` | | Type | `REST_API` (client) | | BASE\_ADDRESS | `https://s3.amazonaws.com` (path-style default) | | Topic prefix | `storage/amazon-s3/...` | | Default Content-Type | `application/octet-stream` | | Hub status | Technical preview (`WITH STATUS "beta"`) | ## Authentication ### AWS Signature Version 4 S3 does **not** use `Authorization: Bearer …`. Sign each request with [SigV4](https://docs.aws.amazon.com/AmazonS3/latest/API/sig-v4-authenticating-requests.html) and pass: | MQTT JSON field | HTTP header | | - | - | | `authorization` | `Authorization` (`AWS4-HMAC-SHA256 Credential=…`) | | `x_amz_date` | `x-amz-date` | | `x_amz_content_sha256` | `x-amz-content-sha256` | Use Hub secrets for `AWS_ACCESS_KEY_ID` / `AWS_SECRET_ACCESS_KEY` in an upstream signing helper. Never commit real keys. STS sessions may also need `x-amz-security-token` (add that header in a customized fork if required). ### Presigned URLs (recommended for edge) Generate a time-limited PUT or GET URL with the AWS CLI/SDK. For PutPresigned: 1. Point `BASE_ADDRESS` at the URL origin. 2. Put path + query into `presigned_path`. 3. Omit Authorization — do not re-sign on the broker. ## MQTT topics | Event | Request topic | Response topic | | - | - | - | | ListBuckets | `storage/amazon-s3/buckets/list` | `…/response` | | PutObject | `storage/amazon-s3/object/put` | `…/response` | | GetObject | `storage/amazon-s3/object/get` | `…/response` | | HeadObject | `storage/amazon-s3/object/head` | `…/response` | | PutPresigned | `storage/amazon-s3/object/put-presigned` | `…/response` | ## Example payloads **ListBuckets (after signing):** ```json theme={"theme":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} { "authorization": "AWS4-HMAC-SHA256 Credential=AKIA.../20261004/eu-west-1/s3/aws4_request, SignedHeaders=host;x-amz-content-sha256;x-amz-date, Signature=...", "x_amz_date": "20261004T040000Z", "x_amz_content_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" } ``` **PutObject CSV rollup:** ```json theme={"theme":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} { "bucket": "my-iiot-rollups", "key": "line-a/2026-10-04/rollup.csv", "content_type": "text/csv", "authorization": "AWS4-HMAC-SHA256 Credential=...", "x_amz_date": "20261004T040100Z", "x_amz_content_sha256": "", "payload": "ts,device,temp\n2026-10-04T04:01:00Z,sensor-01,72.5\n" } ``` **PutPresigned:** ```json theme={"theme":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} { "presigned_path": "/line-a/snapshot.json?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=...&X-Amz-Signature=...", "content_type": "application/json", "payload": "{\"device\":\"sensor-01\",\"temp\":72.5}" } ``` ## References * [S3 API PutObject](https://docs.aws.amazon.com/AmazonS3/latest/API/API_PutObject.html) * [S3 API GetObject](https://docs.aws.amazon.com/AmazonS3/latest/API/API_GetObject.html) * [S3 API HeadObject](https://docs.aws.amazon.com/AmazonS3/latest/API/API_HeadObject.html) * [S3 API ListBuckets](https://docs.aws.amazon.com/AmazonS3/latest/API/API_ListBuckets.html) * [Upload with a presigned URL](https://docs.aws.amazon.com/AmazonS3/latest/userguide/PresignedUrlUploadObject.html) * [Authenticating Requests (SigV4)](https://docs.aws.amazon.com/AmazonS3/latest/API/sig-v4-authenticating-requests.html) * [Coreflux REST API Connectors](/latest/lot-language/routes/rest-api-routes) ## Package status # Amazon SNS Source: https://docs.coreflux.org/latest/lot-language/routes/rest-api-apps/amazon-sns LoT connector for Amazon SNS: publish to topics or SMS, list and create topics, read attributes and subscribe endpoints from MQTT with SigV4 auth Technical Preview ## Overview A client-mode connector for a **curated messaging / notifications** slice of [Amazon SNS](https://docs.aws.amazon.com/sns/latest/dg/welcome.html): | Catalog connector event | Entity / path | Notes | | - | - | - | | POST Publish (topic) | `POST /` on `sns.{region}.amazonaws.com` | Query `Action=Publish&Version=2010-03-31` + `TopicArn` + `Message`; SigV4 service `sns`; expect **200** XML `MessageId` | | POST Publish SMS | `POST /` (same host) | Same Publish action with `PhoneNumber` (E.164) instead of `TopicArn` | | ListTopics | `POST /` | Preferred credential check → **200** XML topic list | | GetTopicAttributes | `POST /` | Topic property map | | CreateTopic | `POST /` | Create / idempotent get by name | | Subscribe | `POST /` | e.g. `Protocol=https` + `Endpoint`; pending until confirmed | | Confirm / receive HTTPS subscription | inbound HTTPS | **ENABLE\_SERVER companion** (docs only — not in this client `.lot`); client `ConfirmSubscription` Query action optional in a fork | Default `BASE_ADDRESS` is `https://sns.eu-west-1.amazonaws.com`. Topics use `messaging/amazon-sns//`. Official protocol is **Query** form-urlencoded (XML responses), not CloudWatch-style JSON `X-Amz-Target`. > [!NOTE] > > Prefer Hub secrets for `AWS_ACCESS_KEY_ID` / `AWS_SECRET_ACCESS_KEY` / `AWS_REGION` used by an upstream SigV4 signer. Pass `Authorization`, `x-amz-date`, and `x-amz-content-sha256` on each MQTT payload — this is **not** Bearer auth. ## Basic Syntax ```lot wrap theme={"theme":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} # Coreflux REST_API template — Amazon SNS (Query API 2010-03-31) # Auth: SigV4 headers from MQTT payload (NOT Bearer) # BASE_ADDRESS: https://sns.eu-west-1.amazonaws.com DEFINE CONNECTOR AmazonSnsRestApi WITH TYPE REST_API ADD REST_API_CONFIG WITH BASE_ADDRESS "https://sns.eu-west-1.amazonaws.com" WITH ENABLE_CLIENT "true" WITH USE_SSL "true" WITH REQUEST_TIMEOUT '60' WITH DEFAULT_CONTENT_TYPE "application/x-www-form-urlencoded" ADD EVENT Publish WITH SOURCE_TOPIC "messaging/amazon-sns/topic/publish" WITH DESTINATION_TOPIC "messaging/amazon-sns/topic/publish/response" WITH METHOD "POST" WITH ENDPOINT "/" WITH HEADERS ADD "Accept" "application/xml" ADD "Content-Type" "application/x-www-form-urlencoded" ADD "Authorization" "{value.json.authorization}" ADD "x-amz-date" "{value.json.x_amz_date}" ADD "x-amz-content-sha256" "{value.json.x_amz_content_sha256}" WITH BODY "{payload}" ADD EVENT Publishsms WITH SOURCE_TOPIC "messaging/amazon-sns/sms/publish" WITH DESTINATION_TOPIC "messaging/amazon-sns/sms/publish/response" WITH METHOD "POST" WITH ENDPOINT "/" WITH HEADERS ADD "Accept" "application/xml" ADD "Content-Type" "application/x-www-form-urlencoded" ADD "Authorization" "{value.json.authorization}" ADD "x-amz-date" "{value.json.x_amz_date}" ADD "x-amz-content-sha256" "{value.json.x_amz_content_sha256}" WITH BODY "{payload}" ADD EVENT Listtopics WITH SOURCE_TOPIC "messaging/amazon-sns/topics/list" WITH DESTINATION_TOPIC "messaging/amazon-sns/topics/list/response" WITH METHOD "POST" WITH ENDPOINT "/" WITH HEADERS ADD "Accept" "application/xml" ADD "Content-Type" "application/x-www-form-urlencoded" ADD "Authorization" "{value.json.authorization}" ADD "x-amz-date" "{value.json.x_amz_date}" ADD "x-amz-content-sha256" "{value.json.x_amz_content_sha256}" WITH BODY "{payload}" ADD EVENT Gettopicattributes WITH SOURCE_TOPIC "messaging/amazon-sns/topic/get-attributes" WITH DESTINATION_TOPIC "messaging/amazon-sns/topic/get-attributes/response" WITH METHOD "POST" WITH ENDPOINT "/" WITH HEADERS ADD "Accept" "application/xml" ADD "Content-Type" "application/x-www-form-urlencoded" ADD "Authorization" "{value.json.authorization}" ADD "x-amz-date" "{value.json.x_amz_date}" ADD "x-amz-content-sha256" "{value.json.x_amz_content_sha256}" WITH BODY "{payload}" ADD EVENT Createtopic WITH SOURCE_TOPIC "messaging/amazon-sns/topic/create" WITH DESTINATION_TOPIC "messaging/amazon-sns/topic/create/response" WITH METHOD "POST" WITH ENDPOINT "/" WITH HEADERS ADD "Accept" "application/xml" ADD "Content-Type" "application/x-www-form-urlencoded" ADD "Authorization" "{value.json.authorization}" ADD "x-amz-date" "{value.json.x_amz_date}" ADD "x-amz-content-sha256" "{value.json.x_amz_content_sha256}" WITH BODY "{payload}" ADD EVENT Subscribe WITH SOURCE_TOPIC "messaging/amazon-sns/topic/subscribe" WITH DESTINATION_TOPIC "messaging/amazon-sns/topic/subscribe/response" WITH METHOD "POST" WITH ENDPOINT "/" WITH HEADERS ADD "Accept" "application/xml" ADD "Content-Type" "application/x-www-form-urlencoded" ADD "Authorization" "{value.json.authorization}" ADD "x-amz-date" "{value.json.x_amz_date}" ADD "x-amz-content-sha256" "{value.json.x_amz_content_sha256}" WITH BODY "{payload}" ``` ## Authentication | Mode | Header | Notes | | - | - | - | | AWS Signature Version 4 | `Authorization: {value.json.authorization}` (+ `x-amz-date`, `x-amz-content-sha256`) | Default in this template — **not** Bearer | Sign with service name `sns` and a region matching the host. Store `AWS_ACCESS_KEY_ID` / `AWS_SECRET_ACCESS_KEY` / `AWS_REGION` (optional `AWS_SESSION_TOKEN`) in Hub secrets and resolve via an upstream signer + `GET SECRET` — never commit credentials. ## MQTT examples **ListTopics** (a good first call; form body): ``` Action=ListTopics&Version=2010-03-31 ``` Expect **HTTP 200**. **Publish** to topic: ``` Action=Publish&Version=2010-03-31&TopicArn=arn%3Aaws%3Asns%3Aeu-west-1%3A123456789012%3Acoreflux-plant&Message=vibration_rms%3D12.4&Subject=Plant%20alarm ``` Expect **HTTP 200** with `MessageId`. **Publish SMS:** ``` Action=Publish&Version=2010-03-31&PhoneNumber=%2B351912345678&Message=Plant%20alarm ``` **Subscribe** HTTPS: ``` Action=Subscribe&Version=2010-03-31&TopicArn=arn%3Aaws%3Asns%3Aeu-west-1%3A123456789012%3Acoreflux-plant&Protocol=https&Endpoint=https%3A%2F%2Fbroker.example.com%2Fsns%2Fplant&ReturnSubscriptionArn=true ``` Include SigV4 header fields (`authorization`, `x_amz_date`, `x_amz_content_sha256`) on the MQTT JSON envelope; put the form string in the HTTP body (`{payload}`). ## ENABLE\_SERVER companion When SNS delivers **SubscriptionConfirmation** / **Notification** / **UnsubscribeConfirmation** to your HTTPS endpoint, handle them with a separate `ENABLE_SERVER` connector (not packaged here). Confirm with the client [ConfirmSubscription](https://docs.aws.amazon.com/sns/latest/api/API_ConfirmSubscription.html) Query action (`Token` + `TopicArn`) or by GETting the `SubscribeURL` from the confirmation payload. ## Related * [REST API Connectors](/latest/lot-language/routes/rest-api-routes) * [Amazon CloudWatch](/latest/lot-language/routes/rest-api-apps/amazon-cloudwatch) (SigV4 pattern; SNS alarm companion) * AWS docs: [Publish](https://docs.aws.amazon.com/sns/latest/api/API_Publish.html), [ListTopics](https://docs.aws.amazon.com/sns/latest/api/API_ListTopics.html), [HTTPS subscribers](https://docs.aws.amazon.com/sns/latest/dg/sns-http-https-endpoint-as-subscriber.html) # Anaplan Source: https://docs.coreflux.org/latest/lot-language/routes/rest-api-apps/anaplan Client-mode REST API connectors for Anaplan Integration API v2: auth token plus workspaces and models and file upload and import and export and process tasks Technical Preview ## Overview Client-mode REST API connectors for Anaplan Integration API v2: auth token plus workspaces and models and file upload and import and export and process tasks. `BASE_ADDRESS` defaults to `https://api.anaplan.com`. Topics use `analytics/anaplan//`. | Event | Request | Source topic | | - | - | - | | `TokenAuthenticate` | `POST /token/authenticate` | `analytics/anaplan/auth/token/create` | | `TokenRefresh` | `POST /token/refresh` | `analytics/anaplan/auth/token/refresh` | | `TokenValidate` | `GET /token/validate` | `analytics/anaplan/auth/token/validate` | | `WorkspacesList` | `GET /2/0/workspaces` | `analytics/anaplan/workspaces/list` | | `ModelsList` | `GET /2/0/workspaces/{value.json.workspaceId}/models` | `analytics/anaplan/models/list` | | `FilesList` | `GET /2/0/workspaces/{value.json.workspaceId}/models/{value.json.modelId}/files` | `analytics/anaplan/files/list` | | `FileUpload` | `PUT /2/0/workspaces/{value.json.workspaceId}/models/{value.json.modelId}/files/{value.json.fileId}` | `analytics/anaplan/files/upload` | | `ImportsList` | `GET /2/0/workspaces/{value.json.workspaceId}/models/{value.json.modelId}/imports` | `analytics/anaplan/imports/list` | | `ImportTaskStart` | `POST /2/0/workspaces/{value.json.workspaceId}/models/{value.json.modelId}/imports/{value.json.importId}/tasks` | `analytics/anaplan/imports/tasks/start` | | `ImportTaskGet` | `GET /2/0/workspaces/{value.json.workspaceId}/models/{value.json.modelId}/imports/{value.json.importId}/tasks/{value.json.taskId}` | `analytics/anaplan/imports/tasks/get` | | `ExportTaskStart` | `POST /2/0/workspaces/{value.json.workspaceId}/models/{value.json.modelId}/exports/{value.json.exportId}/tasks` | `analytics/anaplan/exports/tasks/start` | | `ExportTaskGet` | `GET /2/0/workspaces/{value.json.workspaceId}/models/{value.json.modelId}/exports/{value.json.exportId}/tasks/{value.json.taskId}` | `analytics/anaplan/exports/tasks/get` | | `ProcessTaskStart` | `POST /2/0/workspaces/{value.json.workspaceId}/models/{value.json.modelId}/processes/{value.json.processId}/tasks` | `analytics/anaplan/processes/tasks/start` | | `ProcessTaskGet` | `GET /2/0/workspaces/{value.json.workspaceId}/models/{value.json.modelId}/processes/{value.json.processId}/tasks/{value.json.taskId}` | `analytics/anaplan/processes/tasks/get` | ## Authentication AnaplanAuthToken. AnaplanAuthRestApi ([https://auth.anaplan.com](https://auth.anaplan.com)) POST /token/authenticate with Authorization: \{value.json.authorization} ("Basic base64(user:password)" or the CACertificate header form) returns tokenInfo.tokenValue; refresh with /token/refresh before 30 minutes. AnaplanRestApi events send Authorization: AnaplanAuthToken \{value.json.auth\_token}. > [!WARNING] > > Keep credentials in broker secrets and never retain topics that carry tokens or client secrets. ## Notes * Two connectors because authentication lives on auth.anaplan.com and the Integration API on api.anaplan.com. * FileUpload sends \{value.json.body} as the whole file (single chunk, application/octet-stream). Larger files need the chunked PUT /chunks flow; not packaged. * Tokens expire after 30 minutes; call TokenRefresh before expiry. ## Basic Syntax ```lot wrap theme={"theme":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} DEFINE CONNECTOR AnaplanAuthRestApi WITH TYPE REST_API ADD REST_API_CONFIG WITH BASE_ADDRESS "https://auth.anaplan.com" WITH ENABLE_CLIENT "true" WITH USE_SSL "true" WITH REQUEST_TIMEOUT '60' WITH DEFAULT_CONTENT_TYPE "application/json" ADD EVENT TokenAuthenticate WITH SOURCE_TOPIC "analytics/anaplan/auth/token/create" WITH DESTINATION_TOPIC "analytics/anaplan/auth/token/create/response" WITH METHOD "POST" WITH ENDPOINT "/token/authenticate" WITH HEADERS ADD "Accept" "application/json" ADD "Authorization" "{value.json.authorization}" WITH BODY "" ADD EVENT TokenRefresh WITH SOURCE_TOPIC "analytics/anaplan/auth/token/refresh" WITH DESTINATION_TOPIC "analytics/anaplan/auth/token/refresh/response" WITH METHOD "POST" WITH ENDPOINT "/token/refresh" WITH HEADERS ADD "Accept" "application/json" ADD "Authorization" "AnaplanAuthToken {value.json.auth_token}" WITH BODY "" ADD EVENT TokenValidate WITH SOURCE_TOPIC "analytics/anaplan/auth/token/validate" WITH DESTINATION_TOPIC "analytics/anaplan/auth/token/validate/response" WITH METHOD "GET" WITH ENDPOINT "/token/validate" WITH HEADERS ADD "Accept" "application/json" ADD "Authorization" "AnaplanAuthToken {value.json.auth_token}" DEFINE CONNECTOR AnaplanRestApi WITH TYPE REST_API ADD REST_API_CONFIG WITH BASE_ADDRESS "https://api.anaplan.com" WITH ENABLE_CLIENT "true" WITH USE_SSL "true" WITH REQUEST_TIMEOUT '120' WITH DEFAULT_CONTENT_TYPE "application/json" ADD EVENT WorkspacesList WITH SOURCE_TOPIC "analytics/anaplan/workspaces/list" WITH DESTINATION_TOPIC "analytics/anaplan/workspaces/list/response" WITH METHOD "GET" WITH ENDPOINT "/2/0/workspaces" WITH HEADERS ADD "Accept" "application/json" ADD "Authorization" "AnaplanAuthToken {value.json.auth_token}" ADD EVENT ModelsList WITH SOURCE_TOPIC "analytics/anaplan/models/list" WITH DESTINATION_TOPIC "analytics/anaplan/models/list/response" WITH METHOD "GET" WITH ENDPOINT "/2/0/workspaces/{value.json.workspaceId}/models" WITH HEADERS ADD "Accept" "application/json" ADD "Authorization" "AnaplanAuthToken {value.json.auth_token}" ADD EVENT FilesList WITH SOURCE_TOPIC "analytics/anaplan/files/list" WITH DESTINATION_TOPIC "analytics/anaplan/files/list/response" WITH METHOD "GET" WITH ENDPOINT "/2/0/workspaces/{value.json.workspaceId}/models/{value.json.modelId}/files" WITH HEADERS ADD "Accept" "application/json" ADD "Authorization" "AnaplanAuthToken {value.json.auth_token}" ADD EVENT FileUpload WITH SOURCE_TOPIC "analytics/anaplan/files/upload" WITH DESTINATION_TOPIC "analytics/anaplan/files/upload/response" WITH METHOD "PUT" WITH ENDPOINT "/2/0/workspaces/{value.json.workspaceId}/models/{value.json.modelId}/files/{value.json.fileId}" WITH HEADERS ADD "Accept" "application/json" ADD "Content-Type" "application/octet-stream" ADD "Authorization" "AnaplanAuthToken {value.json.auth_token}" WITH BODY "{value.json.body}" ADD EVENT ImportsList WITH SOURCE_TOPIC "analytics/anaplan/imports/list" WITH DESTINATION_TOPIC "analytics/anaplan/imports/list/response" WITH METHOD "GET" WITH ENDPOINT "/2/0/workspaces/{value.json.workspaceId}/models/{value.json.modelId}/imports" WITH HEADERS ADD "Accept" "application/json" ADD "Authorization" "AnaplanAuthToken {value.json.auth_token}" ADD EVENT ImportTaskStart WITH SOURCE_TOPIC "analytics/anaplan/imports/tasks/start" WITH DESTINATION_TOPIC "analytics/anaplan/imports/tasks/start/response" WITH METHOD "POST" WITH ENDPOINT "/2/0/workspaces/{value.json.workspaceId}/models/{value.json.modelId}/imports/{value.json.importId}/tasks" WITH HEADERS ADD "Accept" "application/json" ADD "Content-Type" "application/json" ADD "Authorization" "AnaplanAuthToken {value.json.auth_token}" WITH BODY "{value.json.body}" ADD EVENT ImportTaskGet WITH SOURCE_TOPIC "analytics/anaplan/imports/tasks/get" WITH DESTINATION_TOPIC "analytics/anaplan/imports/tasks/get/response" WITH METHOD "GET" WITH ENDPOINT "/2/0/workspaces/{value.json.workspaceId}/models/{value.json.modelId}/imports/{value.json.importId}/tasks/{value.json.taskId}" WITH HEADERS ADD "Accept" "application/json" ADD "Authorization" "AnaplanAuthToken {value.json.auth_token}" ADD EVENT ExportTaskStart WITH SOURCE_TOPIC "analytics/anaplan/exports/tasks/start" WITH DESTINATION_TOPIC "analytics/anaplan/exports/tasks/start/response" WITH METHOD "POST" WITH ENDPOINT "/2/0/workspaces/{value.json.workspaceId}/models/{value.json.modelId}/exports/{value.json.exportId}/tasks" WITH HEADERS ADD "Accept" "application/json" ADD "Content-Type" "application/json" ADD "Authorization" "AnaplanAuthToken {value.json.auth_token}" WITH BODY "{value.json.body}" ADD EVENT ExportTaskGet WITH SOURCE_TOPIC "analytics/anaplan/exports/tasks/get" WITH DESTINATION_TOPIC "analytics/anaplan/exports/tasks/get/response" WITH METHOD "GET" WITH ENDPOINT "/2/0/workspaces/{value.json.workspaceId}/models/{value.json.modelId}/exports/{value.json.exportId}/tasks/{value.json.taskId}" WITH HEADERS ADD "Accept" "application/json" ADD "Authorization" "AnaplanAuthToken {value.json.auth_token}" ADD EVENT ProcessTaskStart WITH SOURCE_TOPIC "analytics/anaplan/processes/tasks/start" WITH DESTINATION_TOPIC "analytics/anaplan/processes/tasks/start/response" WITH METHOD "POST" WITH ENDPOINT "/2/0/workspaces/{value.json.workspaceId}/models/{value.json.modelId}/processes/{value.json.processId}/tasks" WITH HEADERS ADD "Accept" "application/json" ADD "Content-Type" "application/json" ADD "Authorization" "AnaplanAuthToken {value.json.auth_token}" WITH BODY "{value.json.body}" ADD EVENT ProcessTaskGet WITH SOURCE_TOPIC "analytics/anaplan/processes/tasks/get" WITH DESTINATION_TOPIC "analytics/anaplan/processes/tasks/get/response" WITH METHOD "GET" WITH ENDPOINT "/2/0/workspaces/{value.json.workspaceId}/models/{value.json.modelId}/processes/{value.json.processId}/tasks/{value.json.taskId}" WITH HEADERS ADD "Accept" "application/json" ADD "Authorization" "AnaplanAuthToken {value.json.auth_token}" ``` ## Sources * [https://help.anaplan.com/upload-and-import-a-csv-75094951-6424-446c-af03-cf9852af1179](https://help.anaplan.com/upload-and-import-a-csv-75094951-6424-446c-af03-cf9852af1179) * [https://help.anaplan.com/start-the-import-7e0dcedb-3628-44a1-8688-4a68eef85cdb](https://help.anaplan.com/start-the-import-7e0dcedb-3628-44a1-8688-4a68eef85cdb) * [https://help.anaplan.com/monitor-the-import-tasks-107143d0-91b3-4158-b2ee-c30a9ba4ee5f](https://help.anaplan.com/monitor-the-import-tasks-107143d0-91b3-4158-b2ee-c30a9ba4ee5f) * [https://www.postman.com/apiplan/official-anaplan-collection/documentation/2m6331d/official-anaplan-collection](https://www.postman.com/apiplan/official-anaplan-collection/documentation/2m6331d/official-anaplan-collection) # Anthropic Source: https://docs.coreflux.org/latest/lot-language/routes/rest-api-apps/anthropic Client-mode REST API connector for Anthropic Claude Messages and Message Batches — MQTT into shift-log summarization and nightly batches Technical Preview ## Overview A client-mode connector for a **curated** slice of the [Anthropic Claude API](https://docs.anthropic.com/en/api/getting-started) focused on IIoT / edge ops text into Claude: | Catalog connector event | Entity / path | Notes | | - | - | - | | POST messages | `POST /v1/messages` | Messages API — summarize shift log; extract fields with tools | | POST messages/batches | `POST /v1/messages/batches` | Message Batches — nightly / async batch (up to 24h) | | (supporting) GET models | `GET /v1/models` | List models (checks the token) | | (supporting) GET model | `GET /v1/models/{model}` | Retrieve one model | Default `BASE_ADDRESS` is `https://api.anthropic.com` (no trailing slash). Topics use `ai/anthropic//`. See the base guide: [REST API Connectors](/latest/lot-language/routes/rest-api-routes). > [!NOTE] > > **Auth:** `x-api-key: <Anthropic API key>` **and** `anthropic-version: 2023-06-01` on every request. Prefer Hub secrets (`GET SECRET "ANTHROPIC_API_KEY"`) mapped into MQTT `api_token`. Never commit real `sk-ant-...` keys. ## Basic Syntax ```lot wrap theme={"theme":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} # Coreflux REST_API template — Anthropic # Auth: x-api-key {value.json.api_token} + anthropic-version 2023-06-01 # BASE_ADDRESS: https://api.anthropic.com DEFINE CONNECTOR AnthropicRestApi WITH TYPE REST_API ADD REST_API_CONFIG WITH BASE_ADDRESS "https://api.anthropic.com" WITH ENABLE_CLIENT "true" WITH USE_SSL "true" WITH REQUEST_TIMEOUT '60' WITH DEFAULT_CONTENT_TYPE "application/json" ADD EVENT PostMessages WITH SOURCE_TOPIC "ai/anthropic/messages/post" WITH DESTINATION_TOPIC "ai/anthropic/messages/post/response" WITH METHOD "POST" WITH ENDPOINT "/v1/messages" WITH HEADERS ADD "Accept" "application/json" ADD "Content-Type" "application/json" ADD "x-api-key" "{value.json.api_token}" ADD "anthropic-version" "2023-06-01" WITH BODY "{payload}" ADD EVENT PostMessagesBatches WITH SOURCE_TOPIC "ai/anthropic/messages/batches/post" WITH DESTINATION_TOPIC "ai/anthropic/messages/batches/post/response" WITH METHOD "POST" WITH ENDPOINT "/v1/messages/batches" WITH HEADERS ADD "Accept" "application/json" ADD "Content-Type" "application/json" ADD "x-api-key" "{value.json.api_token}" ADD "anthropic-version" "2023-06-01" WITH BODY "{payload}" ``` ## Topics | Event | Source topic | Destination topic | | - | - | - | | PostMessages | `ai/anthropic/messages/post` | `…/response` | | PostMessagesBatches | `ai/anthropic/messages/batches/post` | `…/response` | | GetModels | `ai/anthropic/models/list` | `…/response` | | GetModel | `ai/anthropic/models/get` | `…/response` | ## Configuration 1. Set `BASE_ADDRESS` to `https://api.anthropic.com`. 2. Store an Anthropic API key in Hub secrets (`ANTHROPIC_API_KEY`); map into MQTT `api_token`. 3. Always send `anthropic-version: 2023-06-01` (already on every event in the template). 4. For shift-log summarization / tool use: publish to `ai/anthropic/messages/post` with a Messages body (`model`, `max_tokens`, `messages`, optional `tools`). 5. For nightly batches: publish to `ai/anthropic/messages/batches/post` with a `requests` array of `{custom_id, params}`. 6. Use `GetModels` as a cheap credential check before production workloads. ## References * [Anthropic API overview](https://docs.anthropic.com/en/api/getting-started) * [Messages](https://docs.anthropic.com/en/api/messages) * [Creating Message Batches](https://docs.anthropic.com/en/api/creating-message-batches) * [Models](https://docs.anthropic.com/en/api/models) # Apache Superset Source: https://docs.coreflux.org/latest/lot-language/routes/rest-api-apps/apache-superset Connect Coreflux LoT to Apache Superset to run chart and SQL Lab queries, upload CSV data, refresh datasets, add annotations and issue guest tokens Technical Preview ## Overview A client-mode connector for the [Apache Superset REST API](https://superset.apache.org/docs/api). Use it to put BI results on MQTT and plant events into Superset: run a chart query or a SQL Lab statement and publish the rows to a display, upload a CSV rollup (shift totals, OEE per line) into a database, register and refresh datasets, warm the chart cache when new data lands, and mark downtime, changeover or alarm windows as annotations on time-series charts. It also reads alerts and reports with their run logs and issues guest tokens for dashboards embedded on an HMI or kiosk. The connector `ApacheSupersetRestApi` in `ApacheSupersetRestApi.lot` has 38 events (26 GET, 9 POST, 3 PUT). 9 are writes: `DatasetCreate`, `DatasetRefresh`, `DatasetWarmUpCache`, `DatabaseCsvUpload`, `SqlLabExecute`, `AnnotationLayerCreate`, `AnnotationCreate`, `AnnotationUpdate`, `GuestTokenCreate`. There are no DELETE events. Set `BASE_ADDRESS` to your Superset web root with no trailing slash, for example `https://superset.example.com` (use `http://` and `USE_SSL "false"` for a plain-HTTP install on a LAN). Every endpoint already starts with `/api/v1`, except `/health`. Topics use `analytics/apache-superset//`; replies land on `/response`. See the base guide: [REST API Connectors](/latest/lot-language/routes/rest-api-routes). > [!NOTE] > > **Auth:** publish `{"body":{"username":"...","password":"...","provider":"db","refresh":true}}` to `analytics/apache-superset/auth/login` (`provider` is `ldap` for LDAP users). The reply carries a JWT `access_token`, which expires after 15 minutes by default, and a `refresh_token` when `refresh` is true. Every event except `HealthGet` and `SecurityLogin` sends `Authorization: Bearer `. To renew, publish `{"access_token":""}` to `analytics/apache-superset/auth/refresh`, or log in again. Use a dedicated Superset user with only the roles the broker needs. Keep `SUPERSET_USER` and `SUPERSET_PASSWORD` in broker secrets, read them with `GET SECRET`, never retain topics that carry a password or token, and restrict `analytics/apache-superset/auth/#` and `analytics/apache-superset/embedded/#` with ACLs. Deployments that only allow OAuth or SAML sign-in need a database or LDAP service user for the API. > [!WARNING] > > **CSRF on writes.** Superset 6.x checks a CSRF token on API `POST` and `PUT` calls even when they carry a JWT, and the token only works together with a browser session cookie, which a broker connector does not keep. Without a change, every write except `ChartDataQuery` answers `400 The CSRF token is missing`. Reads, `SecurityLogin`, `SecurityRefresh` and `ChartDataQuery` work as is. To allow the packaged writes, add their views to `WTF_CSRF_EXEMPT_LIST` in `superset_config.py` and restart Superset. This exempts only those views; a signed-in browser could then be tricked into calling them, so keep the list to the writes you use. ```python theme={"theme":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} import os from superset.config import WTF_CSRF_EXEMPT_LIST as _DEFAULT_EXEMPT SECRET_KEY = os.environ["SUPERSET_SECRET_KEY"] PREVENT_UNSAFE_DB_CONNECTIONS = False FEATURE_FLAGS = {"ALERT_REPORTS": True, "EMBEDDED_SUPERSET": True} # Coreflux ApacheSupersetRestApi route: API writes with a JWT and no browser session. WTF_CSRF_EXEMPT_LIST = [*_DEFAULT_EXEMPT, "superset.datasets.api.post", "superset.datasets.api.refresh", "superset.datasets.api.warm_up_cache", "superset.databases.api.upload", "superset.sqllab.api.execute_sql_query", "superset.annotation_layers.api.post", "superset.annotation_layers.annotations.api.post", "superset.annotation_layers.annotations.api.put", "superset.security.api.guest_token"] GUEST_TOKEN_JWT_SECRET = os.environ["SUPERSET_SECRET_KEY"] + "-guest" ``` ## Events | Event | Request | Source topic | Notes | | - | - | - | - | | HealthGet | `GET /health` | `analytics/apache-superset/health/get` | Smoke without login: answers OK when the web server is up. No Authorization header. | | SecurityLogin | `POST /api/v1/security/login` | `analytics/apache-superset/auth/login` | Log in and get a JWT access\_token (and refresh\_token with refresh:true). No Authorization header. | | SecurityRefresh | `POST /api/v1/security/refresh` | `analytics/apache-superset/auth/refresh` | New access\_token from a refresh token: send the refresh token as access\_token in the payload (Authorization: Bearer \). No body. | | CsrfTokenGet | `GET /api/v1/security/csrf_token/` | `analytics/apache-superset/auth/csrf_token` | CSRF token, only needed when the server enforces CSRF on API writes (see the docs page). Not needed with the default JWT setup tested here. | | MeGet | `GET /api/v1/me/` | `analytics/apache-superset/me/get` | Smoke: the user behind the token (id, username, first\_name, is\_active). | | MeRolesGet | `GET /api/v1/me/roles/` | `analytics/apache-superset/me/roles` | Roles and permissions of the token user. | | DashboardsList | `GET /api/v1/dashboard/?{value.json.query}` | `analytics/apache-superset/dashboards/list` | Dashboards. | | DashboardGet | `GET /api/v1/dashboard/{value.json.dashboard_id}` | `analytics/apache-superset/dashboards/get` | One dashboard by id or slug (title, url, owners, json\_metadata). | | DashboardChartsList | `GET /api/v1/dashboard/{value.json.dashboard_id}/charts` | `analytics/apache-superset/dashboards/charts` | Charts on a dashboard (ids, form\_data). | | DashboardDatasetsList | `GET /api/v1/dashboard/{value.json.dashboard_id}/datasets` | `analytics/apache-superset/dashboards/datasets` | Datasets behind a dashboard. | | ChartsList | `GET /api/v1/chart/?{value.json.query}` | `analytics/apache-superset/charts/list` | Charts. | | ChartGet | `GET /api/v1/chart/{value.json.chart_id}` | `analytics/apache-superset/charts/get` | One chart (viz\_type, params, query\_context). | | ChartDataGet | `GET /api/v1/chart/{value.json.chart_id}/data/?{value.json.query}` | `analytics/apache-superset/charts/data_get` | Result of a saved chart's query (catalog: chart result to MQTT). | | ChartDataQuery | `POST /api/v1/chart/data` | `analytics/apache-superset/charts/data_query` | Run an ad hoc query context against a dataset and publish the rows (catalog event: POST chart/data). Read-only. | | DatasetsList | `GET /api/v1/dataset/?{value.json.query}` | `analytics/apache-superset/datasets/list` | Datasets. | | DatasetGet | `GET /api/v1/dataset/{value.json.dataset_id}` | `analytics/apache-superset/datasets/get` | One dataset (columns, metrics, database, table\_name). | | DatasetCreate | `POST /api/v1/dataset/` | `analytics/apache-superset/datasets/create` | Write. Register a table or view as a dataset. | | DatasetRefresh | `PUT /api/v1/dataset/{value.json.dataset_id}/refresh` | `analytics/apache-superset/datasets/refresh` | Write (re-reads column metadata from the source table). Catalog event 'POST dataset/(id)/refresh' (the API method is PUT). No body. | | DatasetWarmUpCache | `PUT /api/v1/dataset/warm_up_cache` | `analytics/apache-superset/datasets/warm_up_cache` | Write (fills the chart cache). Warm the cache for every chart on a table after new data lands. | | DatabasesList | `GET /api/v1/database/?{value.json.query}` | `analytics/apache-superset/databases/list` | Database connections (no passwords). | | DatabaseGet | `GET /api/v1/database/{value.json.database_id}` | `analytics/apache-superset/databases/get` | One database connection (backend, allow\_dml, allow\_file\_upload). | | DatabaseSchemasList | `GET /api/v1/database/{value.json.database_id}/schemas/?{value.json.query}` | `analytics/apache-superset/databases/schemas` | Schemas of a database. | | DatabaseTablesList | `GET /api/v1/database/{value.json.database_id}/tables/?{value.json.query}` | `analytics/apache-superset/databases/tables` | Tables in a schema. | | DatabaseCsvUpload | `POST /api/v1/database/{value.json.database_id}/upload/` | `analytics/apache-superset/databases/upload_csv` | Write (creates or appends a table). Upload a CSV rollup (shift totals, OEE per line) into a database that allows file upload. | | SqlLabExecute | `POST /api/v1/sqllab/execute/` | `analytics/apache-superset/sqllab/execute` | Write only when the database allows DML. Run SQL and publish the rows. | | SqlLabResultsGet | `GET /api/v1/sqllab/results/?{value.json.query}` | `analytics/apache-superset/sqllab/results` | Results of an async SQL Lab query. | | QueriesList | `GET /api/v1/query/?{value.json.query}` | `analytics/apache-superset/queries/list` | SQL Lab query history (status, rows, error). | | SavedQueriesList | `GET /api/v1/saved_query/?{value.json.query}` | `analytics/apache-superset/saved_queries/list` | Saved SQL Lab queries. | | ReportsList | `GET /api/v1/report/?{value.json.query}` | `analytics/apache-superset/reports/list` | Alerts and reports (needs ALERT\_REPORTS feature flag). | | ReportGet | `GET /api/v1/report/{value.json.report_id}` | `analytics/apache-superset/reports/get` | One alert or report (crontab, recipients, last\_state). | | ReportLogsList | `GET /api/v1/report/{value.json.report_id}/log/?{value.json.query}` | `analytics/apache-superset/reports/logs` | Execution log of an alert or report (state, error\_message, end\_dttm). | | AnnotationLayersList | `GET /api/v1/annotation_layer/?{value.json.query}` | `analytics/apache-superset/annotation_layers/list` | Annotation layers (overlays on time-series charts). | | AnnotationLayerCreate | `POST /api/v1/annotation_layer/` | `analytics/apache-superset/annotation_layers/create` | Write. Create a layer for plant events. | | AnnotationsList | `GET /api/v1/annotation_layer/{value.json.layer_id}/annotation/?{value.json.query}` | `analytics/apache-superset/annotations/list` | Annotations in a layer. | | AnnotationCreate | `POST /api/v1/annotation_layer/{value.json.layer_id}/annotation/` | `analytics/apache-superset/annotations/create` | Write. Mark a downtime, changeover or alarm window on time-series charts. | | AnnotationUpdate | `PUT /api/v1/annotation_layer/{value.json.layer_id}/annotation/{value.json.annotation_id}` | `analytics/apache-superset/annotations/update` | Write. Close an open window (set end\_dttm) or fix its text. body e.g. ("end\_dttm":"2026-10-05T08:12:00"). | | LogRecentActivity | `GET /api/v1/log/recent_activity/?{value.json.query}` | `analytics/apache-superset/logs/recent_activity` | Recent views and edits of the token user. | | GuestTokenCreate | `POST /api/v1/security/guest_token/` | `analytics/apache-superset/embedded/guest_token` | Write (issues a token). Guest token for an embedded dashboard on an HMI or kiosk (needs EMBEDDED\_SUPERSET and the can\_grant\_guest\_token permission). | Payload fields: `access_token`; ids `dashboard_id` (id or slug), `chart_id`, `dataset_id`, `database_id`, `report_id`, `layer_id`, `annotation_id`; `query` (URL query string without `?`, or `""`; list endpoints take Rison in `q`, for example `q=(page_size:20)`); `body` (JSON); `multipart` (whole `multipart/form-data` body for `DatabaseCsvUpload`). ## Basic Syntax ```lot theme={"theme":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} DEFINE CONNECTOR ApacheSupersetRestApi WITH TYPE REST_API ADD REST_API_CONFIG WITH BASE_ADDRESS "https://superset.example.com" WITH ENABLE_CLIENT "true" WITH USE_SSL "true" WITH REQUEST_TIMEOUT '120' WITH DEFAULT_CONTENT_TYPE "application/json" ADD EVENT SecurityLogin WITH SOURCE_TOPIC "analytics/apache-superset/auth/login" WITH DESTINATION_TOPIC "analytics/apache-superset/auth/login/response" WITH METHOD "POST" WITH ENDPOINT "/api/v1/security/login" WITH HEADERS ADD "Accept" "application/json" ADD "User-Agent" "Coreflux-Broker REST_API route (https://www.coreflux.org)" ADD "Content-Type" "application/json" WITH BODY "{value.json.body}" ADD EVENT MeGet WITH SOURCE_TOPIC "analytics/apache-superset/me/get" WITH DESTINATION_TOPIC "analytics/apache-superset/me/get/response" WITH METHOD "GET" WITH ENDPOINT "/api/v1/me/" WITH HEADERS ADD "Accept" "application/json" ADD "User-Agent" "Coreflux-Broker REST_API route (https://www.coreflux.org)" ADD "Authorization" "Bearer {value.json.access_token}" ADD EVENT ChartDataQuery WITH SOURCE_TOPIC "analytics/apache-superset/charts/data_query" WITH DESTINATION_TOPIC "analytics/apache-superset/charts/data_query/response" WITH METHOD "POST" WITH ENDPOINT "/api/v1/chart/data" WITH HEADERS ADD "Accept" "application/json" ADD "User-Agent" "Coreflux-Broker REST_API route (https://www.coreflux.org)" ADD "Authorization" "Bearer {value.json.access_token}" ADD "Content-Type" "application/json" WITH BODY "{value.json.body}" ADD EVENT DatabaseCsvUpload WITH SOURCE_TOPIC "analytics/apache-superset/databases/upload_csv" WITH DESTINATION_TOPIC "analytics/apache-superset/databases/upload_csv/response" WITH METHOD "POST" WITH ENDPOINT "/api/v1/database/{value.json.database_id}/upload/" WITH HEADERS ADD "Accept" "application/json" ADD "User-Agent" "Coreflux-Broker REST_API route (https://www.coreflux.org)" ADD "Authorization" "Bearer {value.json.access_token}" ADD "Content-Type" "multipart/form-data; boundary=CorefluxSupersetBoundary" WITH BODY "{value.json.multipart}" ADD EVENT AnnotationCreate WITH SOURCE_TOPIC "analytics/apache-superset/annotations/create" WITH DESTINATION_TOPIC "analytics/apache-superset/annotations/create/response" WITH METHOD "POST" WITH ENDPOINT "/api/v1/annotation_layer/{value.json.layer_id}/annotation/" WITH HEADERS ADD "Accept" "application/json" ADD "User-Agent" "Coreflux-Broker REST_API route (https://www.coreflux.org)" ADD "Authorization" "Bearer {value.json.access_token}" ADD "Content-Type" "application/json" WITH BODY "{value.json.body}" ``` ## Examples ### Keep a token for the other actions ```lot theme={"theme":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} DEFINE ACTION SupersetLogin ON EVERY 10 MINUTES DO SET "user" WITH GET SECRET "SUPERSET_USER" SET "password" WITH GET SECRET "SUPERSET_PASSWORD" PUBLISH TOPIC "analytics/apache-superset/auth/login" WITH { "body": { "username": {user}, "password": {password}, "provider": "db", "refresh": false } } DEFINE ACTION SupersetKeepToken ON TOPIC "analytics/apache-superset/auth/login/response" DO SET "token" WITH GET JSON "access_token" IN PAYLOAD AS STRING KEEP TOPIC "state/superset/access_token" WITH {token} ``` ### Mark a downtime window on charts Create a layer once with `AnnotationLayerCreate`, then add an annotation when a stop starts and close it with `AnnotationUpdate` when the line runs again. Times are local timestamps without a zone, for example `2026-10-05T08:00:00`. ```lot theme={"theme":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} DEFINE ACTION SupersetMarkDowntime ON TOPIC "plant/line2/downtime/start" DO SET "token" WITH GET TOPIC "state/superset/access_token" AS STRING SET "start" WITH GET JSON "start" IN PAYLOAD AS STRING PUBLISH TOPIC "analytics/apache-superset/annotations/create" WITH { "access_token": {token}, "layer_id": "3", "body": { "short_descr": "Line 2 stop", "start_dttm": {start}, "end_dttm": {start} } } ``` The reply on `analytics/apache-superset/annotations/create/response` carries the annotation `id`. Publish `{"access_token":"...","layer_id":"3","annotation_id":"","body":{"end_dttm":"2026-10-05T08:12:00"}}` to `analytics/apache-superset/annotations/update` to close the window. ### Publish chart rows to a display `ChartDataQuery` runs a query context against a dataset and returns the rows in `result[0].data`: ```json theme={"theme":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} {"access_token":"...","body":{"datasource":{"id":4,"type":"table"},"queries":[{"columns":["line"],"metrics":[{"expressionType":"SQL","sqlExpression":"SUM(units)","label":"units"}],"row_limit":100}],"result_format":"json","result_type":"full"}} ``` For a saved chart, publish `{"access_token":"...","chart_id":"12","query":"format=json&type=full"}` to `analytics/apache-superset/charts/data_get`. The chart needs a saved query context (charts saved from the Explore view in recent versions have one). ### Upload a CSV rollup `DatabaseCsvUpload` sends `multipart` unchanged with `Content-Type: multipart/form-data; boundary=CorefluxSupersetBoundary`. Use CRLF line endings. The database needs **Allow file uploads to database** and the schema must be allowed for uploads. Superset 6.x registers the new table as a dataset itself. ```text theme={"theme":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} --CorefluxSupersetBoundary Content-Disposition: form-data; name="type" csv --CorefluxSupersetBoundary Content-Disposition: form-data; name="table_name" shift_rollup --CorefluxSupersetBoundary Content-Disposition: form-data; name="schema" main --CorefluxSupersetBoundary Content-Disposition: form-data; name="already_exists" append --CorefluxSupersetBoundary Content-Disposition: form-data; name="file"; filename="rollup.csv" Content-Type: text/csv line,shift,units,oee Line 1,A,1200,0.81 --CorefluxSupersetBoundary-- ``` ### SQL Lab `SqlLabExecute` runs SQL synchronously with `"runAsync":false` and returns the rows in `data`. Keep **Allow DML** off on the connection so the connector can only read: a mutating statement then fails with `This database does not allow for DDL/DML`. `SqlLabResultsGet` only applies to async queries with a results backend. ## Not packaged * DELETE operations, on purpose. * Alert and report create and update: those views share one wrapped handler with report deletes and plugin forms, so they cannot be CSRF-exempted on their own. Create alerts in the Superset UI; the connector reads them with `ReportsList`, `ReportGet` and `ReportLogsList`. * Import and export ZIPs, screenshots and thumbnails, security, role and user administration, row-level security, themes, tags, explore form data and permalinks. # Apprise API Source: https://docs.coreflux.org/latest/lot-language/routes/rest-api-apps/apprise-api LoT connector for the Apprise API: send stateless or stateful notifications, check health, list supported services and save or inspect URL configs Technical Preview ## Overview A client-mode connector for a **curated messaging** slice of the [Apprise API](https://github.com/caronc/apprise-api) (Docker `caronc/apprise`), the lightweight REST wrapper around the [Apprise](https://github.com/caronc/apprise) notification library (100+ services): | Catalog connector event | Entity / path | Notes | | - | - | - | | POST notify/\{key} | `POST /notify/{key}` | Stateful send through a saved Config ID (`title`, `body`, `type`) | | POST notify | `POST /notify` | Stateless send with explicit `urls` list (+ `body`) | | GET status | `GET /status` | Health → **200** OK / JSON (or **417** on config issues) | | GET details | `GET /details` | Supported Apprise URL schemas (`Accept: application/json`) | | POST add/\{key} | `POST /add/{key}` | Persist URLs/config under a key | | GET json/urls/\{key} | `GET /json/urls/{key}` | Inspect saved URLs/tags (`privacy=1` redacts secrets) | Default `BASE_ADDRESS` placeholder is `https://apprise.example.com`. Replace with your self-hosted origin via `APPRISE_BASE_URL` (common local Docker: `http://localhost:8000`). Topics use `messaging/apprise-api//`. > [!NOTE] > > Auth is **deployment-dependent**: many lab containers expose endpoints with no credentials. When the server sets `APPRISE_AUTH_REQUIRED`, use HTTP Basic (`APPRISE_USER` / `APPRISE_PASSWORD`) or per-key Basic. Prefer Hub secrets. ## Basic Syntax ```lot wrap theme={"theme":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} # Coreflux REST_API template — Apprise API # BASE_ADDRESS: set from APPRISE_BASE_URL (placeholder below) # Optional Basic Auth when APPRISE_AUTH_REQUIRED DEFINE CONNECTOR AppriseApiRestApi WITH TYPE REST_API ADD REST_API_CONFIG WITH BASE_ADDRESS "https://apprise.example.com" WITH ENABLE_CLIENT "true" WITH USE_SSL "true" WITH REQUEST_TIMEOUT '60' WITH DEFAULT_CONTENT_TYPE "application/json" ADD EVENT Getstatus WITH SOURCE_TOPIC "messaging/apprise-api/status/get" WITH DESTINATION_TOPIC "messaging/apprise-api/status/get/response" WITH METHOD "GET" WITH ENDPOINT "/status" WITH HEADERS ADD "Accept" "application/json" ADD EVENT Getdetails WITH SOURCE_TOPIC "messaging/apprise-api/details/get" WITH DESTINATION_TOPIC "messaging/apprise-api/details/get/response" WITH METHOD "GET" WITH ENDPOINT "/details" WITH HEADERS ADD "Accept" "application/json" ADD EVENT Notifystateless WITH SOURCE_TOPIC "messaging/apprise-api/notify/stateless" WITH DESTINATION_TOPIC "messaging/apprise-api/notify/stateless/response" WITH METHOD "POST" WITH ENDPOINT "/notify" WITH HEADERS ADD "Accept" "application/json" ADD "Content-Type" "application/json" WITH BODY "{payload}" ADD EVENT Notifybykey WITH SOURCE_TOPIC "messaging/apprise-api/notify/by-key" WITH DESTINATION_TOPIC "messaging/apprise-api/notify/by-key/response" WITH METHOD "POST" WITH ENDPOINT "/notify/{value.json.key}" WITH HEADERS ADD "Accept" "application/json" ADD "Content-Type" "application/json" WITH BODY "{payload}" ADD EVENT Addconfiguration WITH SOURCE_TOPIC "messaging/apprise-api/config/add" WITH DESTINATION_TOPIC "messaging/apprise-api/config/add/response" WITH METHOD "POST" WITH ENDPOINT "/add/{value.json.key}" WITH HEADERS ADD "Accept" "application/json" ADD "Content-Type" "application/json" WITH BODY "{payload}" ADD EVENT Geturls WITH SOURCE_TOPIC "messaging/apprise-api/urls/get" WITH DESTINATION_TOPIC "messaging/apprise-api/urls/get/response" WITH METHOD "GET" WITH ENDPOINT "/json/urls/{value.json.key}?privacy={value.json.privacy}" WITH HEADERS ADD "Accept" "application/json" ``` ## Events | Event | Method | Endpoint | Request topic | Response topic | | - | - | - | - | - | | Getstatus | GET | `/status` | `messaging/apprise-api/status/get` | `messaging/apprise-api/status/get/response` | | Getdetails | GET | `/details` | `messaging/apprise-api/details/get` | `messaging/apprise-api/details/get/response` | | Notifystateless | POST | `/notify` | `messaging/apprise-api/notify/stateless` | `messaging/apprise-api/notify/stateless/response` | | Notifybykey | POST | `/notify/{key}` | `messaging/apprise-api/notify/by-key` | `messaging/apprise-api/notify/by-key/response` | | Addconfiguration | POST | `/add/{key}` | `messaging/apprise-api/config/add` | `messaging/apprise-api/config/add/response` | | Geturls | GET | `/json/urls/{key}` | `messaging/apprise-api/urls/get` | `messaging/apprise-api/urls/get/response` | ## Auth | Mode | How | | - | - | | Open lab instance | No credentials; check with `GET /status` | | Global Basic Auth | `APPRISE_USER` + `APPRISE_PASSWORD` (Hub `GET SECRET`) on the connector config | | Per-key Basic | Credentials assigned to a Config ID; still use Basic header | | Stateful key | Path `{key}` from MQTT `value.json.key` (env alias `APPRISE_KEY` / `APPRISE_CONFIG_ID`) | Never put real passwords in the `.lot` file. Optional Basic: ```lot theme={"theme":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} WITH USERNAME "YOUR_APPRISE_USER" WITH PASSWORD GET SECRET "APPRISE_PASSWORD" ``` ## Example payloads **Stateless notify** (urls in body): ```json theme={"theme":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} { "urls": ["json://localhost"], "title": "Zone A", "body": "RMS high", "type": "warning" } ``` **Stateful notify** (saved key): ```json theme={"theme":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} { "key": "plant-alerts", "title": "Zone A trip", "body": "E-stop engaged", "type": "failure" } ``` **Add configuration:** ```json theme={"theme":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} { "key": "plant-alerts", "urls": ["tgram://BOT_TOKEN/CHAT_ID/"] } ``` ## BASE\_ADDRESS Self-hosted only — there is no shared public Apprise API tenant for Coreflux alert: | Env / setting | Example | | - | - | | `APPRISE_BASE_URL` → `BASE_ADDRESS` | `http://localhost:8000` | | HTTPS reverse proxy | `https://apprise.your-plant.local` | | Placeholder in package | `https://apprise.example.com` | If you use plain HTTP, set `WITH USE_SSL "false"`. ## Server / inbound companion This package is **client-mode first** (MQTT → HTTP to Apprise). Apprise API does not provide a general inbound webhook-to-MQTT surface for Coreflux; notification *destinations* are Apprise URLs (Slack, Telegram, email, …). If you need HTTP callbacks into the broker, use a separate `ENABLE_SERVER` companion connector (e.g. ntfy action buttons or your own webhook) — not included here. ## Deploy notes * Deploy `AppriseApiRestApi.lot` only (no `ADD METADATA`). * Hub catalog metadata lives in `hub-metadata.lot` (`WITH STATUS "beta"`, icon, doclink). * Docs: [https://docs.coreflux.org/latest/lot-language/routes/rest-api-apps/apprise-api](https://docs.coreflux.org/latest/lot-language/routes/rest-api-apps/apprise-api) ## Sources * [Apprise API](https://github.com/caronc/apprise-api) * [OpenAPI / swagger.yaml](https://github.com/caronc/apprise-api/blob/master/swagger.yaml) * [API Endpoints](https://appriseit.com/api/endpoints/) * [Apprise library](https://github.com/caronc/apprise) * Docker image `caronc/apprise` # Aras Innovator Api Source: https://docs.coreflux.org/latest/lot-language/routes/rest-api-apps/aras-innovator-api Connect Coreflux LoT to Aras Innovator PLM to query parts, BOMs, documents, problem reports and ECNs over the OData REST API with OAuth tokens Technical Preview ## Overview Client-mode REST API connector for Aras Innovator PLM: query parts, read BOMs, where-used, CAD and document links, documents and files, problem reports, ECRs and ECNs, and turn an inspection or quality event into a problem report or a BOM line, through the Aras RESTful API (OData v4) with OAuth tokens. This is one of the templates in the Aras Innovator REST API app (`aras-innovator`). Deploy it alongside the other connectors of that app when they depend on each other (for example a token connector plus an API connector). ## Auth OAuth 2.0 Bearer from the Aras OAuth server (\/oauthserver/connect/token). ArasInnovatorAuth GetToken: password grant, scope Innovator, client\_id (IOMApp by default), username, password = MD5 hex hash (SHA on FIPS servers), database; returns access\_token (3600 s default), token\_type Bearer. RefreshToken: refresh\_token grant for offline\_access logins (14+ PKCE, client InnovatorClient). ArasInnovatorApi events take payload field authorization = 'Bearer \'. DATABASE / AUTHUSER / AUTHPASSWORD headers are unsupported since 12.0. Auth headers used by the template: `Authorization: {value.json.authorization}`. ## Template parameters | Placeholder | Type | Description | Required | | - | - | - | - | | `RouteName` | string | Name of the Aras Innovator Api connector | no | | `BaseUrl` | string | Aras Innovator Api base URL without trailing slash (e.g. https\://YOUR\_ARAS\_SERVER) | yes | `BASE_ADDRESS` default: `https://YOUR_ARAS_SERVER`. ## Events | Event | Method | Endpoint | Request topic | Response topic | Payload fields | | - | - | - | - | - | - | | `QueryParts` | GET | `/InnovatorServer/Server/odata/Part?$filter={value.json.filter}&$top={value.json.top}` | `mes/aras-innovator/parts/query` | `mes/aras-innovator/parts/query/response` | `authorization`, `filter`, `top` | | `GetPart` | GET | `/InnovatorServer/Server/odata/Part('{value.json.id}')` | `mes/aras-innovator/part/get` | `mes/aras-innovator/part/get/response` | `authorization`, `id` | | `GetPartBom` | GET | `/InnovatorServer/Server/odata/Part('{value.json.id}')/Part_BOM?$expand=related_id` | `mes/aras-innovator/part-bom/get` | `mes/aras-innovator/part-bom/get/response` | `authorization`, `id` | | `GetPartWhereUsed` | GET | `/InnovatorServer/Server/odata/Part?$filter=[Part%20BOM]/related_id/item_number%20eq%20'{value.json.item_number}'&$select=id,item_number,name,state` | `mes/aras-innovator/part-where-used/get` | `mes/aras-innovator/part-where-used/get/response` | `authorization`, `item_number` | | `GetPartCad` | GET | `/InnovatorServer/Server/odata/Part('{value.json.id}')/Part_CAD?$expand=related_id` | `mes/aras-innovator/part-cad/get` | `mes/aras-innovator/part-cad/get/response` | `authorization`, `id` | | `GetPartDocuments` | GET | `/InnovatorServer/Server/odata/Part('{value.json.id}')/Part_Document?$expand=related_id` | `mes/aras-innovator/part-documents/get` | `mes/aras-innovator/part-documents/get/response` | `authorization`, `id` | | `QueryDocuments` | GET | `/InnovatorServer/Server/odata/Document?$filter={value.json.filter}&$top={value.json.top}` | `mes/aras-innovator/documents/query` | `mes/aras-innovator/documents/query/response` | `authorization`, `filter`, `top` | | `GetDocument` | GET | `/InnovatorServer/Server/odata/Document('{value.json.id}')` | `mes/aras-innovator/document/get` | `mes/aras-innovator/document/get/response` | `authorization`, `id` | | `GetDocumentFiles` | GET | `/InnovatorServer/Server/odata/Document('{value.json.id}')/Document_File?$expand=related_id` | `mes/aras-innovator/document-files/get` | `mes/aras-innovator/document-files/get/response` | `authorization`, `id` | | `GetFileInfo` | GET | `/InnovatorServer/Server/odata/File('{value.json.id}')` | `mes/aras-innovator/file/get` | `mes/aras-innovator/file/get/response` | `authorization`, `id` | | `ListProblemReports` | GET | `/InnovatorServer/Server/odata/PR?$filter={value.json.filter}&$top={value.json.top}` | `mes/aras-innovator/problem-reports/list` | `mes/aras-innovator/problem-reports/list/response` | `authorization`, `filter`, `top` | | `GetProblemReport` | GET | `/InnovatorServer/Server/odata/PR('{value.json.id}')?$expand=affected_item` | `mes/aras-innovator/problem-report/get` | `mes/aras-innovator/problem-report/get/response` | `authorization`, `id` | | `CreateProblemReport` | POST | `/InnovatorServer/Server/odata/PR` | `mes/aras-innovator/problem-report/create` | `mes/aras-innovator/problem-report/create/response` | `authorization`, `request` | | `ListEcrs` | GET | `/InnovatorServer/Server/odata/ECR?$filter={value.json.filter}&$top={value.json.top}` | `mes/aras-innovator/ecrs/list` | `mes/aras-innovator/ecrs/list/response` | `authorization`, `filter`, `top` | | `ListEcns` | GET | `/InnovatorServer/Server/odata/ECN?$filter={value.json.filter}&$top={value.json.top}` | `mes/aras-innovator/ecns/list` | `mes/aras-innovator/ecns/list/response` | `authorization`, `filter`, `top` | | `AddPartBomLine` | POST | `/InnovatorServer/Server/odata/Part_BOM` | `mes/aras-innovator/part-bom/create` | `mes/aras-innovator/part-bom/create/response` | `authorization`, `request` | | `QueryItems` | GET | `/InnovatorServer/Server/odata/{value.json.itemtype}?$filter={value.json.filter}&$top={value.json.top}` | `mes/aras-innovator/items/query` | `mes/aras-innovator/items/query/response` | `authorization`, `filter`, `itemtype`, `top` | | `QueryItemTypes` | GET | `/InnovatorServer/Server/odata/ItemType?$filter={value.json.filter}&$select=id,name,label` | `mes/aras-innovator/item-types/query` | `mes/aras-innovator/item-types/query/response` | `authorization`, `filter` | ## Deploy Get the template card (JSON with `template` and `metadata`) from the broker, then deploy the connector: ```bash theme={"theme":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} mosquitto_pub -t '$SYS/Coreflux/Command' -m "-connectorCode ARAS_INNOVATOR" ``` The deployable connector from the REST API app, with `BASE_ADDRESS` already filled in: ```lot theme={"theme":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} DEFINE CONNECTOR ArasInnovatorApi WITH TYPE REST_API ADD REST_API_CONFIG WITH BASE_ADDRESS "https://YOUR_ARAS_SERVER" WITH ENABLE_CLIENT "true" WITH USE_SSL "true" WITH REQUEST_TIMEOUT '60' WITH DEFAULT_CONTENT_TYPE "application/json" ADD EVENT QueryParts WITH SOURCE_TOPIC "mes/aras-innovator/parts/query" WITH DESTINATION_TOPIC "mes/aras-innovator/parts/query/response" WITH METHOD "GET" WITH ENDPOINT "/InnovatorServer/Server/odata/Part?$filter={value.json.filter}&$top={value.json.top}" WITH HEADERS ADD "Accept" "application/json" ADD "Authorization" "{value.json.authorization}" ADD EVENT GetPart WITH SOURCE_TOPIC "mes/aras-innovator/part/get" WITH DESTINATION_TOPIC "mes/aras-innovator/part/get/response" WITH METHOD "GET" WITH ENDPOINT "/InnovatorServer/Server/odata/Part('{value.json.id}')" WITH HEADERS ADD "Accept" "application/json" ADD "Authorization" "{value.json.authorization}" ADD EVENT GetPartBom WITH SOURCE_TOPIC "mes/aras-innovator/part-bom/get" WITH DESTINATION_TOPIC "mes/aras-innovator/part-bom/get/response" WITH METHOD "GET" WITH ENDPOINT "/InnovatorServer/Server/odata/Part('{value.json.id}')/Part_BOM?$expand=related_id" WITH HEADERS ADD "Accept" "application/json" ADD "Authorization" "{value.json.authorization}" ADD EVENT GetPartWhereUsed WITH SOURCE_TOPIC "mes/aras-innovator/part-where-used/get" WITH DESTINATION_TOPIC "mes/aras-innovator/part-where-used/get/response" WITH METHOD "GET" WITH ENDPOINT "/InnovatorServer/Server/odata/Part?$filter=[Part%20BOM]/related_id/item_number%20eq%20'{value.json.item_number}'&$select=id,item_number,name,state" WITH HEADERS ADD "Accept" "application/json" ADD "Authorization" "{value.json.authorization}" ADD EVENT GetPartCad WITH SOURCE_TOPIC "mes/aras-innovator/part-cad/get" WITH DESTINATION_TOPIC "mes/aras-innovator/part-cad/get/response" WITH METHOD "GET" WITH ENDPOINT "/InnovatorServer/Server/odata/Part('{value.json.id}')/Part_CAD?$expand=related_id" WITH HEADERS ADD "Accept" "application/json" ADD "Authorization" "{value.json.authorization}" ADD EVENT GetPartDocuments WITH SOURCE_TOPIC "mes/aras-innovator/part-documents/get" WITH DESTINATION_TOPIC "mes/aras-innovator/part-documents/get/response" WITH METHOD "GET" WITH ENDPOINT "/InnovatorServer/Server/odata/Part('{value.json.id}')/Part_Document?$expand=related_id" WITH HEADERS ADD "Accept" "application/json" ADD "Authorization" "{value.json.authorization}" ADD EVENT QueryDocuments WITH SOURCE_TOPIC "mes/aras-innovator/documents/query" WITH DESTINATION_TOPIC "mes/aras-innovator/documents/query/response" WITH METHOD "GET" WITH ENDPOINT "/InnovatorServer/Server/odata/Document?$filter={value.json.filter}&$top={value.json.top}" WITH HEADERS ADD "Accept" "application/json" ADD "Authorization" "{value.json.authorization}" ADD EVENT GetDocument WITH SOURCE_TOPIC "mes/aras-innovator/document/get" WITH DESTINATION_TOPIC "mes/aras-innovator/document/get/response" WITH METHOD "GET" WITH ENDPOINT "/InnovatorServer/Server/odata/Document('{value.json.id}')" WITH HEADERS ADD "Accept" "application/json" ADD "Authorization" "{value.json.authorization}" ADD EVENT GetDocumentFiles WITH SOURCE_TOPIC "mes/aras-innovator/document-files/get" WITH DESTINATION_TOPIC "mes/aras-innovator/document-files/get/response" WITH METHOD "GET" WITH ENDPOINT "/InnovatorServer/Server/odata/Document('{value.json.id}')/Document_File?$expand=related_id" WITH HEADERS ADD "Accept" "application/json" ADD "Authorization" "{value.json.authorization}" ADD EVENT GetFileInfo WITH SOURCE_TOPIC "mes/aras-innovator/file/get" WITH DESTINATION_TOPIC "mes/aras-innovator/file/get/response" WITH METHOD "GET" WITH ENDPOINT "/InnovatorServer/Server/odata/File('{value.json.id}')" WITH HEADERS ADD "Accept" "application/json" ADD "Authorization" "{value.json.authorization}" ADD EVENT ListProblemReports WITH SOURCE_TOPIC "mes/aras-innovator/problem-reports/list" WITH DESTINATION_TOPIC "mes/aras-innovator/problem-reports/list/response" WITH METHOD "GET" WITH ENDPOINT "/InnovatorServer/Server/odata/PR?$filter={value.json.filter}&$top={value.json.top}" WITH HEADERS ADD "Accept" "application/json" ADD "Authorization" "{value.json.authorization}" ADD EVENT GetProblemReport WITH SOURCE_TOPIC "mes/aras-innovator/problem-report/get" WITH DESTINATION_TOPIC "mes/aras-innovator/problem-report/get/response" WITH METHOD "GET" WITH ENDPOINT "/InnovatorServer/Server/odata/PR('{value.json.id}')?$expand=affected_item" WITH HEADERS ADD "Accept" "application/json" ADD "Authorization" "{value.json.authorization}" ADD EVENT CreateProblemReport WITH SOURCE_TOPIC "mes/aras-innovator/problem-report/create" WITH DESTINATION_TOPIC "mes/aras-innovator/problem-report/create/response" WITH METHOD "POST" WITH ENDPOINT "/InnovatorServer/Server/odata/PR" WITH HEADERS ADD "Accept" "application/json" ADD "Authorization" "{value.json.authorization}" ADD "Content-Type" "application/json" ADD "Prefer" "return=representation" WITH BODY "{value.json.request}" ADD EVENT ListEcrs WITH SOURCE_TOPIC "mes/aras-innovator/ecrs/list" WITH DESTINATION_TOPIC "mes/aras-innovator/ecrs/list/response" WITH METHOD "GET" WITH ENDPOINT "/InnovatorServer/Server/odata/ECR?$filter={value.json.filter}&$top={value.json.top}" WITH HEADERS ADD "Accept" "application/json" ADD "Authorization" "{value.json.authorization}" ADD EVENT ListEcns WITH SOURCE_TOPIC "mes/aras-innovator/ecns/list" WITH DESTINATION_TOPIC "mes/aras-innovator/ecns/list/response" WITH METHOD "GET" WITH ENDPOINT "/InnovatorServer/Server/odata/ECN?$filter={value.json.filter}&$top={value.json.top}" WITH HEADERS ADD "Accept" "application/json" ADD "Authorization" "{value.json.authorization}" ADD EVENT AddPartBomLine WITH SOURCE_TOPIC "mes/aras-innovator/part-bom/create" WITH DESTINATION_TOPIC "mes/aras-innovator/part-bom/create/response" WITH METHOD "POST" WITH ENDPOINT "/InnovatorServer/Server/odata/Part_BOM" WITH HEADERS ADD "Accept" "application/json" ADD "Authorization" "{value.json.authorization}" ADD "Content-Type" "application/json" ADD "Prefer" "return=representation" WITH BODY "{value.json.request}" ADD EVENT QueryItems WITH SOURCE_TOPIC "mes/aras-innovator/items/query" WITH DESTINATION_TOPIC "mes/aras-innovator/items/query/response" WITH METHOD "GET" WITH ENDPOINT "/InnovatorServer/Server/odata/{value.json.itemtype}?$filter={value.json.filter}&$top={value.json.top}" WITH HEADERS ADD "Accept" "application/json" ADD "Authorization" "{value.json.authorization}" ADD EVENT QueryItemTypes WITH SOURCE_TOPIC "mes/aras-innovator/item-types/query" WITH DESTINATION_TOPIC "mes/aras-innovator/item-types/query/response" WITH METHOD "GET" WITH ENDPOINT "/InnovatorServer/Server/odata/ItemType?$filter={value.json.filter}&$select=id,name,label" WITH HEADERS ADD "Accept" "application/json" ADD "Authorization" "{value.json.authorization}" ``` Deploy it with `-addConnector` (do not include `ADD METADATA`). ## Usage example Call `QueryParts` by publishing a JSON payload to its request topic, and read the reply on the response topic: ```bash theme={"theme":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} mosquitto_sub -t 'mes/aras-innovator/parts/query/response' & mosquitto_pub -t 'mes/aras-innovator/parts/query' -m '{"authorization": "", "filter": "", "top": ""}' ``` ## Related * [REST API Connectors](/latest/lot-language/routes/rest-api-routes) * [REST API Apps catalog](/latest/lot-language/routes/rest-api-apps) # Asana Source: https://docs.coreflux.org/latest/lot-language/routes/rest-api-apps/asana Client-mode REST API connector for a curated Asana REST API v1.0 slice: create/update tasks, list project tasks, users/me check, workspaces, and webhook create Technical Preview ## Overview A client-mode connector for a **curated work-management** slice of the [Asana REST API](https://developers.asana.com/reference/rest-api-reference): | Catalog connector event | Entity / path | Notes | | - | - | - | | POST tasks | `POST /tasks` | Create task from alarm / MQTT (body `{"data":{…}}`) | | PUT tasks/\{gid} | `PUT /tasks/{task_gid}` | Update task fields / complete | | GET projects/\{gid}/tasks | `GET /projects/{project_gid}/tasks` | List project tasks | | GET users/me | `GET /users/{user_gid}` with `me` | Credential check → `data.gid` | | GET workspaces | `GET /workspaces` | Workspace discovery | | GET tasks/\{gid} | `GET /tasks/{task_gid}` | Single task read | | POST webhooks | `POST /webhooks` | Register Asana → HTTPS target | Default `BASE_ADDRESS` is `https://app.asana.com/api/1.0`. Topics use `devops/asana//`. > [!NOTE] > > Auth is **Bearer** personal access token or OAuth 2.0 access token (`Authorization: Bearer …`). Prefer Hub secret `ASANA_ACCESS_TOKEN` (aliases `ASANA_PAT`, `ASANA_TOKEN`). ## Basic Syntax ```lot wrap theme={"theme":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} # Coreflux REST_API template — Asana REST API v1.0 # Auth: Bearer {value.json.access_token} (PAT or OAuth) # BASE_ADDRESS: https://app.asana.com/api/1.0 DEFINE CONNECTOR AsanaRestApi WITH TYPE REST_API ADD REST_API_CONFIG WITH BASE_ADDRESS "https://app.asana.com/api/1.0" WITH ENABLE_CLIENT "true" WITH USE_SSL "true" WITH REQUEST_TIMEOUT '60' WITH DEFAULT_CONTENT_TYPE "application/json" ADD EVENT Getuser WITH SOURCE_TOPIC "devops/asana/user/get" WITH DESTINATION_TOPIC "devops/asana/user/get/response" WITH METHOD "GET" WITH ENDPOINT "/users/{value.json.user_gid}" WITH HEADERS ADD "Accept" "application/json" ADD "Authorization" "Bearer {value.json.access_token}" ADD EVENT Getworkspaces WITH SOURCE_TOPIC "devops/asana/workspace/list" WITH DESTINATION_TOPIC "devops/asana/workspace/list/response" WITH METHOD "GET" WITH ENDPOINT "/workspaces" WITH HEADERS ADD "Accept" "application/json" ADD "Authorization" "Bearer {value.json.access_token}" ADD EVENT Createtask WITH SOURCE_TOPIC "devops/asana/task/create" WITH DESTINATION_TOPIC "devops/asana/task/create/response" WITH METHOD "POST" WITH ENDPOINT "/tasks" WITH HEADERS ADD "Accept" "application/json" ADD "Content-Type" "application/json" ADD "Authorization" "Bearer {value.json.access_token}" WITH BODY "{payload}" ADD EVENT Updatetask WITH SOURCE_TOPIC "devops/asana/task/update" WITH DESTINATION_TOPIC "devops/asana/task/update/response" WITH METHOD "PUT" WITH ENDPOINT "/tasks/{value.json.task_gid}" WITH HEADERS ADD "Accept" "application/json" ADD "Content-Type" "application/json" ADD "Authorization" "Bearer {value.json.access_token}" WITH BODY "{payload}" ADD EVENT Gettask WITH SOURCE_TOPIC "devops/asana/task/get" WITH DESTINATION_TOPIC "devops/asana/task/get/response" WITH METHOD "GET" WITH ENDPOINT "/tasks/{value.json.task_gid}" WITH HEADERS ADD "Accept" "application/json" ADD "Authorization" "Bearer {value.json.access_token}" ADD EVENT Gettasksforproject WITH SOURCE_TOPIC "devops/asana/project/tasks" WITH DESTINATION_TOPIC "devops/asana/project/tasks/response" WITH METHOD "GET" WITH ENDPOINT "/projects/{value.json.project_gid}/tasks" WITH HEADERS ADD "Accept" "application/json" ADD "Authorization" "Bearer {value.json.access_token}" ADD EVENT Createwebhook WITH SOURCE_TOPIC "devops/asana/webhook/create" WITH DESTINATION_TOPIC "devops/asana/webhook/create/response" WITH METHOD "POST" WITH ENDPOINT "/webhooks" WITH HEADERS ADD "Accept" "application/json" ADD "Content-Type" "application/json" ADD "Authorization" "Bearer {value.json.access_token}" WITH BODY "{payload}" ``` ## Events | Event | Method | Endpoint | Request topic | Response topic | | - | - | - | - | - | | Getuser | GET | `/users/{user_gid}` | `devops/asana/user/get` | `devops/asana/user/get/response` | | Getworkspaces | GET | `/workspaces` | `devops/asana/workspace/list` | `devops/asana/workspace/list/response` | | Createtask | POST | `/tasks` | `devops/asana/task/create` | `devops/asana/task/create/response` | | Updatetask | PUT | `/tasks/{task_gid}` | `devops/asana/task/update` | `devops/asana/task/update/response` | | Gettask | GET | `/tasks/{task_gid}` | `devops/asana/task/get` | `devops/asana/task/get/response` | | Gettasksforproject | GET | `/projects/{project_gid}/tasks` | `devops/asana/project/tasks` | `devops/asana/project/tasks/response` | | Createwebhook | POST | `/webhooks` | `devops/asana/webhook/create` | `devops/asana/webhook/create/response` | ## Auth 1. Create a [personal access token](https://developers.asana.com/docs/personal-access-token) in Asana developer console, **or** obtain an OAuth 2.0 access token. 2. Set connector `BASE_ADDRESS` to `https://app.asana.com/api/1.0`. 3. Pass `access_token` on MQTT JSON payloads so the Bearer header resolves. For path params, also pass `user_gid` / `task_gid` / `project_gid`. 4. Store secrets in Hub / `GET SECRET "ASANA_ACCESS_TOKEN"` (aliases `ASANA_PAT`, `ASANA_TOKEN`). Never commit real credentials. 5. A successful check: `GET /users/me` → **HTTP 200** with `data.gid`. Invalid tokens → **HTTP 401**. ## Example payloads **getUser:** ```json theme={"theme":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} { "access_token": "YOUR_ASANA_ACCESS_TOKEN", "user_gid": "me" } ``` **createTask:** ```json theme={"theme":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} { "data": { "name": "Coreflux alarm — Zone A", "notes": "RMS exceeded limit", "projects": ["YOUR_PROJECT_GID"] } } ``` **updateTask:** ```json theme={"theme":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} { "task_gid": "YOUR_TASK_GID", "data": { "completed": true } } ``` **createWebhook:** ```json theme={"theme":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} { "data": { "resource": "YOUR_PROJECT_GID", "target": "https://YOUR_PUBLIC_HTTPS_CALLBACK/asana-webhook" } } ``` ## Server / inbound companion `POST /webhooks` is **client-mode** registration (MQTT → Asana). Asana then POSTs events to your `target` URL (handshake with `X-Hook-Secret`). Receiving those callbacks into MQTT requires a separate companion `ENABLE_SERVER` connector — not included in `AsanaRestApi.lot`. ## Deploy notes * Deploy `AsanaRestApi.lot` only (no `ADD METADATA`). * Hub catalog metadata lives in `hub-metadata.lot` (`WITH STATUS "beta"`, icon, doclink). * Docs: [https://docs.coreflux.org/latest/lot-language/routes/rest-api-apps/asana](https://docs.coreflux.org/latest/lot-language/routes/rest-api-apps/asana) ## Sources * [Asana REST API reference](https://developers.asana.com/reference/rest-api-reference) * [Personal access token](https://developers.asana.com/docs/personal-access-token) * [OAuth](https://developers.asana.com/docs/oauth) * [Webhooks guide](https://developers.asana.com/docs/webhooks-guide) * [Official OpenAPI](https://raw.githubusercontent.com/Asana/openapi/master/defs/asana_oas.yaml) # Auth0 Source: https://docs.coreflux.org/latest/lot-language/routes/rest-api-apps/auth0 Client-mode REST API connector for a curated Auth0 Management API v2 slice: list users and create a user Technical Preview # Auth0 Client-mode REST API connector for a curated Auth0 Management API v2 slice: list users and create a user. * Hub category: **Identity**. * Base: `https://tenant.us.auth0.com` ## Template reference Generated from the shipped broker template `auth0` (`-connectorCode AUTH0`). It lists every event the template defines. ### Auth Auth headers used by the template: `Authorization: Bearer {value.json.api_token}`. ### Template parameters | Placeholder | Type | Description | Required | | - | - | - | - | | `RouteName` | string | Name of the Auth0 connector | no | | `BaseUrl` | string | Auth0 base URL without trailing slash (e.g. [https://tenant.us.auth0.com](https://tenant.us.auth0.com)) | yes | `BASE_ADDRESS` default: `https://tenant.us.auth0.com`. ### Events | Event | Method | Endpoint | Request topic | Response topic | Payload fields | | - | - | - | - | - | - | | `Listusers` | GET | `/api/v2/users` | `identity/auth0/users/list` | `identity/auth0/users/list/response` | `api_token` | | `Createuser` | POST | `/api/v2/users` | `identity/auth0/users/create` | `identity/auth0/users/create/response` | `api_token`, `body` | | `Getuser` | GET | `/api/v2/users/{value.json.user_id}` | `identity/auth0/users/get` | `identity/auth0/users/get/response` | `api_token`, `user_id` | ### Deploy Get the template card (JSON with `template` and `metadata`) from the broker, then deploy the connector: ```bash theme={"theme":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} mosquitto_pub -t '$SYS/Coreflux/Command' -m "-connectorCode AUTH0" ``` The deployable connector from the REST API app, with `BASE_ADDRESS` already filled in: ```lot theme={"theme":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} DEFINE CONNECTOR Auth0RestApi WITH TYPE REST_API ADD REST_API_CONFIG WITH BASE_ADDRESS "https://tenant.us.auth0.com" WITH ENABLE_CLIENT "true" WITH USE_SSL "true" WITH REQUEST_TIMEOUT '60' WITH DEFAULT_CONTENT_TYPE "application/json" ADD EVENT Listusers WITH SOURCE_TOPIC "identity/auth0/users/list" WITH DESTINATION_TOPIC "identity/auth0/users/list/response" WITH METHOD "GET" WITH ENDPOINT "/api/v2/users" WITH HEADERS ADD "Accept" "application/json" ADD "Authorization" "Bearer {value.json.api_token}" ADD EVENT Createuser WITH SOURCE_TOPIC "identity/auth0/users/create" WITH DESTINATION_TOPIC "identity/auth0/users/create/response" WITH METHOD "POST" WITH ENDPOINT "/api/v2/users" WITH HEADERS ADD "Accept" "application/json" ADD "Content-Type" "application/json" ADD "Authorization" "Bearer {value.json.api_token}" WITH BODY "{value.json.body}" ADD EVENT Getuser WITH SOURCE_TOPIC "identity/auth0/users/get" WITH DESTINATION_TOPIC "identity/auth0/users/get/response" WITH METHOD "GET" WITH ENDPOINT "/api/v2/users/{value.json.user_id}" WITH HEADERS ADD "Accept" "application/json" ADD "Authorization" "Bearer {value.json.api_token}" ``` Deploy it with `-addConnector` (do not include `ADD METADATA`). ### Usage example Call `Listusers` by publishing a JSON payload to its request topic, and read the reply on the response topic: ```bash theme={"theme":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} mosquitto_sub -t 'identity/auth0/users/list/response' & mosquitto_pub -t 'identity/auth0/users/list' -m '{"api_token": ""}' ``` # Autodesk Platform Services Api Source: https://docs.coreflux.org/latest/lot-language/routes/rest-api-apps/autodesk-platform-services-api LoT connector for Autodesk Platform Services: browse hubs, projects and item versions, read model derivatives, add webhooks and query Fusion BOMs Technical Preview ## Overview Client-mode REST API connector for Autodesk Platform Services (APS): browse Data Management hubs, projects, folders, items and versions to show the latest design or drawing revision as a work instruction, read Model Derivative manifests and properties, subscribe webhooks to new versions, and query Fusion component properties and BOMs through the Manufacturing Data Model GraphQL API, with OAuth 2.0 tokens. This is one of the templates in the Autodesk Platform Services (Data Management, Manufacturing Data Model, Fusion) REST API app (`autodesk-platform-services`). Deploy it alongside the other connectors of that app when they depend on each other (for example a token connector plus an API connector). ## Auth OAuth 2.0 (APS Authentication v2, POST /authentication/v2/token). Client authentication: Authorization: Basic base64(client\_id:client\_secret) from payload field authorization. GetToken = client\_credentials (2-legged, scopes URL-encoded space-separated, e.g. data:read%20data:create); ExchangeCode = authorization\_code (3-legged, after one browser sign-in to /authentication/v2/authorize); RefreshToken = refresh\_token (rotates the refresh token). API events take authorization = 'Bearer \'. Data Management / Model Derivative / Webhooks accept 2-legged (BIM 360 Docs / ACC after the account admin adds the app as an integration) or 3-legged; Fusion Team / BIM 360 Team / A360 data and all Manufacturing Data Model calls require 3-legged. Auth headers used by the template: `Authorization: {value.json.authorization}`. ## Template parameters | Placeholder | Type | Description | Required | | - | - | - | - | | `RouteName` | string | Name of the Autodesk Platform Services Api connector | no | | `BaseUrl` | string | Autodesk Platform Services Api base URL without trailing slash (e.g. [https://developer.api.autodesk.com](https://developer.api.autodesk.com)) | yes | `BASE_ADDRESS` default: `https://developer.api.autodesk.com`. ## Events | Event | Method | Endpoint | Request topic | Response topic | Payload fields | | - | - | - | - | - | - | | `ListHubs` | GET | `/project/v1/hubs` | `mes/autodesk-platform-services/hubs/list` | `mes/autodesk-platform-services/hubs/list/response` | `authorization` | | `ListProjects` | GET | `/project/v1/hubs/{value.json.hub_id}/projects` | `mes/autodesk-platform-services/projects/list` | `mes/autodesk-platform-services/projects/list/response` | `authorization`, `hub_id` | | `GetProject` | GET | `/project/v1/hubs/{value.json.hub_id}/projects/{value.json.project_id}` | `mes/autodesk-platform-services/project/get` | `mes/autodesk-platform-services/project/get/response` | `authorization`, `hub_id`, `project_id` | | `ListTopFolders` | GET | `/project/v1/hubs/{value.json.hub_id}/projects/{value.json.project_id}/topFolders` | `mes/autodesk-platform-services/top-folders/list` | `mes/autodesk-platform-services/top-folders/list/response` | `authorization`, `hub_id`, `project_id` | | `ListFolderContents` | GET | `/data/v1/projects/{value.json.project_id}/folders/{value.json.folder_id}/contents` | `mes/autodesk-platform-services/folder-contents/list` | `mes/autodesk-platform-services/folder-contents/list/response` | `authorization`, `folder_id`, `project_id` | | `SearchFolder` | GET | `/data/v1/projects/{value.json.project_id}/folders/{value.json.folder_id}/search?{value.json.filter}` | `mes/autodesk-platform-services/folder/search` | `mes/autodesk-platform-services/folder/search/response` | `authorization`, `filter`, `folder_id`, `project_id` | | `GetItem` | GET | `/data/v1/projects/{value.json.project_id}/items/{value.json.item_id}` | `mes/autodesk-platform-services/item/get` | `mes/autodesk-platform-services/item/get/response` | `authorization`, `item_id`, `project_id` | | `GetItemTip` | GET | `/data/v1/projects/{value.json.project_id}/items/{value.json.item_id}/tip` | `mes/autodesk-platform-services/item-tip/get` | `mes/autodesk-platform-services/item-tip/get/response` | `authorization`, `item_id`, `project_id` | | `ListItemVersions` | GET | `/data/v1/projects/{value.json.project_id}/items/{value.json.item_id}/versions` | `mes/autodesk-platform-services/item-versions/list` | `mes/autodesk-platform-services/item-versions/list/response` | `authorization`, `item_id`, `project_id` | | `GetVersion` | GET | `/data/v1/projects/{value.json.project_id}/versions/{value.json.version_id}` | `mes/autodesk-platform-services/version/get` | `mes/autodesk-platform-services/version/get/response` | `authorization`, `project_id`, `version_id` | | `ListVersionDownloads` | GET | `/data/v1/projects/{value.json.project_id}/versions/{value.json.version_id}/downloads` | `mes/autodesk-platform-services/version-downloads/list` | `mes/autodesk-platform-services/version-downloads/list/response` | `authorization`, `project_id`, `version_id` | | `GetManifest` | GET | `/modelderivative/v2/designdata/{value.json.urn}/manifest` | `mes/autodesk-platform-services/manifest/get` | `mes/autodesk-platform-services/manifest/get/response` | `authorization`, `region`, `urn` | | `ListModelViews` | GET | `/modelderivative/v2/designdata/{value.json.urn}/metadata` | `mes/autodesk-platform-services/model-views/list` | `mes/autodesk-platform-services/model-views/list/response` | `authorization`, `region`, `urn` | | `GetModelProperties` | GET | `/modelderivative/v2/designdata/{value.json.urn}/metadata/{value.json.guid}/properties` | `mes/autodesk-platform-services/model-properties/get` | `mes/autodesk-platform-services/model-properties/get/response` | `authorization`, `guid`, `region`, `urn` | | `CreateWebhook` | POST | `/webhooks/v1/systems/{value.json.system}/events/{value.json.event}/hooks?region={value.json.region}` | `mes/autodesk-platform-services/webhook/create` | `mes/autodesk-platform-services/webhook/create/response` | `authorization`, `event`, `region`, `request`, `system` | | `ListWebhooks` | GET | `/webhooks/v1/hooks?region={value.json.region}` | `mes/autodesk-platform-services/webhooks/list` | `mes/autodesk-platform-services/webhooks/list/response` | `authorization`, `region` | | `ListAppWebhooks` | GET | `/webhooks/v1/app/hooks?region={value.json.region}` | `mes/autodesk-platform-services/app-webhooks/list` | `mes/autodesk-platform-services/app-webhooks/list/response` | `authorization`, `region` | | `ListEventWebhooks` | GET | `/webhooks/v1/systems/{value.json.system}/events/{value.json.event}/hooks?region={value.json.region}` | `mes/autodesk-platform-services/event-webhooks/list` | `mes/autodesk-platform-services/event-webhooks/list/response` | `authorization`, `event`, `region`, `system` | | `GetWebhook` | GET | `/webhooks/v1/systems/{value.json.system}/events/{value.json.event}/hooks/{value.json.hook_id}?region={value.json.region}` | `mes/autodesk-platform-services/webhook/get` | `mes/autodesk-platform-services/webhook/get/response` | `authorization`, `event`, `hook_id`, `region`, `system` | | `MdmQuery` | POST | `/mfg/v3/graphql/public` | `mes/autodesk-platform-services/mdm/query` | `mes/autodesk-platform-services/mdm/query/response` | `authorization`, `request` | | `MdmMutation` | POST | `/mfg/v3/graphql/public` | `mes/autodesk-platform-services/mdm/mutate` | `mes/autodesk-platform-services/mdm/mutate/response` | `authorization`, `request` | ## Deploy Get the template card (JSON with `template` and `metadata`) from the broker, then deploy the connector: ```bash theme={"theme":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} mosquitto_pub -t '$SYS/Coreflux/Command' -m "-connectorCode AUTODESK_PLATFORM_SERVICES" ``` The deployable connector from the REST API app, with `BASE_ADDRESS` already filled in: ```lot theme={"theme":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} DEFINE CONNECTOR AutodeskPlatformServicesApi WITH TYPE REST_API ADD REST_API_CONFIG WITH BASE_ADDRESS "https://developer.api.autodesk.com" WITH ENABLE_CLIENT "true" WITH USE_SSL "true" WITH REQUEST_TIMEOUT '60' WITH DEFAULT_CONTENT_TYPE "application/json" ADD EVENT ListHubs WITH SOURCE_TOPIC "mes/autodesk-platform-services/hubs/list" WITH DESTINATION_TOPIC "mes/autodesk-platform-services/hubs/list/response" WITH METHOD "GET" WITH ENDPOINT "/project/v1/hubs" WITH HEADERS ADD "Accept" "application/json" ADD "Authorization" "{value.json.authorization}" ADD EVENT ListProjects WITH SOURCE_TOPIC "mes/autodesk-platform-services/projects/list" WITH DESTINATION_TOPIC "mes/autodesk-platform-services/projects/list/response" WITH METHOD "GET" WITH ENDPOINT "/project/v1/hubs/{value.json.hub_id}/projects" WITH HEADERS ADD "Accept" "application/json" ADD "Authorization" "{value.json.authorization}" ADD EVENT GetProject WITH SOURCE_TOPIC "mes/autodesk-platform-services/project/get" WITH DESTINATION_TOPIC "mes/autodesk-platform-services/project/get/response" WITH METHOD "GET" WITH ENDPOINT "/project/v1/hubs/{value.json.hub_id}/projects/{value.json.project_id}" WITH HEADERS ADD "Accept" "application/json" ADD "Authorization" "{value.json.authorization}" ADD EVENT ListTopFolders WITH SOURCE_TOPIC "mes/autodesk-platform-services/top-folders/list" WITH DESTINATION_TOPIC "mes/autodesk-platform-services/top-folders/list/response" WITH METHOD "GET" WITH ENDPOINT "/project/v1/hubs/{value.json.hub_id}/projects/{value.json.project_id}/topFolders" WITH HEADERS ADD "Accept" "application/json" ADD "Authorization" "{value.json.authorization}" ADD EVENT ListFolderContents WITH SOURCE_TOPIC "mes/autodesk-platform-services/folder-contents/list" WITH DESTINATION_TOPIC "mes/autodesk-platform-services/folder-contents/list/response" WITH METHOD "GET" WITH ENDPOINT "/data/v1/projects/{value.json.project_id}/folders/{value.json.folder_id}/contents" WITH HEADERS ADD "Accept" "application/json" ADD "Authorization" "{value.json.authorization}" ADD EVENT SearchFolder WITH SOURCE_TOPIC "mes/autodesk-platform-services/folder/search" WITH DESTINATION_TOPIC "mes/autodesk-platform-services/folder/search/response" WITH METHOD "GET" WITH ENDPOINT "/data/v1/projects/{value.json.project_id}/folders/{value.json.folder_id}/search?{value.json.filter}" WITH HEADERS ADD "Accept" "application/json" ADD "Authorization" "{value.json.authorization}" ADD EVENT GetItem WITH SOURCE_TOPIC "mes/autodesk-platform-services/item/get" WITH DESTINATION_TOPIC "mes/autodesk-platform-services/item/get/response" WITH METHOD "GET" WITH ENDPOINT "/data/v1/projects/{value.json.project_id}/items/{value.json.item_id}" WITH HEADERS ADD "Accept" "application/json" ADD "Authorization" "{value.json.authorization}" ADD EVENT GetItemTip WITH SOURCE_TOPIC "mes/autodesk-platform-services/item-tip/get" WITH DESTINATION_TOPIC "mes/autodesk-platform-services/item-tip/get/response" WITH METHOD "GET" WITH ENDPOINT "/data/v1/projects/{value.json.project_id}/items/{value.json.item_id}/tip" WITH HEADERS ADD "Accept" "application/json" ADD "Authorization" "{value.json.authorization}" ADD EVENT ListItemVersions WITH SOURCE_TOPIC "mes/autodesk-platform-services/item-versions/list" WITH DESTINATION_TOPIC "mes/autodesk-platform-services/item-versions/list/response" WITH METHOD "GET" WITH ENDPOINT "/data/v1/projects/{value.json.project_id}/items/{value.json.item_id}/versions" WITH HEADERS ADD "Accept" "application/json" ADD "Authorization" "{value.json.authorization}" ADD EVENT GetVersion WITH SOURCE_TOPIC "mes/autodesk-platform-services/version/get" WITH DESTINATION_TOPIC "mes/autodesk-platform-services/version/get/response" WITH METHOD "GET" WITH ENDPOINT "/data/v1/projects/{value.json.project_id}/versions/{value.json.version_id}" WITH HEADERS ADD "Accept" "application/json" ADD "Authorization" "{value.json.authorization}" ADD EVENT ListVersionDownloads WITH SOURCE_TOPIC "mes/autodesk-platform-services/version-downloads/list" WITH DESTINATION_TOPIC "mes/autodesk-platform-services/version-downloads/list/response" WITH METHOD "GET" WITH ENDPOINT "/data/v1/projects/{value.json.project_id}/versions/{value.json.version_id}/downloads" WITH HEADERS ADD "Accept" "application/json" ADD "Authorization" "{value.json.authorization}" ADD EVENT GetManifest WITH SOURCE_TOPIC "mes/autodesk-platform-services/manifest/get" WITH DESTINATION_TOPIC "mes/autodesk-platform-services/manifest/get/response" WITH METHOD "GET" WITH ENDPOINT "/modelderivative/v2/designdata/{value.json.urn}/manifest" WITH HEADERS ADD "Accept" "application/json" ADD "Authorization" "{value.json.authorization}" ADD "region" "{value.json.region}" ADD EVENT ListModelViews WITH SOURCE_TOPIC "mes/autodesk-platform-services/model-views/list" WITH DESTINATION_TOPIC "mes/autodesk-platform-services/model-views/list/response" WITH METHOD "GET" WITH ENDPOINT "/modelderivative/v2/designdata/{value.json.urn}/metadata" WITH HEADERS ADD "Accept" "application/json" ADD "Authorization" "{value.json.authorization}" ADD "region" "{value.json.region}" ADD EVENT GetModelProperties WITH SOURCE_TOPIC "mes/autodesk-platform-services/model-properties/get" WITH DESTINATION_TOPIC "mes/autodesk-platform-services/model-properties/get/response" WITH METHOD "GET" WITH ENDPOINT "/modelderivative/v2/designdata/{value.json.urn}/metadata/{value.json.guid}/properties" WITH HEADERS ADD "Accept" "application/json" ADD "Authorization" "{value.json.authorization}" ADD "region" "{value.json.region}" ADD EVENT CreateWebhook WITH SOURCE_TOPIC "mes/autodesk-platform-services/webhook/create" WITH DESTINATION_TOPIC "mes/autodesk-platform-services/webhook/create/response" WITH METHOD "POST" WITH ENDPOINT "/webhooks/v1/systems/{value.json.system}/events/{value.json.event}/hooks?region={value.json.region}" WITH HEADERS ADD "Accept" "application/json" ADD "Authorization" "{value.json.authorization}" ADD "Content-Type" "application/json" WITH BODY "{value.json.request}" ADD EVENT ListWebhooks WITH SOURCE_TOPIC "mes/autodesk-platform-services/webhooks/list" WITH DESTINATION_TOPIC "mes/autodesk-platform-services/webhooks/list/response" WITH METHOD "GET" WITH ENDPOINT "/webhooks/v1/hooks?region={value.json.region}" WITH HEADERS ADD "Accept" "application/json" ADD "Authorization" "{value.json.authorization}" ADD EVENT ListAppWebhooks WITH SOURCE_TOPIC "mes/autodesk-platform-services/app-webhooks/list" WITH DESTINATION_TOPIC "mes/autodesk-platform-services/app-webhooks/list/response" WITH METHOD "GET" WITH ENDPOINT "/webhooks/v1/app/hooks?region={value.json.region}" WITH HEADERS ADD "Accept" "application/json" ADD "Authorization" "{value.json.authorization}" ADD EVENT ListEventWebhooks WITH SOURCE_TOPIC "mes/autodesk-platform-services/event-webhooks/list" WITH DESTINATION_TOPIC "mes/autodesk-platform-services/event-webhooks/list/response" WITH METHOD "GET" WITH ENDPOINT "/webhooks/v1/systems/{value.json.system}/events/{value.json.event}/hooks?region={value.json.region}" WITH HEADERS ADD "Accept" "application/json" ADD "Authorization" "{value.json.authorization}" ADD EVENT GetWebhook WITH SOURCE_TOPIC "mes/autodesk-platform-services/webhook/get" WITH DESTINATION_TOPIC "mes/autodesk-platform-services/webhook/get/response" WITH METHOD "GET" WITH ENDPOINT "/webhooks/v1/systems/{value.json.system}/events/{value.json.event}/hooks/{value.json.hook_id}?region={value.json.region}" WITH HEADERS ADD "Accept" "application/json" ADD "Authorization" "{value.json.authorization}" ADD EVENT MdmQuery WITH SOURCE_TOPIC "mes/autodesk-platform-services/mdm/query" WITH DESTINATION_TOPIC "mes/autodesk-platform-services/mdm/query/response" WITH METHOD "POST" WITH ENDPOINT "/mfg/v3/graphql/public" WITH HEADERS ADD "Accept" "application/json" ADD "Authorization" "{value.json.authorization}" ADD "Content-Type" "application/json" WITH BODY "{value.json.request}" ADD EVENT MdmMutation WITH SOURCE_TOPIC "mes/autodesk-platform-services/mdm/mutate" WITH DESTINATION_TOPIC "mes/autodesk-platform-services/mdm/mutate/response" WITH METHOD "POST" WITH ENDPOINT "/mfg/v3/graphql/public" WITH HEADERS ADD "Accept" "application/json" ADD "Authorization" "{value.json.authorization}" ADD "Content-Type" "application/json" WITH BODY "{value.json.request}" ``` Deploy it with `-addConnector` (do not include `ADD METADATA`). ## Usage example Call `ListHubs` by publishing a JSON payload to its request topic, and read the reply on the response topic: ```bash theme={"theme":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} mosquitto_sub -t 'mes/autodesk-platform-services/hubs/list/response' & mosquitto_pub -t 'mes/autodesk-platform-services/hubs/list' -m '{"authorization": ""}' ``` ## Related * [REST API Connectors](/latest/lot-language/routes/rest-api-routes) * [REST API Apps catalog](/latest/lot-language/routes/rest-api-apps) # AVEVA CONNECT data services Source: https://docs.coreflux.org/latest/lot-language/routes/rest-api-apps/aveva-connect-data-services LoT connector for AVEVA CONNECT data services: ingest OMF types, containers and data, list SDS streams and read or write stream data from MQTT Technical Preview ## Overview A client-mode connector for a **curated** slice of [AVEVA CONNECT data services](https://docs.aveva.com/bundle/connect-data-services-developer/page/developer-guide/sequential-data-store-dev/sds-quick-start.html) (formerly AVEVA Data Hub / OSIsoft Cloud Services) under your regional host `https://{region}.datahub.connect.aveva.com/api/v1/Tenants/{tenant}/Namespaces/{ns}`: | Catalog connector event | Entity / path | Notes | | - | - | - | | | `GET /Streams` | List SDS streams in the namespace | | POST omf (type) | `POST /omf` + `MessageType: type` | OMF type → SDS type | | POST omf (container) | `POST /omf` + `MessageType: container` | OMF container → SDS stream | | POST omf (data) | `POST /omf` + `MessageType: data` | High-throughput async ingest (expects **202**) | | GET Streams/\{id}/Data | `GET /Streams/{streamId}/Data?startIndex=` | Also `endIndex` / `count` | | PUT Streams/\{id}/Data | `PUT /Streams/{streamId}/Data` | Update/upsert SDS values | `BASE_ADDRESS` includes tenant and namespace (no trailing slash; endpoints start with `/`). Topics use `historian/aveva-connect-data-services//`. Default stub region is `uswe` (US West). See the base guide: [REST API Connectors](/latest/lot-language/routes/rest-api-routes). > [!NOTE] > > **Tenant + OAuth required.** There is no public CONNECT sandbox. Replace `YOUR_TENANT` / `YOUR_NAMESPACE`, pick your region, and supply an OAuth 2.0 **client credentials** access token. Prefer Hub secrets. > [!NOTE] > > **OMF connection.** For `POST /omf`, map the client-credentials client to an **OMF connection** that targets the namespace (CONNECT portal: Data Management → Connections). Otherwise OMF ingress is unauthorized or not routed. > [!NOTE] > > Prefer Hub secrets for tokens / client secrets. ## Basic Syntax ```lot wrap theme={"theme":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} # Coreflux REST_API template — AVEVA CONNECT data services # Auth: OAuth 2.0 client credentials → Bearer {value.json.api_token} # BASE_ADDRESS: https://uswe.datahub.connect.aveva.com/api/v1/Tenants/YOUR_TENANT/Namespaces/YOUR_NAMESPACE DEFINE CONNECTOR AvevaConnectDataServicesRestApi WITH TYPE REST_API ADD REST_API_CONFIG WITH BASE_ADDRESS "https://uswe.datahub.connect.aveva.com/api/v1/Tenants/YOUR_TENANT/Namespaces/YOUR_NAMESPACE" WITH ENABLE_CLIENT "true" WITH USE_SSL "true" WITH REQUEST_TIMEOUT '60' WITH DEFAULT_CONTENT_TYPE "application/json" ADD EVENT ListStreams WITH SOURCE_TOPIC "historian/aveva-connect-data-services/streams/list" WITH DESTINATION_TOPIC "historian/aveva-connect-data-services/streams/list/response" WITH METHOD "GET" WITH ENDPOINT "/Streams" WITH HEADERS ADD "Accept" "application/json" ADD "Authorization" "Bearer {value.json.api_token}" ADD EVENT PostOmfType WITH SOURCE_TOPIC "historian/aveva-connect-data-services/omf/type" WITH DESTINATION_TOPIC "historian/aveva-connect-data-services/omf/type/response" WITH METHOD "POST" WITH ENDPOINT "/omf" WITH HEADERS ADD "Accept" "application/json" ADD "Content-Type" "application/json" ADD "Authorization" "Bearer {value.json.api_token}" ADD "MessageType" "type" ADD "MessageFormat" "json" ADD "OmfVersion" "1.2" WITH BODY "{payload}" ADD EVENT PostOmfContainer WITH SOURCE_TOPIC "historian/aveva-connect-data-services/omf/container" WITH DESTINATION_TOPIC "historian/aveva-connect-data-services/omf/container/response" WITH METHOD "POST" WITH ENDPOINT "/omf" WITH HEADERS ADD "Accept" "application/json" ADD "Content-Type" "application/json" ADD "Authorization" "Bearer {value.json.api_token}" ADD "MessageType" "container" ADD "MessageFormat" "json" ADD "OmfVersion" "1.2" WITH BODY "{payload}" ADD EVENT PostOmfData WITH SOURCE_TOPIC "historian/aveva-connect-data-services/omf/data" WITH DESTINATION_TOPIC "historian/aveva-connect-data-services/omf/data/response" WITH METHOD "POST" WITH ENDPOINT "/omf" WITH HEADERS ADD "Accept" "application/json" ADD "Content-Type" "application/json" ADD "Authorization" "Bearer {value.json.api_token}" ADD "MessageType" "data" ADD "MessageFormat" "json" ADD "OmfVersion" "1.2" WITH BODY "{payload}" ADD EVENT GetStreamData WITH SOURCE_TOPIC "historian/aveva-connect-data-services/streams/data/get" WITH DESTINATION_TOPIC "historian/aveva-connect-data-services/streams/data/get/response" WITH METHOD "GET" WITH ENDPOINT "/Streams/{value.json.streamId}/Data?startIndex={value.json.startIndex}&endIndex={value.json.endIndex}&count={value.json.count}" WITH HEADERS ADD "Accept" "application/json" ADD "Authorization" "Bearer {value.json.api_token}" ADD EVENT PutStreamData WITH SOURCE_TOPIC "historian/aveva-connect-data-services/streams/data/put" WITH DESTINATION_TOPIC "historian/aveva-connect-data-services/streams/data/put/response" WITH METHOD "PUT" WITH ENDPOINT "/Streams/{value.json.streamId}/Data" WITH HEADERS ADD "Accept" "application/json" ADD "Content-Type" "application/json" ADD "Authorization" "Bearer {value.json.api_token}" WITH BODY "{payload}" ``` ## Events | Event | Method | Endpoint | Request topic | Response topic | | - | - | - | - | - | | ListStreams | GET | `/Streams` | `historian/aveva-connect-data-services/streams/list` | `.../response` | | PostOmfType | POST | `/omf` (`MessageType: type`) | `.../omf/type` | `.../response` | | PostOmfContainer | POST | `/omf` (`MessageType: container`) | `.../omf/container` | `.../response` | | PostOmfData | POST | `/omf` (`MessageType: data`) | `.../omf/data` | `.../response` | | GetStreamData | GET | `/Streams/{streamId}/Data?startIndex=&endIndex=&count=` | `.../streams/data/get` | `.../response` | | PutStreamData | PUT | `/Streams/{streamId}/Data` | `.../streams/data/put` | `.../response` | ## Auth 1. Create a **Client-Credentials** client in CONNECT (Security → Clients) with roles that can read/write SDS streams (and OMF if ingesting). 2. For OMF, create an **OMF connection** and map that client to the target namespace(s). 3. Exchange credentials at the regional token endpoint, e.g.\ `POST https://uswe.datahub.connect.aveva.com/identity/connect/token`\ with `grant_type=client_credentials`, `client_id`, `client_secret`. 4. Discovery: `https://uswe.datahub.connect.aveva.com/identity/.well-known/openid-configuration`. 5. Pass the access token on each MQTT payload as `api_token` → header `Authorization: Bearer {value.json.api_token}`. 6. Prefer Hub secrets. Never commit `client_secret` or access tokens. ## Example payloads **List streams:** ```json theme={"theme":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} { "api_token": "YOUR_OAUTH_ACCESS_TOKEN" } ``` **OMF data message (body is OMF JSON; MessageType is set by the event headers):** ```json theme={"theme":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} [ { "containerid": "SimpleStream", "values": [ { "Time": "2026-10-04T10:00:00Z", "Measurement": 42.5 } ] } ] ``` Publish that array as the MQTT payload (and include `api_token` in a wrapper only if your upstream strips non-body keys before `{payload}` — otherwise publish the OMF body alone and pass the token via a Hub secret injection pattern your broker supports). Practical pattern: put `api_token` alongside path fields when the connector uses `{value.json.*}` for path/query, and keep `{payload}` as the full MQTT JSON for PUT/POST when the API accepts extra fields being ignored — for OMF, prefer a clean OMF array body. **Get stream data:** ```json theme={"theme":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} { "api_token": "YOUR_OAUTH_ACCESS_TOKEN", "streamId": "SimpleStream", "startIndex": "2026-10-04T00:00:00Z", "endIndex": "2026-10-04T23:59:59Z", "count": "1000" } ``` **Put stream data:** ```json theme={"theme":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} [ { "Time": "2026-10-04T10:00:00Z", "State": 0, "Measurement": 123.45 } ] ``` Use `streamId` in the MQTT JSON for the path placeholder; shape the body as an SDS value array. ## Configuration checklist 1. Set `BASE_ADDRESS` region + tenant + namespace. 2. Store OAuth client id/secret (or a short-lived access token) in Hub secrets. 3. Map the client to an OMF connection before using `PostOmf*`. 4. Deploy the `.lot` with `-addConnector` (no `ADD METADATA` in the deployable file). 5. Smoke: publish to `historian/aveva-connect-data-services/streams/list` → expect HTTP 200 on the response topic. ## Sources * [OMF Ingress](https://docs.aveva.com/bundle/connect-data-services-developer/page/api-reference/data-ingress/omf-ingress/omf-ingress-omf.html) * [SDS Quick Start](https://docs.aveva.com/bundle/connect-data-services-developer/page/developer-guide/sequential-data-store-dev/sds-quick-start.html) * [SDS Streams API](https://docs.aveva.com/bundle/connect-data-services-developer/page/api-reference/sequential-data-store/sds-streams.html) * [CONNECT developer docs (GitHub)](https://github.com/aveva/CONNECT-data-services-developer-docs) * [OMF samples](https://github.com/AVEVA/AVEVA-Samples-OMF) ## Related * [REST API Connectors](/latest/lot-language/routes/rest-api-routes) * [AVEVA PI Web API](/latest/lot-language/routes/rest-api-apps/aveva-pi-web-api) (on-prem sister historian package) * [REST API Apps catalog](/latest/lot-language/routes/rest-api-apps) # Aveva Mes Api Source: https://docs.coreflux.org/latest/lot-language/routes/rest-api-apps/aveva-mes-api Connect Coreflux LoT to AVEVA MES Web API V3 to read job queues, work orders and OEE and post production counts, downtime and job state changes Technical Preview ## Overview Client-mode REST API connector for AVEVA MES (formerly Wonderware MES): read the job queue, work orders, OEE and utilization, and post production counts, consumption, downtime events and job state changes from MQTT through the MES Web API V3. This is one of the templates in the AVEVA MES (formerly Wonderware MES) REST API app (`aveva-mes`). Deploy it alongside the other connectors of that app when they depend on each other (for example a token connector plus an API connector). ## Auth MES security mode OS User or OS Group + AVEVA Identity Manager. Register a service client with RegisterAIMServiceClient.ps1 (InstallFiles/CD-MES/AIMRegistration), POST https\://\/identitymanager/connect/token grant\_type=client\_credentials (form) -> access\_token (24 h default), then Authorization: Bearer \ from the MQTT payload field authorization. Service-to-service calls run as the MES Default Background User (system attribute 199). Auth headers used by the template: `Authorization: {value.json.authorization}`. ## Template parameters | Placeholder | Type | Description | Required | | - | - | - | - | | `RouteName` | string | Name of the Aveva Mes Api connector | no | | `BaseUrl` | string | Aveva Mes Api base URL without trailing slash (e.g. https\://YOUR\_MES\_SERVER) | yes | `BASE_ADDRESS` default: `https://YOUR_MES_SERVER`. ## Events | Event | Method | Endpoint | Request topic | Response topic | Payload fields | | - | - | - | - | - | - | | `GetOpenApiSpec` | GET | `/mesmw/swagger/docs/v3` | `mes/aveva-mes/openapi/get` | `mes/aveva-mes/openapi/get/response` | `authorization` | | `ListEntities` | GET | `/mesmw/api/v3/Entity?{value.json.query}` | `mes/aveva-mes/entity/list` | `mes/aveva-mes/entity/list/response` | `authorization`, `query` | | `GetEntity` | GET | `/mesmw/api/v3/Entity/key?entId={value.json.ent_id}` | `mes/aveva-mes/entity/get` | `mes/aveva-mes/entity/get/response` | `authorization`, `ent_id` | | `FindEntityId` | GET | `/mesmw/api/v3/Entity/name?entName={value.json.ent_name}&siteName={value.json.site_name}` | `mes/aveva-mes/entity-id/get` | `mes/aveva-mes/entity-id/get/response` | `authorization`, `ent_name`, `site_name` | | `ListLines` | GET | `/mesmw/api/v3/line` | `mes/aveva-mes/line/list` | `mes/aveva-mes/line/list/response` | `authorization` | | `ListLineEntities` | GET | `/mesmw/api/v3/LineEntity?{value.json.query}` | `mes/aveva-mes/line-entity/list` | `mes/aveva-mes/line-entity/list/response` | `authorization`, `query` | | `GetJobQueue` | GET | `/mesmw/api/v3/JobQueue?{value.json.query}` | `mes/aveva-mes/job-queue/get` | `mes/aveva-mes/job-queue/get/response` | `authorization`, `query` | | `ListWorkOrders` | GET | `/mesmw/api/v3/WO/filter?{value.json.query}` | `mes/aveva-mes/work-order/list` | `mes/aveva-mes/work-order/list/response` | `authorization`, `query` | | `GetWorkOrder` | GET | `/mesmw/api/v3/WO/key?woId={value.json.wo_id}` | `mes/aveva-mes/work-order/get` | `mes/aveva-mes/work-order/get/response` | `authorization`, `wo_id` | | `ListWorkOrderJobs` | GET | `/mesmw/api/v3/Jobs?woId={value.json.wo_id}` | `mes/aveva-mes/work-order-jobs/list` | `mes/aveva-mes/work-order-jobs/list/response` | `authorization`, `wo_id` | | `GetJob` | GET | `/mesmw/api/v3/Jobs/key?woId={value.json.wo_id}&operId={value.json.oper_id}&seqNo={value.json.seq_no}` | `mes/aveva-mes/job/get` | `mes/aveva-mes/job/get/response` | `authorization`, `oper_id`, `seq_no`, `wo_id` | | `GetJobExec` | GET | `/mesmw/api/v3/JobExec/key?{value.json.query}` | `mes/aveva-mes/job-exec/get` | `mes/aveva-mes/job-exec/get/response` | `authorization`, `query` | | `GetOeeExec` | GET | `/mesmw/api/v3/OeeExec/key?entId={value.json.ent_id}` | `mes/aveva-mes/oee-exec/get` | `mes/aveva-mes/oee-exec/get/response` | `authorization`, `ent_id` | | `GetEntityCountsAndDuration` | GET | `/mesmw/api/v3/countsAndDuration/entity?{value.json.query}` | `mes/aveva-mes/counts-duration-entity/get` | `mes/aveva-mes/counts-duration-entity/get/response` | `authorization`, `query` | | `GetLineCountsAndDuration` | GET | `/mesmw/api/v3/countsAndDuration/line?{value.json.query}` | `mes/aveva-mes/counts-duration-line/get` | `mes/aveva-mes/counts-duration-line/get/response` | `authorization`, `query` | | `ListUtilStates` | GET | `/mesmw/api/v3/UtilState` | `mes/aveva-mes/util-state/list` | `mes/aveva-mes/util-state/list/response` | `authorization` | | `ListUtilReasons` | GET | `/mesmw/api/v3/utilreason/flat` | `mes/aveva-mes/util-reason/list` | `mes/aveva-mes/util-reason/list/response` | `authorization` | | `GetAllowableReasons` | GET | `/mesmw/api/v3/Util/AllowableReasons?entID={value.json.ent_id}` | `mes/aveva-mes/allowable-reason/list` | `mes/aveva-mes/allowable-reason/list/response` | `authorization`, `ent_id` | | `ListProduction` | GET | `/mesmw/api/v3/Production/filter?{value.json.query}` | `mes/aveva-mes/production/list` | `mes/aveva-mes/production/list/response` | `authorization`, `query` | | `ListConsumption` | GET | `/mesmw/api/v3/Consumption/filter?{value.json.query}` | `mes/aveva-mes/consumption/list` | `mes/aveva-mes/consumption/list/response` | `authorization`, `query` | | `ListItems` | GET | `/mesmw/api/v3/Item/filter?{value.json.query}` | `mes/aveva-mes/item/list` | `mes/aveva-mes/item/list/response` | `authorization`, `query` | | `ListInventory` | GET | `/mesmw/api/v3/ItemInventory/filter?{value.json.query}` | `mes/aveva-mes/inventory/list` | `mes/aveva-mes/inventory/list/response` | `authorization`, `query` | | `ListLots` | GET | `/mesmw/api/v3/Lot/filter?{value.json.query}` | `mes/aveva-mes/lot/list` | `mes/aveva-mes/lot/list/response` | `authorization`, `query` | | `ListSamples` | GET | `/mesmw/api/v3/Sample/filter?{value.json.query}` | `mes/aveva-mes/sample/list` | `mes/aveva-mes/sample/list/response` | `authorization`, `query` | | `AddProductionByJob` | POST | `/mesmw/api/v3/Production/ByJob` | `mes/aveva-mes/production/post` | `mes/aveva-mes/production/post/response` | `authorization`, `body` | | `AddProductionPostExec` | POST | `/mesmw/api/v3/Production/PostExec` | `mes/aveva-mes/production-post-exec/post` | `mes/aveva-mes/production-post-exec/post/response` | `authorization`, `body` | | `AddConsumptionByJob` | POST | `/mesmw/api/v3/Consumption/ByJob` | `mes/aveva-mes/consumption/post` | `mes/aveva-mes/consumption/post/response` | `authorization`, `body` | | `AddUtilizationEvent` | POST | `/mesmw/api/v3/UtilHistory/AddEvent` | `mes/aveva-mes/util-event/post` | `mes/aveva-mes/util-event/post/response` | `authorization`, `body` | | `AddCurrentUtilizationEvent` | POST | `/mesmw/api/v3/UtilHistory` | `mes/aveva-mes/util-history/post` | `mes/aveva-mes/util-history/post/response` | `authorization`, `body` | | `StartJob` | PUT | `/mesmw/api/v3/Jobs/Start` | `mes/aveva-mes/job/start` | `mes/aveva-mes/job/start/response` | `authorization`, `body` | | `PauseJob` | PUT | `/mesmw/api/v3/Jobs/Pause` | `mes/aveva-mes/job/pause` | `mes/aveva-mes/job/pause/response` | `authorization`, `body` | | `EndJob` | PUT | `/mesmw/api/v3/Jobs/End` | `mes/aveva-mes/job/end` | `mes/aveva-mes/job/end/response` | `authorization`, `body` | | `ChangeJobStates` | PUT | `/mesmw/api/v3/Jobs/ChangeJobStates` | `mes/aveva-mes/job-states/put` | `mes/aveva-mes/job-states/put/response` | `authorization`, `body` | | `ReleaseWorkOrder` | PUT | `/mesmw/api/v3/WO/Release` | `mes/aveva-mes/work-order/release` | `mes/aveva-mes/work-order/release/response` | `authorization`, `body` | | `RecordSampleData` | PUT | `/mesmw/api/v3/Sample/RecordData` | `mes/aveva-mes/sample-data/put` | `mes/aveva-mes/sample-data/put/response` | `authorization`, `body` | | `MiddlewareAccessXml` | POST | `/mesmw/api/v3/MiddlewareAccess/xml` | `mes/aveva-mes/middleware-xml/post` | `mes/aveva-mes/middleware-xml/post/response` | `authorization`, `body` | ## Deploy Get the template card (JSON with `template` and `metadata`) from the broker, then deploy the connector: ```bash theme={"theme":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} mosquitto_pub -t '$SYS/Coreflux/Command' -m "-connectorCode AVEVA_MES" ``` The deployable connector from the REST API app, with `BASE_ADDRESS` already filled in: ```lot theme={"theme":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} DEFINE CONNECTOR AvevaMesApi WITH TYPE REST_API ADD REST_API_CONFIG WITH BASE_ADDRESS "https://YOUR_MES_SERVER" WITH ENABLE_CLIENT "true" WITH USE_SSL "true" WITH REQUEST_TIMEOUT '60' WITH DEFAULT_CONTENT_TYPE "application/json" ADD EVENT GetOpenApiSpec WITH SOURCE_TOPIC "mes/aveva-mes/openapi/get" WITH DESTINATION_TOPIC "mes/aveva-mes/openapi/get/response" WITH METHOD "GET" WITH ENDPOINT "/mesmw/swagger/docs/v3" WITH HEADERS ADD "Accept" "application/json" ADD "Authorization" "{value.json.authorization}" ADD EVENT ListEntities WITH SOURCE_TOPIC "mes/aveva-mes/entity/list" WITH DESTINATION_TOPIC "mes/aveva-mes/entity/list/response" WITH METHOD "GET" WITH ENDPOINT "/mesmw/api/v3/Entity?{value.json.query}" WITH HEADERS ADD "Accept" "application/json" ADD "Authorization" "{value.json.authorization}" ADD EVENT GetEntity WITH SOURCE_TOPIC "mes/aveva-mes/entity/get" WITH DESTINATION_TOPIC "mes/aveva-mes/entity/get/response" WITH METHOD "GET" WITH ENDPOINT "/mesmw/api/v3/Entity/key?entId={value.json.ent_id}" WITH HEADERS ADD "Accept" "application/json" ADD "Authorization" "{value.json.authorization}" ADD EVENT FindEntityId WITH SOURCE_TOPIC "mes/aveva-mes/entity-id/get" WITH DESTINATION_TOPIC "mes/aveva-mes/entity-id/get/response" WITH METHOD "GET" WITH ENDPOINT "/mesmw/api/v3/Entity/name?entName={value.json.ent_name}&siteName={value.json.site_name}" WITH HEADERS ADD "Accept" "application/json" ADD "Authorization" "{value.json.authorization}" ADD EVENT ListLines WITH SOURCE_TOPIC "mes/aveva-mes/line/list" WITH DESTINATION_TOPIC "mes/aveva-mes/line/list/response" WITH METHOD "GET" WITH ENDPOINT "/mesmw/api/v3/line" WITH HEADERS ADD "Accept" "application/json" ADD "Authorization" "{value.json.authorization}" ADD EVENT ListLineEntities WITH SOURCE_TOPIC "mes/aveva-mes/line-entity/list" WITH DESTINATION_TOPIC "mes/aveva-mes/line-entity/list/response" WITH METHOD "GET" WITH ENDPOINT "/mesmw/api/v3/LineEntity?{value.json.query}" WITH HEADERS ADD "Accept" "application/json" ADD "Authorization" "{value.json.authorization}" ADD EVENT GetJobQueue WITH SOURCE_TOPIC "mes/aveva-mes/job-queue/get" WITH DESTINATION_TOPIC "mes/aveva-mes/job-queue/get/response" WITH METHOD "GET" WITH ENDPOINT "/mesmw/api/v3/JobQueue?{value.json.query}" WITH HEADERS ADD "Accept" "application/json" ADD "Authorization" "{value.json.authorization}" ADD EVENT ListWorkOrders WITH SOURCE_TOPIC "mes/aveva-mes/work-order/list" WITH DESTINATION_TOPIC "mes/aveva-mes/work-order/list/response" WITH METHOD "GET" WITH ENDPOINT "/mesmw/api/v3/WO/filter?{value.json.query}" WITH HEADERS ADD "Accept" "application/json" ADD "Authorization" "{value.json.authorization}" ADD EVENT GetWorkOrder WITH SOURCE_TOPIC "mes/aveva-mes/work-order/get" WITH DESTINATION_TOPIC "mes/aveva-mes/work-order/get/response" WITH METHOD "GET" WITH ENDPOINT "/mesmw/api/v3/WO/key?woId={value.json.wo_id}" WITH HEADERS ADD "Accept" "application/json" ADD "Authorization" "{value.json.authorization}" ADD EVENT ListWorkOrderJobs WITH SOURCE_TOPIC "mes/aveva-mes/work-order-jobs/list" WITH DESTINATION_TOPIC "mes/aveva-mes/work-order-jobs/list/response" WITH METHOD "GET" WITH ENDPOINT "/mesmw/api/v3/Jobs?woId={value.json.wo_id}" WITH HEADERS ADD "Accept" "application/json" ADD "Authorization" "{value.json.authorization}" ADD EVENT GetJob WITH SOURCE_TOPIC "mes/aveva-mes/job/get" WITH DESTINATION_TOPIC "mes/aveva-mes/job/get/response" WITH METHOD "GET" WITH ENDPOINT "/mesmw/api/v3/Jobs/key?woId={value.json.wo_id}&operId={value.json.oper_id}&seqNo={value.json.seq_no}" WITH HEADERS ADD "Accept" "application/json" ADD "Authorization" "{value.json.authorization}" ADD EVENT GetJobExec WITH SOURCE_TOPIC "mes/aveva-mes/job-exec/get" WITH DESTINATION_TOPIC "mes/aveva-mes/job-exec/get/response" WITH METHOD "GET" WITH ENDPOINT "/mesmw/api/v3/JobExec/key?{value.json.query}" WITH HEADERS ADD "Accept" "application/json" ADD "Authorization" "{value.json.authorization}" ADD EVENT GetOeeExec WITH SOURCE_TOPIC "mes/aveva-mes/oee-exec/get" WITH DESTINATION_TOPIC "mes/aveva-mes/oee-exec/get/response" WITH METHOD "GET" WITH ENDPOINT "/mesmw/api/v3/OeeExec/key?entId={value.json.ent_id}" WITH HEADERS ADD "Accept" "application/json" ADD "Authorization" "{value.json.authorization}" ADD EVENT GetEntityCountsAndDuration WITH SOURCE_TOPIC "mes/aveva-mes/counts-duration-entity/get" WITH DESTINATION_TOPIC "mes/aveva-mes/counts-duration-entity/get/response" WITH METHOD "GET" WITH ENDPOINT "/mesmw/api/v3/countsAndDuration/entity?{value.json.query}" WITH HEADERS ADD "Accept" "application/json" ADD "Authorization" "{value.json.authorization}" ADD EVENT GetLineCountsAndDuration WITH SOURCE_TOPIC "mes/aveva-mes/counts-duration-line/get" WITH DESTINATION_TOPIC "mes/aveva-mes/counts-duration-line/get/response" WITH METHOD "GET" WITH ENDPOINT "/mesmw/api/v3/countsAndDuration/line?{value.json.query}" WITH HEADERS ADD "Accept" "application/json" ADD "Authorization" "{value.json.authorization}" ADD EVENT ListUtilStates WITH SOURCE_TOPIC "mes/aveva-mes/util-state/list" WITH DESTINATION_TOPIC "mes/aveva-mes/util-state/list/response" WITH METHOD "GET" WITH ENDPOINT "/mesmw/api/v3/UtilState" WITH HEADERS ADD "Accept" "application/json" ADD "Authorization" "{value.json.authorization}" ADD EVENT ListUtilReasons WITH SOURCE_TOPIC "mes/aveva-mes/util-reason/list" WITH DESTINATION_TOPIC "mes/aveva-mes/util-reason/list/response" WITH METHOD "GET" WITH ENDPOINT "/mesmw/api/v3/utilreason/flat" WITH HEADERS ADD "Accept" "application/json" ADD "Authorization" "{value.json.authorization}" ADD EVENT GetAllowableReasons WITH SOURCE_TOPIC "mes/aveva-mes/allowable-reason/list" WITH DESTINATION_TOPIC "mes/aveva-mes/allowable-reason/list/response" WITH METHOD "GET" WITH ENDPOINT "/mesmw/api/v3/Util/AllowableReasons?entID={value.json.ent_id}" WITH HEADERS ADD "Accept" "application/json" ADD "Authorization" "{value.json.authorization}" ADD EVENT ListProduction WITH SOURCE_TOPIC "mes/aveva-mes/production/list" WITH DESTINATION_TOPIC "mes/aveva-mes/production/list/response" WITH METHOD "GET" WITH ENDPOINT "/mesmw/api/v3/Production/filter?{value.json.query}" WITH HEADERS ADD "Accept" "application/json" ADD "Authorization" "{value.json.authorization}" ADD EVENT ListConsumption WITH SOURCE_TOPIC "mes/aveva-mes/consumption/list" WITH DESTINATION_TOPIC "mes/aveva-mes/consumption/list/response" WITH METHOD "GET" WITH ENDPOINT "/mesmw/api/v3/Consumption/filter?{value.json.query}" WITH HEADERS ADD "Accept" "application/json" ADD "Authorization" "{value.json.authorization}" ADD EVENT ListItems WITH SOURCE_TOPIC "mes/aveva-mes/item/list" WITH DESTINATION_TOPIC "mes/aveva-mes/item/list/response" WITH METHOD "GET" WITH ENDPOINT "/mesmw/api/v3/Item/filter?{value.json.query}" WITH HEADERS ADD "Accept" "application/json" ADD "Authorization" "{value.json.authorization}" ADD EVENT ListInventory WITH SOURCE_TOPIC "mes/aveva-mes/inventory/list" WITH DESTINATION_TOPIC "mes/aveva-mes/inventory/list/response" WITH METHOD "GET" WITH ENDPOINT "/mesmw/api/v3/ItemInventory/filter?{value.json.query}" WITH HEADERS ADD "Accept" "application/json" ADD "Authorization" "{value.json.authorization}" ADD EVENT ListLots WITH SOURCE_TOPIC "mes/aveva-mes/lot/list" WITH DESTINATION_TOPIC "mes/aveva-mes/lot/list/response" WITH METHOD "GET" WITH ENDPOINT "/mesmw/api/v3/Lot/filter?{value.json.query}" WITH HEADERS ADD "Accept" "application/json" ADD "Authorization" "{value.json.authorization}" ADD EVENT ListSamples WITH SOURCE_TOPIC "mes/aveva-mes/sample/list" WITH DESTINATION_TOPIC "mes/aveva-mes/sample/list/response" WITH METHOD "GET" WITH ENDPOINT "/mesmw/api/v3/Sample/filter?{value.json.query}" WITH HEADERS ADD "Accept" "application/json" ADD "Authorization" "{value.json.authorization}" ADD EVENT AddProductionByJob WITH SOURCE_TOPIC "mes/aveva-mes/production/post" WITH DESTINATION_TOPIC "mes/aveva-mes/production/post/response" WITH METHOD "POST" WITH ENDPOINT "/mesmw/api/v3/Production/ByJob" WITH HEADERS ADD "Accept" "application/json" ADD "Content-Type" "application/json" ADD "Authorization" "{value.json.authorization}" WITH BODY "{value.json.body}" ADD EVENT AddProductionPostExec WITH SOURCE_TOPIC "mes/aveva-mes/production-post-exec/post" WITH DESTINATION_TOPIC "mes/aveva-mes/production-post-exec/post/response" WITH METHOD "POST" WITH ENDPOINT "/mesmw/api/v3/Production/PostExec" WITH HEADERS ADD "Accept" "application/json" ADD "Content-Type" "application/json" ADD "Authorization" "{value.json.authorization}" WITH BODY "{value.json.body}" ADD EVENT AddConsumptionByJob WITH SOURCE_TOPIC "mes/aveva-mes/consumption/post" WITH DESTINATION_TOPIC "mes/aveva-mes/consumption/post/response" WITH METHOD "POST" WITH ENDPOINT "/mesmw/api/v3/Consumption/ByJob" WITH HEADERS ADD "Accept" "application/json" ADD "Content-Type" "application/json" ADD "Authorization" "{value.json.authorization}" WITH BODY "{value.json.body}" ADD EVENT AddUtilizationEvent WITH SOURCE_TOPIC "mes/aveva-mes/util-event/post" WITH DESTINATION_TOPIC "mes/aveva-mes/util-event/post/response" WITH METHOD "POST" WITH ENDPOINT "/mesmw/api/v3/UtilHistory/AddEvent" WITH HEADERS ADD "Accept" "application/json" ADD "Content-Type" "application/json" ADD "Authorization" "{value.json.authorization}" WITH BODY "{value.json.body}" ADD EVENT AddCurrentUtilizationEvent WITH SOURCE_TOPIC "mes/aveva-mes/util-history/post" WITH DESTINATION_TOPIC "mes/aveva-mes/util-history/post/response" WITH METHOD "POST" WITH ENDPOINT "/mesmw/api/v3/UtilHistory" WITH HEADERS ADD "Accept" "application/json" ADD "Content-Type" "application/json" ADD "Authorization" "{value.json.authorization}" WITH BODY "{value.json.body}" ADD EVENT StartJob WITH SOURCE_TOPIC "mes/aveva-mes/job/start" WITH DESTINATION_TOPIC "mes/aveva-mes/job/start/response" WITH METHOD "PUT" WITH ENDPOINT "/mesmw/api/v3/Jobs/Start" WITH HEADERS ADD "Accept" "application/json" ADD "Content-Type" "application/json" ADD "Authorization" "{value.json.authorization}" WITH BODY "{value.json.body}" ADD EVENT PauseJob WITH SOURCE_TOPIC "mes/aveva-mes/job/pause" WITH DESTINATION_TOPIC "mes/aveva-mes/job/pause/response" WITH METHOD "PUT" WITH ENDPOINT "/mesmw/api/v3/Jobs/Pause" WITH HEADERS ADD "Accept" "application/json" ADD "Content-Type" "application/json" ADD "Authorization" "{value.json.authorization}" WITH BODY "{value.json.body}" ADD EVENT EndJob WITH SOURCE_TOPIC "mes/aveva-mes/job/end" WITH DESTINATION_TOPIC "mes/aveva-mes/job/end/response" WITH METHOD "PUT" WITH ENDPOINT "/mesmw/api/v3/Jobs/End" WITH HEADERS ADD "Accept" "application/json" ADD "Content-Type" "application/json" ADD "Authorization" "{value.json.authorization}" WITH BODY "{value.json.body}" ADD EVENT ChangeJobStates WITH SOURCE_TOPIC "mes/aveva-mes/job-states/put" WITH DESTINATION_TOPIC "mes/aveva-mes/job-states/put/response" WITH METHOD "PUT" WITH ENDPOINT "/mesmw/api/v3/Jobs/ChangeJobStates" WITH HEADERS ADD "Accept" "application/json" ADD "Content-Type" "application/json" ADD "Authorization" "{value.json.authorization}" WITH BODY "{value.json.body}" ADD EVENT ReleaseWorkOrder WITH SOURCE_TOPIC "mes/aveva-mes/work-order/release" WITH DESTINATION_TOPIC "mes/aveva-mes/work-order/release/response" WITH METHOD "PUT" WITH ENDPOINT "/mesmw/api/v3/WO/Release" WITH HEADERS ADD "Accept" "application/json" ADD "Content-Type" "application/json" ADD "Authorization" "{value.json.authorization}" WITH BODY "{value.json.body}" ADD EVENT RecordSampleData WITH SOURCE_TOPIC "mes/aveva-mes/sample-data/put" WITH DESTINATION_TOPIC "mes/aveva-mes/sample-data/put/response" WITH METHOD "PUT" WITH ENDPOINT "/mesmw/api/v3/Sample/RecordData" WITH HEADERS ADD "Accept" "application/json" ADD "Content-Type" "application/json" ADD "Authorization" "{value.json.authorization}" WITH BODY "{value.json.body}" ADD EVENT MiddlewareAccessXml WITH SOURCE_TOPIC "mes/aveva-mes/middleware-xml/post" WITH DESTINATION_TOPIC "mes/aveva-mes/middleware-xml/post/response" WITH METHOD "POST" WITH ENDPOINT "/mesmw/api/v3/MiddlewareAccess/xml" WITH HEADERS ADD "Accept" "application/json" ADD "Content-Type" "application/json" ADD "Authorization" "{value.json.authorization}" WITH BODY "{value.json.body}" ``` Deploy it with `-addConnector` (do not include `ADD METADATA`). ## Usage example Call `GetOpenApiSpec` by publishing a JSON payload to its request topic, and read the reply on the response topic: ```bash theme={"theme":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} mosquitto_sub -t 'mes/aveva-mes/openapi/get/response' & mosquitto_pub -t 'mes/aveva-mes/openapi/get' -m '{"authorization": ""}' ``` ## Related * [REST API Connectors](/latest/lot-language/routes/rest-api-routes) * [REST API Apps catalog](/latest/lot-language/routes/rest-api-apps) # AVEVA PI Web API Source: https://docs.coreflux.org/latest/lot-language/routes/rest-api-apps/aveva-pi-web-api Client-mode REST API connector for AVEVA PI Web API (PI System / FactoryTalk Historian): stream value/recorded, streamset recorded, ad-hoc bulk write, and batch Technical Preview ## Overview A client-mode connector for a **curated** slice of the [AVEVA PI Web API](https://docs.aveva.com/bundle/pi-web-api/page/topics/getting-started.html) (PI System / FactoryTalk Historian) under your customer host `https://{host}/piwebapi`: | Catalog connector event | Entity / path | Notes | | - | - | - | | | `GET /system/status` | Uptime / system state | | GET streams/\{webId}/value | `GET /streams/{webId}/value` | Current or time-specified value | | (supporting) stream recorded | `GET /streams/{webId}/recorded` | `startTime` / `endTime` / `maxCount` | | GET streamsets/\{webId}/recorded | `GET /streamsets/{webId}/recorded` | Catalog: `?startTime=` | | POST streamsets/recorded | `POST /streamsets/recorded` | Ad-hoc bulk write MQTT → PI points | | POST batch | `POST /batch` | Multi-request dictionary | `BASE_ADDRESS` is `https://YOUR_PI_HOST/piwebapi` (no trailing slash; endpoints start with `/`). Topics use `historian/aveva-pi-web-api//`. Pass stream / element **WebIds** on each MQTT payload. See the base guide: [REST API Connectors](/latest/lot-language/routes/rest-api-routes). > [!NOTE] > > **Customer-hosted.** There is no public PI Web API sandbox. Replace `YOUR_PI_HOST` and supply Basic, Kerberos, or Bearer (OIDC) credentials configured on your instance. Prefer Hub secrets. > [!NOTE] > > **Channels (WebSocket)** for live value change streams are out of scope. This package is **client REST only**. Use host Swagger at `https://{host}/piwebapi/help` for the full controller surface. > [!NOTE] > > Prefer Hub secrets for tokens / passwords. ## Basic Syntax ```lot wrap theme={"theme":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} # Coreflux REST_API template — AVEVA PI Web API # Auth: Bearer {value.json.api_token} (or Basic via USERNAME/PASSWORD secrets) # BASE_ADDRESS: https://YOUR_PI_HOST/piwebapi DEFINE CONNECTOR AvevaPiWebApiRestApi WITH TYPE REST_API ADD REST_API_CONFIG WITH BASE_ADDRESS "https://YOUR_PI_HOST/piwebapi" WITH ENABLE_CLIENT "true" WITH USE_SSL "true" WITH REQUEST_TIMEOUT '60' WITH DEFAULT_CONTENT_TYPE "application/json" ADD EVENT GetSystemStatus WITH SOURCE_TOPIC "historian/aveva-pi-web-api/system/status" WITH DESTINATION_TOPIC "historian/aveva-pi-web-api/system/status/response" WITH METHOD "GET" WITH ENDPOINT "/system/status" WITH HEADERS ADD "Accept" "application/json" ADD "Authorization" "Bearer {value.json.api_token}" ADD EVENT GetStreamValue WITH SOURCE_TOPIC "historian/aveva-pi-web-api/streams/value" WITH DESTINATION_TOPIC "historian/aveva-pi-web-api/streams/value/response" WITH METHOD "GET" WITH ENDPOINT "/streams/{value.json.webId}/value" WITH HEADERS ADD "Accept" "application/json" ADD "Authorization" "Bearer {value.json.api_token}" ADD EVENT GetStreamRecorded WITH SOURCE_TOPIC "historian/aveva-pi-web-api/streams/recorded" WITH DESTINATION_TOPIC "historian/aveva-pi-web-api/streams/recorded/response" WITH METHOD "GET" WITH ENDPOINT "/streams/{value.json.webId}/recorded?startTime={value.json.startTime}&endTime={value.json.endTime}&maxCount={value.json.maxCount}" WITH HEADERS ADD "Accept" "application/json" ADD "Authorization" "Bearer {value.json.api_token}" ADD EVENT GetStreamSetRecorded WITH SOURCE_TOPIC "historian/aveva-pi-web-api/streamsets/recorded" WITH DESTINATION_TOPIC "historian/aveva-pi-web-api/streamsets/recorded/response" WITH METHOD "GET" WITH ENDPOINT "/streamsets/{value.json.webId}/recorded?startTime={value.json.startTime}&endTime={value.json.endTime}&maxCount={value.json.maxCount}" WITH HEADERS ADD "Accept" "application/json" ADD "Authorization" "Bearer {value.json.api_token}" ADD EVENT PostStreamSetsRecorded WITH SOURCE_TOPIC "historian/aveva-pi-web-api/streamsets/write" WITH DESTINATION_TOPIC "historian/aveva-pi-web-api/streamsets/write/response" WITH METHOD "POST" WITH ENDPOINT "/streamsets/recorded" WITH HEADERS ADD "Accept" "application/json" ADD "Content-Type" "application/json" ADD "Authorization" "Bearer {value.json.api_token}" WITH BODY "{payload}" ADD EVENT PostBatch WITH SOURCE_TOPIC "historian/aveva-pi-web-api/batch/execute" WITH DESTINATION_TOPIC "historian/aveva-pi-web-api/batch/execute/response" WITH METHOD "POST" WITH ENDPOINT "/batch" WITH HEADERS ADD "Accept" "application/json" ADD "Content-Type" "application/json" ADD "Authorization" "Bearer {value.json.api_token}" WITH BODY "{payload}" ``` ## Events | Event | Method | Endpoint | Request topic | Response topic | | - | - | - | - | - | | GetSystemStatus | GET | `/system/status` | `historian/aveva-pi-web-api/system/status` | `.../response` | | GetStreamValue | GET | `/streams/{webId}/value` | `historian/aveva-pi-web-api/streams/value` | `.../response` | | GetStreamRecorded | GET | `/streams/{webId}/recorded?startTime=&endTime=&maxCount=` | `historian/aveva-pi-web-api/streams/recorded` | `.../response` | | GetStreamSetRecorded | GET | `/streamsets/{webId}/recorded?startTime=&endTime=&maxCount=` | `historian/aveva-pi-web-api/streamsets/recorded` | `.../response` | | PostStreamSetsRecorded | POST | `/streamsets/recorded` | `historian/aveva-pi-web-api/streamsets/write` | `.../response` | | PostBatch | POST | `/batch` | `historian/aveva-pi-web-api/batch/execute` | `.../response` | ## Auth **Bearer (OIDC) — default in the template:** 1. Obtain an access token from your PI Web API OpenID Connect / identity provider configuration. 2. Pass `api_token` on the MQTT request payload so headers resolve: * `Authorization: Bearer {value.json.api_token}` 3. Store the token in Hub secrets / `GET SECRET "PI_WEB_API_TOKEN"`. Never commit real credentials. **Basic (optional):** 1. Uncomment `WITH USERNAME` / `WITH PASSWORD GET SECRET "PI_WEB_API_PASSWORD"` on `REST_API_CONFIG`, **or** send `Authorization: Basic ` by adjusting the header scheme and payload field. 2. Map the identity to a PI / AF identity with read/write rights for the target points. Kerberos is supported by PI Web API deployments but is not expressed in this MQTT client template. ## Example payloads **System status:** ```json theme={"theme":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} { "api_token": "YOUR_OIDC_OR_BEARER_TOKEN" } ``` **Stream value:** ```json theme={"theme":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} { "api_token": "YOUR_OIDC_OR_BEARER_TOKEN", "webId": "YOUR_POINT_OR_ATTRIBUTE_WEBID" } ``` **Stream set recorded:** ```json theme={"theme":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} { "api_token": "YOUR_OIDC_OR_BEARER_TOKEN", "webId": "YOUR_ELEMENT_WEBID", "startTime": "*-1h", "endTime": "*", "maxCount": "1000" } ``` **Ad-hoc write (`PostStreamSetsRecorded`):** ```json theme={"theme":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} { "api_token": "YOUR_OIDC_OR_BEARER_TOKEN", "Items": [ { "WebId": "YOUR_POINT_WEBID", "Items": [ { "Timestamp": "*", "Value": 42.5 } ] } ] } ``` The HTTP body is the full MQTT `{payload}` (include only fields PI Web API expects). ## Deploy notes * Deployable file: `AvevaPiWebApiRestApi.lot` — **no** `ADD METADATA`. * Hub catalog sidecar: `hub-metadata.lot` (`WITH STATUS "beta"`, icon, doclink). * Replace `YOUR_PI_HOST` before use. TLS: keep `USE_SSL "true"` for HTTPS hosts. * Obtain WebIds via Points / Attributes / Elements search on your host (or batch step 1) before stream calls. ## Related * [REST API Connectors](/latest/lot-language/routes/rest-api-routes) * [REST API app catalog](/latest/lot-language/routes/rest-api-apps) * [AVEVA PI Web API getting started](https://docs.aveva.com/bundle/pi-web-api/page/topics/getting-started.html) * [Stream controller](https://docs.aveva.com/bundle/pi-web-api-reference/page/help/controllers/stream.html) * [StreamSet controller](https://docs.aveva.com/bundle/pi-web-api-reference/page/help/controllers/streamset.html) * [Batch controller](https://docs.aveva.com/bundle/pi-web-api-reference/page/help/controllers/batch.html) # AWS IoT Core Source: https://docs.coreflux.org/latest/lot-language/routes/rest-api-apps/aws-iot-core Client-mode REST API connector for a curated AWS IoT Core slice: HTTPS Publish, Device Shadow GET/POST, CreateJob (OTA), and ListThings Technical Preview ## Overview A client-mode connector for a **curated cloud-IoT** slice of [AWS IoT Core](https://docs.aws.amazon.com/iot/latest/developerguide/what-is-aws-iot.html) covering registry, Jobs/OTA, classic Device Shadow, and HTTPS data-plane publish (no MQTT bridge): | Catalog connector event | Entity / path | Notes | | - | - | - | | GET ListThings | `GET /things` | Control-plane credential check | | POST CreateJob (OTA) | `PUT /jobs/{jobId}` | AWS uses **PUT** (catalog named it POST CreateJob) | | GET things/\{name}/shadow | `GET /things/{thingName}/shadow` | Classic Device Shadow | | POST things/\{name}/shadow | `POST /things/{thingName}/shadow` | Classic Device Shadow update | | POST topics/\{topic} | `POST /topics/{topic}?qos=0\|1` | Data-plane HTTPS publish — switch BASE to ATS endpoint | Default `BASE_ADDRESS` is control-plane `https://iot.eu-west-1.amazonaws.com` (no trailing slash; substitute `{region}`). Topics use `cloud-iot/aws-iot-core//`. See the base guide: [REST API Connectors](/latest/lot-language/routes/rest-api-routes). > [!NOTE] > > **Data-plane Publish:** set `BASE_ADDRESS` to your ATS endpoint from `aws iot describe-endpoint --endpoint-type iot:Data-ATS` (for example `https://xxxxxx-ats.iot.eu-west-1.amazonaws.com`). SigV4 service name on the data plane is `iotdevicegateway` (port 443). X.509 on `:8443` is AWS-supported but outside this LoT template. > [!NOTE] > > **Control-plane SigV4 service name** is `iot`. Pass `Authorization`, `x-amz-date`, and `x-amz-content-sha256` from an upstream signer via MQTT JSON placeholders. Prefer Hub secrets for AWS keys — never commit them into the `.lot`. > [!NOTE] > > Prefer Hub secrets for AWS keys used by an upstream SigV4 signer. ## Basic Syntax ```lot wrap theme={"theme":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} # Coreflux REST_API template — AWS IoT Core curated slice # Auth: SigV4 via {value.json.authorization}, {value.json.x_amz_date}, # {value.json.x_amz_content_sha256} # BASE_ADDRESS: https://iot.eu-west-1.amazonaws.com (control plane) DEFINE CONNECTOR AwsIotCoreRestApi WITH TYPE REST_API ADD REST_API_CONFIG WITH BASE_ADDRESS "https://iot.eu-west-1.amazonaws.com" WITH ENABLE_CLIENT "true" WITH USE_SSL "true" WITH REQUEST_TIMEOUT '60' WITH DEFAULT_CONTENT_TYPE "application/json" ADD EVENT ListThings WITH SOURCE_TOPIC "cloud-iot/aws-iot-core/things/list" WITH DESTINATION_TOPIC "cloud-iot/aws-iot-core/things/list/response" WITH METHOD "GET" WITH ENDPOINT "/things?maxResults={value.json.max_results}" WITH HEADERS ADD "Accept" "application/json" ADD "Authorization" "{value.json.authorization}" ADD "x-amz-date" "{value.json.x_amz_date}" ADD "x-amz-content-sha256" "{value.json.x_amz_content_sha256}" ADD EVENT CreateJob WITH SOURCE_TOPIC "cloud-iot/aws-iot-core/jobs/create" WITH DESTINATION_TOPIC "cloud-iot/aws-iot-core/jobs/create/response" WITH METHOD "PUT" WITH ENDPOINT "/jobs/{value.json.job_id}" WITH HEADERS ADD "Accept" "application/json" ADD "Content-Type" "application/json" ADD "Authorization" "{value.json.authorization}" ADD "x-amz-date" "{value.json.x_amz_date}" ADD "x-amz-content-sha256" "{value.json.x_amz_content_sha256}" WITH BODY "{payload}" ADD EVENT GetThingShadow WITH SOURCE_TOPIC "cloud-iot/aws-iot-core/shadow/get" WITH DESTINATION_TOPIC "cloud-iot/aws-iot-core/shadow/get/response" WITH METHOD "GET" WITH ENDPOINT "/things/{value.json.thing_name}/shadow" WITH HEADERS ADD "Accept" "application/json" ADD "Authorization" "{value.json.authorization}" ADD "x-amz-date" "{value.json.x_amz_date}" ADD "x-amz-content-sha256" "{value.json.x_amz_content_sha256}" ADD EVENT UpdateThingShadow WITH SOURCE_TOPIC "cloud-iot/aws-iot-core/shadow/update" WITH DESTINATION_TOPIC "cloud-iot/aws-iot-core/shadow/update/response" WITH METHOD "POST" WITH ENDPOINT "/things/{value.json.thing_name}/shadow" WITH HEADERS ADD "Accept" "application/json" ADD "Content-Type" "application/json" ADD "Authorization" "{value.json.authorization}" ADD "x-amz-date" "{value.json.x_amz_date}" ADD "x-amz-content-sha256" "{value.json.x_amz_content_sha256}" WITH BODY "{payload}" ADD EVENT PublishTopic WITH SOURCE_TOPIC "cloud-iot/aws-iot-core/topics/publish" WITH DESTINATION_TOPIC "cloud-iot/aws-iot-core/topics/publish/response" WITH METHOD "POST" WITH ENDPOINT "/topics/{value.json.topic}?qos={value.json.qos}" WITH HEADERS ADD "Accept" "application/json" ADD "Content-Type" "application/json" ADD "Authorization" "{value.json.authorization}" ADD "x-amz-date" "{value.json.x_amz_date}" ADD "x-amz-content-sha256" "{value.json.x_amz_content_sha256}" WITH BODY "{payload}" ``` ## Configuration | Setting | Value | | - | - | | Connector name | `AwsIotCoreRestApi` | | Type | `REST_API` (client) | | BASE\_ADDRESS | `https://iot.eu-west-1.amazonaws.com` (control plane; change region as needed) | | Topic prefix | `cloud-iot/aws-iot-core/...` | | Default Content-Type | `application/json` | | Auth | SigV4 headers from MQTT placeholders | | Status | Technical preview — needs AWS keys (+ data endpoint for Publish) | ## MQTT topics | Event | Source | Response | | - | - | - | | ListThings | `cloud-iot/aws-iot-core/things/list` | `.../things/list/response` | | CreateJob | `cloud-iot/aws-iot-core/jobs/create` | `.../jobs/create/response` | | GetThingShadow | `cloud-iot/aws-iot-core/shadow/get` | `.../shadow/get/response` | | UpdateThingShadow | `cloud-iot/aws-iot-core/shadow/update` | `.../shadow/update/response` | | PublishTopic | `cloud-iot/aws-iot-core/topics/publish` | `.../topics/publish/response` | ## Auth details 1. Sign each request with AWS Signature Version 4 using the correct **service name** (`iot` on control plane; `iotdevicegateway` on data-plane Publish). 2. Map signed values into MQTT JSON: `authorization`, `x_amz_date`, `x_amz_content_sha256` (and optional STS session token). 3. Store `AWS_ACCESS_KEY_ID` / `AWS_SECRET_ACCESS_KEY` as Hub secrets for the upstream signer — never in the deployable `.lot`. ## Related * [REST API Connectors](/latest/lot-language/routes/rest-api-routes) * [Amazon S3](/latest/lot-language/routes/rest-api-apps/amazon-s3) (same SigV4 placeholder pattern) * [AWS IoT ListThings](https://docs.aws.amazon.com/iot/latest/apireference/API_ListThings.html) * [AWS IoT CreateJob](https://docs.aws.amazon.com/iot/latest/apireference/API_CreateJob.html) * [Device Shadow REST API](https://docs.aws.amazon.com/iot/latest/developerguide/device-shadow-rest-api.html) * [HTTPS publish](https://docs.aws.amazon.com/iot/latest/developerguide/http.html) # AWS IoT SiteWise Source: https://docs.coreflux.org/latest/lot-language/routes/rest-api-apps/aws-iot-sitewise Client-mode REST API connector for a curated AWS IoT SiteWise slice: BatchPutAssetPropertyValue, GetAssetPropertyValueHistory, CreateAsset, and ListAssets Technical Preview ## Overview A client-mode connector for a **curated cloud-IoT** slice of [AWS IoT SiteWise](https://docs.aws.amazon.com/iot-sitewise/latest/userguide/what-is-sitewise.html) covering property ingest from UNS topics, property history queries, and asset create from Sparkplug birth: | Catalog connector event | Entity / path | Notes | | - | - | - | | POST properties batch put | `POST /properties` | Data-plane BatchPutAssetPropertyValue | | GET property history | `GET /properties/history` | Data-plane GetAssetPropertyValueHistory | | POST assets create (Sparkplug birth) | `POST /assets` | Control-plane CreateAsset — switch BASE to `api.iotsitewise` | | GET ListAssets (checks the token) | `GET /assets` | Control-plane ListAssets — switch BASE to `api.iotsitewise` | Default `BASE_ADDRESS` is data-plane `https://data.iotsitewise.eu-west-1.amazonaws.com` (no trailing slash; substitute `{region}`). Topics use `cloud-iot/aws-iot-sitewise//`. See the base guide: [REST API Connectors](/latest/lot-language/routes/rest-api-routes). > [!NOTE] > > **Control-plane ops:** set `BASE_ADDRESS` to `https://api.iotsitewise.{region}.amazonaws.com` for CreateAsset and ListAssets. SigV4 service name on both planes is `iotsitewise`. > [!NOTE] > > **Data-plane SigV4:** Pass `Authorization`, `x-amz-date`, and `x-amz-content-sha256` from an upstream signer via MQTT JSON placeholders. Prefer Hub secrets for AWS keys — never commit them into the `.lot`. > [!NOTE] > > Prefer Hub secrets for AWS keys used by an upstream SigV4 signer. ## Basic Syntax ```lot wrap theme={"theme":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} # Coreflux REST_API template — AWS IoT SiteWise curated slice # Auth: SigV4 via {value.json.authorization}, {value.json.x_amz_date}, # {value.json.x_amz_content_sha256} # BASE_ADDRESS: https://data.iotsitewise.eu-west-1.amazonaws.com (data plane) DEFINE CONNECTOR AwsIotSitewiseRestApi WITH TYPE REST_API ADD REST_API_CONFIG WITH BASE_ADDRESS "https://data.iotsitewise.eu-west-1.amazonaws.com" WITH ENABLE_CLIENT "true" WITH USE_SSL "true" WITH REQUEST_TIMEOUT '60' WITH DEFAULT_CONTENT_TYPE "application/json" ADD EVENT BatchPutAssetPropertyValue WITH SOURCE_TOPIC "cloud-iot/aws-iot-sitewise/properties/batch-put" WITH DESTINATION_TOPIC "cloud-iot/aws-iot-sitewise/properties/batch-put/response" WITH METHOD "POST" WITH ENDPOINT "/properties" WITH HEADERS ADD "Accept" "application/json" ADD "Content-Type" "application/json" ADD "Authorization" "{value.json.authorization}" ADD "x-amz-date" "{value.json.x_amz_date}" ADD "x-amz-content-sha256" "{value.json.x_amz_content_sha256}" WITH BODY "{payload}" ADD EVENT GetAssetPropertyValueHistory WITH SOURCE_TOPIC "cloud-iot/aws-iot-sitewise/properties/history" WITH DESTINATION_TOPIC "cloud-iot/aws-iot-sitewise/properties/history/response" WITH METHOD "GET" WITH ENDPOINT "/properties/history?propertyAlias={value.json.property_alias}&startDate={value.json.start_date}&endDate={value.json.end_date}&maxResults={value.json.max_results}" WITH HEADERS ADD "Accept" "application/json" ADD "Authorization" "{value.json.authorization}" ADD "x-amz-date" "{value.json.x_amz_date}" ADD "x-amz-content-sha256" "{value.json.x_amz_content_sha256}" ADD EVENT CreateAsset WITH SOURCE_TOPIC "cloud-iot/aws-iot-sitewise/assets/create" WITH DESTINATION_TOPIC "cloud-iot/aws-iot-sitewise/assets/create/response" WITH METHOD "POST" WITH ENDPOINT "/assets" WITH HEADERS ADD "Accept" "application/json" ADD "Content-Type" "application/json" ADD "Authorization" "{value.json.authorization}" ADD "x-amz-date" "{value.json.x_amz_date}" ADD "x-amz-content-sha256" "{value.json.x_amz_content_sha256}" WITH BODY "{payload}" ADD EVENT ListAssets WITH SOURCE_TOPIC "cloud-iot/aws-iot-sitewise/assets/list" WITH DESTINATION_TOPIC "cloud-iot/aws-iot-sitewise/assets/list/response" WITH METHOD "GET" WITH ENDPOINT "/assets?filter={value.json.filter}&maxResults={value.json.max_results}" WITH HEADERS ADD "Accept" "application/json" ADD "Authorization" "{value.json.authorization}" ADD "x-amz-date" "{value.json.x_amz_date}" ADD "x-amz-content-sha256" "{value.json.x_amz_content_sha256}" ``` ## Configuration | Setting | Value | | - | - | | Connector name | `AwsIotSitewiseRestApi` | | Type | `REST_API` (client) | | BASE\_ADDRESS | `https://data.iotsitewise.eu-west-1.amazonaws.com` (data plane; change region as needed) | | Topic prefix | `cloud-iot/aws-iot-sitewise/...` | | Default Content-Type | `application/json` | | Auth | SigV4 headers from MQTT placeholders (service `iotsitewise`) | | Status | Technical preview — needs AWS keys + region | ## MQTT topics | Event | Source | Response | | - | - | - | | BatchPutAssetPropertyValue | `cloud-iot/aws-iot-sitewise/properties/batch-put` | `.../properties/batch-put/response` | | GetAssetPropertyValueHistory | `cloud-iot/aws-iot-sitewise/properties/history` | `.../properties/history/response` | | CreateAsset | `cloud-iot/aws-iot-sitewise/assets/create` | `.../assets/create/response` | | ListAssets | `cloud-iot/aws-iot-sitewise/assets/list` | `.../assets/list/response` | ## Auth details 1. Sign each request with AWS Signature Version 4 using service name **`iotsitewise`** (both planes). 2. Map signed values into MQTT JSON: `authorization`, `x_amz_date`, `x_amz_content_sha256` (and optional STS session token). 3. Store `AWS_ACCESS_KEY_ID` / `AWS_SECRET_ACCESS_KEY` as Hub secrets for the upstream signer — never in the deployable `.lot`. 4. Use data-plane BASE for BatchPut / history; switch to control-plane BASE (`api.iotsitewise.{region}.amazonaws.com`) for CreateAsset / ListAssets. ## Related * [REST API Connectors](/latest/lot-language/routes/rest-api-routes) * [AWS IoT Core](/latest/lot-language/routes/rest-api-apps/aws-iot-core) (same SigV4 placeholder pattern) * [Amazon S3](/latest/lot-language/routes/rest-api-apps/amazon-s3) (same SigV4 placeholder pattern) * [BatchPutAssetPropertyValue](https://docs.aws.amazon.com/iot-sitewise/latest/APIReference/API_BatchPutAssetPropertyValue.html) * [GetAssetPropertyValueHistory](https://docs.aws.amazon.com/iot-sitewise/latest/APIReference/API_GetAssetPropertyValueHistory.html) * [CreateAsset](https://docs.aws.amazon.com/iot-sitewise/latest/APIReference/API_CreateAsset.html) * [ListAssets](https://docs.aws.amazon.com/iot-sitewise/latest/APIReference/API_ListAssets.html) * [SiteWise endpoints](https://docs.aws.amazon.com/general/latest/gr/iot-sitewise.html) # Azure Blob Storage and Data Lake Gen2 Source: https://docs.coreflux.org/latest/lot-language/routes/rest-api-apps/azure-blob-storage-and-data-lake-gen2 Client-mode REST API connector for a curated Azure Blob slice: Put Blob, Append Block, Get Blob, List Blobs, and PUT/GET via SAS URL Technical Preview ## Overview A client-mode connector for a **curated IIoT object-storage** slice of the [Azure Blob service REST API](https://learn.microsoft.com/en-us/rest/api/storageservices/blob-service-rest-api) under `https://{account}.blob.core.windows.net`: | Catalog connector event | Entity / path | Notes | | - | - | - | | | `GET /{container}?restype=container&comp=list` | List Blobs | | PUT blob | `PUT /{container}/{blob}` | `x-ms-blob-type: BlockBlob` | | (optional) Create Append Blob | `PUT /{container}/{blob}` | `x-ms-blob-type: AppendBlob`, empty body | | PUT append block (continuous log) | `PUT /{container}/{blob}?comp=appendblock` | Existing AppendBlob only | | GET blob | `GET /{container}/{blob}` | Download body | | (optional) HEAD properties | `HEAD /{container}/{blob}` | Metadata without body | | PUT / GET via SAS | `PUT/GET {sas_path}` | No Authorization; switch BASE to URL origin | | Event Grid blob webhook | — | Docs-only; future `ENABLE_SERVER` | `BASE_ADDRESS` defaults to the placeholder `https://ACCOUNT.blob.core.windows.net` (no trailing slash). Replace `ACCOUNT` with your storage account. Topics use `storage/azure-blob-storage-and-data-lake-gen2//`. > [!NOTE] > > **Data Lake Gen2:** hierarchical namespace filesystem REST uses `https://{account}.dfs.core.windows.net`. Keep primary events on the Blob endpoint; swap BASE only when calling DFS path APIs. > [!NOTE] > > **SAS PUT/GET:** set `BASE_ADDRESS` to the SAS URL origin only, put path+query in `sas_path`, and omit Shared Key / Bearer — the query string carries `sv` / `sig`. Ideal for edge devices that must not hold account keys. > [!NOTE] > > **Event Grid** blob created/deleted subscriptions are not inbound HTTP on this client connector. Receiving Event Grid validation and event POSTs needs a separate `ENABLE_SERVER` REST\_API template (not packaged). > [!NOTE] > > Prefer Hub secrets for account keys or Entra client credentials used by an upstream signer. ## Basic Syntax ```lot wrap theme={"theme":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} # Coreflux REST_API template — Azure Blob curated slice # Auth: SharedKey or Bearer via {value.json.authorization}, # {value.json.x_ms_date}, {value.json.x_ms_version} # — or PutViaSas / GetViaSas (no Authorization) # BASE_ADDRESS: https://ACCOUNT.blob.core.windows.net DEFINE CONNECTOR AzureBlobStorageAndDataLakeGen2RestApi WITH TYPE REST_API ADD REST_API_CONFIG WITH BASE_ADDRESS "https://ACCOUNT.blob.core.windows.net" WITH ENABLE_CLIENT "true" WITH USE_SSL "true" WITH REQUEST_TIMEOUT '60' WITH DEFAULT_CONTENT_TYPE "application/octet-stream" ADD EVENT Listblobs WITH SOURCE_TOPIC "storage/azure-blob-storage-and-data-lake-gen2/blobs/list" WITH DESTINATION_TOPIC "storage/azure-blob-storage-and-data-lake-gen2/blobs/list/response" WITH METHOD "GET" WITH ENDPOINT "/{value.json.container}?restype=container&comp=list" WITH HEADERS ADD "Accept" "application/xml" ADD "Authorization" "{value.json.authorization}" ADD "x-ms-date" "{value.json.x_ms_date}" ADD "x-ms-version" "{value.json.x_ms_version}" ADD EVENT Putblob WITH SOURCE_TOPIC "storage/azure-blob-storage-and-data-lake-gen2/blob/put" WITH DESTINATION_TOPIC "storage/azure-blob-storage-and-data-lake-gen2/blob/put/response" WITH METHOD "PUT" WITH ENDPOINT "/{value.json.container}/{value.json.blob}" WITH HEADERS ADD "Accept" "application/xml" ADD "Content-Type" "{value.json.content_type}" ADD "x-ms-blob-type" "BlockBlob" ADD "Authorization" "{value.json.authorization}" ADD "x-ms-date" "{value.json.x_ms_date}" ADD "x-ms-version" "{value.json.x_ms_version}" WITH BODY "{payload}" ADD EVENT Createappendblob WITH SOURCE_TOPIC "storage/azure-blob-storage-and-data-lake-gen2/blob/create-append" WITH DESTINATION_TOPIC "storage/azure-blob-storage-and-data-lake-gen2/blob/create-append/response" WITH METHOD "PUT" WITH ENDPOINT "/{value.json.container}/{value.json.blob}" WITH HEADERS ADD "Accept" "application/xml" ADD "Content-Type" "application/octet-stream" ADD "Content-Length" "0" ADD "x-ms-blob-type" "AppendBlob" ADD "Authorization" "{value.json.authorization}" ADD "x-ms-date" "{value.json.x_ms_date}" ADD "x-ms-version" "{value.json.x_ms_version}" ADD EVENT Appendblock WITH SOURCE_TOPIC "storage/azure-blob-storage-and-data-lake-gen2/blob/append-block" WITH DESTINATION_TOPIC "storage/azure-blob-storage-and-data-lake-gen2/blob/append-block/response" WITH METHOD "PUT" WITH ENDPOINT "/{value.json.container}/{value.json.blob}?comp=appendblock" WITH HEADERS ADD "Accept" "application/xml" ADD "Content-Type" "{value.json.content_type}" ADD "Authorization" "{value.json.authorization}" ADD "x-ms-date" "{value.json.x_ms_date}" ADD "x-ms-version" "{value.json.x_ms_version}" WITH BODY "{payload}" ADD EVENT Getblob WITH SOURCE_TOPIC "storage/azure-blob-storage-and-data-lake-gen2/blob/get" WITH DESTINATION_TOPIC "storage/azure-blob-storage-and-data-lake-gen2/blob/get/response" WITH METHOD "GET" WITH ENDPOINT "/{value.json.container}/{value.json.blob}" WITH HEADERS ADD "Accept" "*/*" ADD "Authorization" "{value.json.authorization}" ADD "x-ms-date" "{value.json.x_ms_date}" ADD "x-ms-version" "{value.json.x_ms_version}" ADD EVENT Getblobproperties WITH SOURCE_TOPIC "storage/azure-blob-storage-and-data-lake-gen2/blob/head" WITH DESTINATION_TOPIC "storage/azure-blob-storage-and-data-lake-gen2/blob/head/response" WITH METHOD "HEAD" WITH ENDPOINT "/{value.json.container}/{value.json.blob}" WITH HEADERS ADD "Authorization" "{value.json.authorization}" ADD "x-ms-date" "{value.json.x_ms_date}" ADD "x-ms-version" "{value.json.x_ms_version}" ADD EVENT Putviasas WITH SOURCE_TOPIC "storage/azure-blob-storage-and-data-lake-gen2/blob/put-via-sas" WITH DESTINATION_TOPIC "storage/azure-blob-storage-and-data-lake-gen2/blob/put-via-sas/response" WITH METHOD "PUT" WITH ENDPOINT "{value.json.sas_path}" WITH HEADERS ADD "Content-Type" "{value.json.content_type}" ADD "x-ms-blob-type" "BlockBlob" ADD "x-ms-version" "{value.json.x_ms_version}" WITH BODY "{payload}" ADD EVENT Getviasas WITH SOURCE_TOPIC "storage/azure-blob-storage-and-data-lake-gen2/blob/get-via-sas" WITH DESTINATION_TOPIC "storage/azure-blob-storage-and-data-lake-gen2/blob/get-via-sas/response" WITH METHOD "GET" WITH ENDPOINT "{value.json.sas_path}" WITH HEADERS ADD "Accept" "*/*" ADD "x-ms-version" "{value.json.x_ms_version}" ``` ## Configuration | Setting | Value | | - | - | | Connector name | `AzureBlobStorageAndDataLakeGen2RestApi` | | Type | `REST_API` (client) | | BASE\_ADDRESS | `https://ACCOUNT.blob.core.windows.net` (replace ACCOUNT) | | Topic prefix | `storage/azure-blob-storage-and-data-lake-gen2/...` | | Default Content-Type | `application/octet-stream` | | API version header | `x-ms-version: 2023-11-03` (payload-driven) | | Hub status | Technical preview (`WITH STATUS "beta"`) | ## Authentication ### Shared Key / Entra Bearer Blob does **not** use a simple static Bearer for Shared Key. Sign each request or obtain an Entra token and pass: | MQTT JSON field | HTTP header | | - | - | | `authorization` | `Authorization` (`SharedKey account:…` or `Bearer …`) | | `x_ms_date` | `x-ms-date` (RFC 1123 UTC) | | `x_ms_version` | `x-ms-version` (e.g. `2023-11-03`) | Use Hub secrets for account keys / Entra client credentials in an upstream signing helper. Never commit real keys. ### SAS URLs (recommended for edge) Generate a time-limited service or user-delegation SAS with Azure CLI/SDK/portal. For PutViaSas / GetViaSas: 1. Point `BASE_ADDRESS` at the URL origin. 2. Put path + query into `sas_path`. 3. Omit Authorization — do not re-sign on the broker. ## MQTT topics | Event | Request topic | Response topic | | - | - | - | | ListBlobs | `storage/azure-blob-storage-and-data-lake-gen2/blobs/list` | `…/response` | | PutBlob | `storage/azure-blob-storage-and-data-lake-gen2/blob/put` | `…/response` | | CreateAppendBlob | `storage/azure-blob-storage-and-data-lake-gen2/blob/create-append` | `…/response` | | AppendBlock | `storage/azure-blob-storage-and-data-lake-gen2/blob/append-block` | `…/response` | | GetBlob | `storage/azure-blob-storage-and-data-lake-gen2/blob/get` | `…/response` | | GetBlobProperties | `storage/azure-blob-storage-and-data-lake-gen2/blob/head` | `…/response` | | PutViaSas | `storage/azure-blob-storage-and-data-lake-gen2/blob/put-via-sas` | `…/response` | | GetViaSas | `storage/azure-blob-storage-and-data-lake-gen2/blob/get-via-sas` | `…/response` | ## Example payloads **ListBlobs (after signing):** ```json theme={"theme":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} { "container": "iiot-rollups", "authorization": "SharedKey myaccount:…", "x_ms_date": "Sun, 04 Oct 2026 04:00:00 GMT", "x_ms_version": "2023-11-03" } ``` **PutBlob CSV rollup:** ```json theme={"theme":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} { "container": "iiot-rollups", "blob": "line-a/2026-10-04/rollup.csv", "content_type": "text/csv", "authorization": "SharedKey myaccount:…", "x_ms_date": "Sun, 04 Oct 2026 04:01:00 GMT", "x_ms_version": "2023-11-03", "payload": "ts,device,temp\n2026-10-04T04:01:00Z,sensor-01,72.5\n" } ``` **PutViaSas:** ```json theme={"theme":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} { "sas_path": "/iiot-rollups/line-a/snapshot.json?sv=2023-11-03&ss=b&srt=o&sp=cw&se=2026-10-04T12:00:00Z&sig=…", "content_type": "application/json", "x_ms_version": "2023-11-03", "payload": "{\"device\":\"sensor-01\",\"temp\":72.5}" } ``` ## References * [Put Blob](https://learn.microsoft.com/en-us/rest/api/storageservices/put-blob) * [Append Block](https://learn.microsoft.com/en-us/rest/api/storageservices/append-block) * [Get Blob](https://learn.microsoft.com/en-us/rest/api/storageservices/get-blob) * [List Blobs](https://learn.microsoft.com/en-us/rest/api/storageservices/list-blobs) * [Authorize with Shared Key](https://learn.microsoft.com/en-us/rest/api/storageservices/authorize-with-shared-key) * [Delegate access with SAS](https://learn.microsoft.com/en-us/rest/api/storageservices/delegate-access-with-shared-access-signature) * [Azure Data Lake Storage Gen2](https://learn.microsoft.com/en-us/azure/storage/blobs/data-lake-storage-introduction) * [Event Grid blob events](https://learn.microsoft.com/en-us/azure/event-grid/event-schema-blob-storage) * [Coreflux REST API Connectors](/latest/lot-language/routes/rest-api-routes) ## Package status # Azure Cosmos DB Source: https://docs.coreflux.org/latest/lot-language/routes/rest-api-apps/azure-cosmos-db LoT connector for Azure Cosmos DB for NoSQL: create, upsert, read, replace, list and SQL query documents from MQTT using Entra ID tokens Technical Preview ## Overview Client-mode REST API connector for the Azure Cosmos DB for NoSQL data plane: create and upsert and read and replace documents and SQL queries with Entra ID tokens. `BASE_ADDRESS` defaults to `https://YOUR_ACCOUNT.documents.azure.com`. Topics use `data/azure-cosmos-db//`. | Event | Request | Source topic | | - | - | - | | `DocumentCreate` | `POST /dbs/{value.json.db}/colls/{value.json.coll}/docs` | `data/azure-cosmos-db/documents/create` | | `DocumentUpsert` | `POST /dbs/{value.json.db}/colls/{value.json.coll}/docs` | `data/azure-cosmos-db/documents/upsert` | | `DocumentGet` | `GET /dbs/{value.json.db}/colls/{value.json.coll}/docs/{value.json.id}` | `data/azure-cosmos-db/documents/get` | | `DocumentReplace` | `PUT /dbs/{value.json.db}/colls/{value.json.coll}/docs/{value.json.id}` | `data/azure-cosmos-db/documents/replace` | | `DocumentQuery` | `POST /dbs/{value.json.db}/colls/{value.json.coll}/docs` | `data/azure-cosmos-db/documents/query` | | `DocumentsList` | `GET /dbs/{value.json.db}/colls/{value.json.coll}/docs` | `data/azure-cosmos-db/documents/list` | ## Authentication Microsoft Entra ID (Cosmos DB RBAC): Authorization: \{value.json.authorization} = URL-encoded "type=aad\&ver=1.0\&sig=\" (scope [https://YOUR\_ACCOUNT.documents.azure.com/.default](https://YOUR_ACCOUNT.documents.azure.com/.default); role Cosmos DB Built-in Data Contributor). A resource token (type=resource) also works. Master-key auth needs a per-request HMAC over verb, resource and date, which the REST\_API connector cannot compute, so it is not supported. x-ms-date (RFC 1123, UTC, within 15 minutes) is passed per payload. > [!WARNING] > > Keep credentials in broker secrets and never retain topics that carry tokens or client secrets. ## Notes * partition\_key is a JSON array string, e.g. \["line-1"]. * Entra RBAC covers data-plane operations only; databases and containers are created through ARM / portal. * DocumentQuery sends \{value.json.body} = \{"query":"SELECT ...","parameters":\[...]} as application/query+json. ## Basic Syntax ```lot wrap theme={"theme":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} DEFINE CONNECTOR AzureCosmosDbRestApi WITH TYPE REST_API ADD REST_API_CONFIG WITH BASE_ADDRESS "https://YOUR_ACCOUNT.documents.azure.com" WITH ENABLE_CLIENT "true" WITH USE_SSL "true" WITH REQUEST_TIMEOUT '60' WITH DEFAULT_CONTENT_TYPE "application/json" ADD EVENT DocumentCreate WITH SOURCE_TOPIC "data/azure-cosmos-db/documents/create" WITH DESTINATION_TOPIC "data/azure-cosmos-db/documents/create/response" WITH METHOD "POST" WITH ENDPOINT "/dbs/{value.json.db}/colls/{value.json.coll}/docs" WITH HEADERS ADD "Accept" "application/json" ADD "Content-Type" "application/json" ADD "Authorization" "{value.json.authorization}" ADD "x-ms-date" "{value.json.x_ms_date}" ADD "x-ms-version" "2018-12-31" ADD "x-ms-documentdb-partitionkey" "{value.json.partition_key}" WITH BODY "{value.json.body}" ADD EVENT DocumentUpsert WITH SOURCE_TOPIC "data/azure-cosmos-db/documents/upsert" WITH DESTINATION_TOPIC "data/azure-cosmos-db/documents/upsert/response" WITH METHOD "POST" WITH ENDPOINT "/dbs/{value.json.db}/colls/{value.json.coll}/docs" WITH HEADERS ADD "Accept" "application/json" ADD "Content-Type" "application/json" ADD "Authorization" "{value.json.authorization}" ADD "x-ms-date" "{value.json.x_ms_date}" ADD "x-ms-version" "2018-12-31" ADD "x-ms-documentdb-partitionkey" "{value.json.partition_key}" ADD "x-ms-documentdb-is-upsert" "True" WITH BODY "{value.json.body}" ADD EVENT DocumentGet WITH SOURCE_TOPIC "data/azure-cosmos-db/documents/get" WITH DESTINATION_TOPIC "data/azure-cosmos-db/documents/get/response" WITH METHOD "GET" WITH ENDPOINT "/dbs/{value.json.db}/colls/{value.json.coll}/docs/{value.json.id}" WITH HEADERS ADD "Accept" "application/json" ADD "Authorization" "{value.json.authorization}" ADD "x-ms-date" "{value.json.x_ms_date}" ADD "x-ms-version" "2018-12-31" ADD "x-ms-documentdb-partitionkey" "{value.json.partition_key}" ADD EVENT DocumentReplace WITH SOURCE_TOPIC "data/azure-cosmos-db/documents/replace" WITH DESTINATION_TOPIC "data/azure-cosmos-db/documents/replace/response" WITH METHOD "PUT" WITH ENDPOINT "/dbs/{value.json.db}/colls/{value.json.coll}/docs/{value.json.id}" WITH HEADERS ADD "Accept" "application/json" ADD "Content-Type" "application/json" ADD "Authorization" "{value.json.authorization}" ADD "x-ms-date" "{value.json.x_ms_date}" ADD "x-ms-version" "2018-12-31" ADD "x-ms-documentdb-partitionkey" "{value.json.partition_key}" WITH BODY "{value.json.body}" ADD EVENT DocumentQuery WITH SOURCE_TOPIC "data/azure-cosmos-db/documents/query" WITH DESTINATION_TOPIC "data/azure-cosmos-db/documents/query/response" WITH METHOD "POST" WITH ENDPOINT "/dbs/{value.json.db}/colls/{value.json.coll}/docs" WITH HEADERS ADD "Accept" "application/json" ADD "Content-Type" "application/query+json" ADD "Authorization" "{value.json.authorization}" ADD "x-ms-date" "{value.json.x_ms_date}" ADD "x-ms-version" "2018-12-31" ADD "x-ms-documentdb-isquery" "True" ADD "x-ms-documentdb-query-enablecrosspartition" "True" ADD "x-ms-max-item-count" "{value.json.max_items}" WITH BODY "{value.json.body}" ADD EVENT DocumentsList WITH SOURCE_TOPIC "data/azure-cosmos-db/documents/list" WITH DESTINATION_TOPIC "data/azure-cosmos-db/documents/list/response" WITH METHOD "GET" WITH ENDPOINT "/dbs/{value.json.db}/colls/{value.json.coll}/docs" WITH HEADERS ADD "Accept" "application/json" ADD "Authorization" "{value.json.authorization}" ADD "x-ms-date" "{value.json.x_ms_date}" ADD "x-ms-version" "2018-12-31" ADD "x-ms-max-item-count" "{value.json.max_items}" ``` ## Sources * [https://learn.microsoft.com/en-us/rest/api/cosmos-db/access-control-on-cosmosdb-resources](https://learn.microsoft.com/en-us/rest/api/cosmos-db/access-control-on-cosmosdb-resources) * [https://learn.microsoft.com/en-us/rest/api/cosmos-db/create-a-document](https://learn.microsoft.com/en-us/rest/api/cosmos-db/create-a-document) * [https://learn.microsoft.com/en-us/rest/api/cosmos-db/get-a-document](https://learn.microsoft.com/en-us/rest/api/cosmos-db/get-a-document) * [https://learn.microsoft.com/en-us/rest/api/cosmos-db/replace-a-document](https://learn.microsoft.com/en-us/rest/api/cosmos-db/replace-a-document) * [https://learn.microsoft.com/en-us/rest/api/cosmos-db/querying-cosmosdb-resources-using-the-rest-api](https://learn.microsoft.com/en-us/rest/api/cosmos-db/querying-cosmosdb-resources-using-the-rest-api) * [https://learn.microsoft.com/en-us/azure/cosmos-db/how-to-setup-rbac](https://learn.microsoft.com/en-us/azure/cosmos-db/how-to-setup-rbac) # Azure Data Explorer and Fabric Eventhouse Source: https://docs.coreflux.org/latest/lot-language/routes/rest-api-apps/azure-data-explorer-and-fabric-eventhouse Client-mode REST API connector for Azure Data Explorer (Kusto) and Fabric Eventhouse — KQL query, management commands, and streaming JSON ingest from MQTT Technical Preview ## Overview A client-mode connector for a **curated** slice of the [Kusto REST API](https://learn.microsoft.com/en-us/kusto/api/rest/?view=azure-data-explorer) used by **Azure Data Explorer** and **Microsoft Fabric Eventhouse**: | Catalog connector event | Entity / path | Notes | | - | - | - | | POST query (KQL) | `POST /v1/rest/query` | Body `{"db","csl"}` — also `POST /v2/rest/query` | | (supporting) GET query | `GET /v1/rest/query?db=&csl=` | Query-string form | | POST mgmt | `POST /v1/rest/mgmt` | Management commands (`.show …`) | | POST ingest JSON | `POST /v1/rest/ingest/{db}/{table}?streamFormat=JSON&mappingName=` | **Streaming** ingest on the **engine** Cluster URI | Default `BASE_ADDRESS` is `https://YOUR_CLUSTER.YOUR_REGION.kusto.windows.net` (no trailing slash). For Fabric, use the Eventhouse **Query URI**. Topics use `data/azure-data-explorer/`. See the base guide: [REST API Connectors](/latest/lot-language/routes/rest-api-routes). > [!NOTE] > > **Auth:** `Authorization: Bearer <entra_access_token>`. Prefer Hub secrets (`GET SECRET "ADX_ACCESS_TOKEN"` or `"KUSTO_ACCESS_TOKEN"`). Token resource is typically `https://api.kusto.windows.net` or the cluster URI. > [!NOTE] > > **Streaming vs queued ingest:** `/v1/rest/ingest/...` streaming ingest uses the **engine** host (same as query). The portal Data ingestion URI (`ingest-*`) is for queued/bulk pipelines and is not the `BASE_ADDRESS` for this template. ## Basic Syntax ```lot wrap theme={"theme":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} # Coreflux REST_API template — Azure Data Explorer / Fabric Eventhouse # Auth: Authorization: Bearer {value.json.access_token} # BASE_ADDRESS: https://YOUR_CLUSTER.YOUR_REGION.kusto.windows.net DEFINE CONNECTOR AzureDataExplorerRestApi WITH TYPE REST_API ADD REST_API_CONFIG WITH BASE_ADDRESS "https://YOUR_CLUSTER.YOUR_REGION.kusto.windows.net" WITH ENABLE_CLIENT "true" WITH USE_SSL "true" WITH REQUEST_TIMEOUT '60' WITH DEFAULT_CONTENT_TYPE "application/json" ADD EVENT PostQuery WITH SOURCE_TOPIC "data/azure-data-explorer/query" WITH DESTINATION_TOPIC "data/azure-data-explorer/query/response" WITH METHOD "POST" WITH ENDPOINT "/v1/rest/query" WITH HEADERS ADD "Accept" "application/json" ADD "Content-Type" "application/json; charset=utf-8" ADD "Authorization" "Bearer {value.json.access_token}" WITH BODY "{payload}" ADD EVENT PostMgmt WITH SOURCE_TOPIC "data/azure-data-explorer/mgmt" WITH DESTINATION_TOPIC "data/azure-data-explorer/mgmt/response" WITH METHOD "POST" WITH ENDPOINT "/v1/rest/mgmt" WITH HEADERS ADD "Accept" "application/json" ADD "Content-Type" "application/json; charset=utf-8" ADD "Authorization" "Bearer {value.json.access_token}" WITH BODY "{payload}" ADD EVENT PostIngestJson WITH SOURCE_TOPIC "data/azure-data-explorer/ingest" WITH DESTINATION_TOPIC "data/azure-data-explorer/ingest/response" WITH METHOD "POST" WITH ENDPOINT "/v1/rest/ingest/{value.json.database}/{value.json.table}?streamFormat=JSON&mappingName={value.json.mapping_name}" WITH HEADERS ADD "Accept" "application/json" ADD "Content-Type" "application/json; charset=utf-8" ADD "Authorization" "Bearer {value.json.access_token}" WITH BODY "{payload}" ``` ## Auth and placeholders | Placeholder | Meaning | | - | - | | `{value.json.access_token}` | Entra ID access token → `Authorization: Bearer …` | | `{value.json.database}` / `{value.json.table}` | Ingest path segments | | `{value.json.mapping_name}` | Pre-created ingestion mapping (required for JSON) | | `{value.json.db}` / `{value.json.csl}` | GET query query-string params | | `{value.json.client_request_id}` | Optional `x-ms-client-request-id` | | `{payload}` | JSON body for query/mgmt, or raw JSON record(s) for ingest | ## Template reference Generated from the shipped broker template `azure-data-explorer-and-fabric-eventhouse` (`-connectorCode AZURE_DATA_EXPLORER_AND_FABRIC_EVENTHOUSE`). It lists every event the template defines. ### Auth Authorization: Bearer \{value.json.access\_token} (Microsoft Entra ID OAuth 2.0). Prefer GET SECRET ADX\_ACCESS\_TOKEN or KUSTO\_ACCESS\_TOKEN. Token resource typically [https://api.kusto.windows.net](https://api.kusto.windows.net) or the cluster URI. Never commit real credentials. Auth headers used by the template: `Authorization: Bearer {value.json.access_token}`. ### Template parameters | Placeholder | Type | Description | Required | | - | - | - | - | | `RouteName` | string | Name of the Azure Data Explorer and Fabric Eventhouse connector | no | | `BaseUrl` | string | Azure Data Explorer and Fabric Eventhouse base URL without trailing slash (e.g. [https://YOUR\_CLUSTER.YOUR\_REGION.kusto.windows.net](https://YOUR_CLUSTER.YOUR_REGION.kusto.windows.net)) | yes | `BASE_ADDRESS` default: `https://YOUR_CLUSTER.YOUR_REGION.kusto.windows.net`. ### Events | Event | Method | Endpoint | Request topic | Response topic | Payload fields | | - | - | - | - | - | - | | `PostQuery` | POST | `/v1/rest/query` | `data/azure-data-explorer/query` | `data/azure-data-explorer/query/response` | `access_token`, `body`, `client_request_id` | | `PostQueryV2` | POST | `/v2/rest/query` | `data/azure-data-explorer/query-v2` | `data/azure-data-explorer/query-v2/response` | `access_token`, `body`, `client_request_id` | | `GetQuery` | GET | `/v1/rest/query?db={value.json.db}&csl={value.json.csl}` | `data/azure-data-explorer/query-get` | `data/azure-data-explorer/query-get/response` | `access_token`, `csl`, `db` | | `PostMgmt` | POST | `/v1/rest/mgmt` | `data/azure-data-explorer/mgmt` | `data/azure-data-explorer/mgmt/response` | `access_token`, `body`, `client_request_id` | | `PostIngestJson` | POST | `/v1/rest/ingest/{value.json.database}/{value.json.table}?streamFormat=JSON&mappingName={value.json.mapping_name}` | `data/azure-data-explorer/ingest` | `data/azure-data-explorer/ingest/response` | `access_token`, `body`, `client_request_id`, `database`, `mapping_name`, `table` | ### Deploy Get the template card (JSON with `template` and `metadata`) from the broker, then deploy the connector: ```bash theme={"theme":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} mosquitto_pub -t '$SYS/Coreflux/Command' -m "-connectorCode AZURE_DATA_EXPLORER_AND_FABRIC_EVENTHOUSE" ``` The deployable connector from the REST API app, with `BASE_ADDRESS` already filled in: ```lot theme={"theme":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} DEFINE CONNECTOR AzureDataExplorerRestApi WITH TYPE REST_API ADD REST_API_CONFIG WITH BASE_ADDRESS "https://YOUR_CLUSTER.YOUR_REGION.kusto.windows.net" WITH ENABLE_CLIENT "true" WITH USE_SSL "true" WITH REQUEST_TIMEOUT '60' WITH DEFAULT_CONTENT_TYPE "application/json" ADD EVENT PostQuery WITH SOURCE_TOPIC "data/azure-data-explorer/query" WITH DESTINATION_TOPIC "data/azure-data-explorer/query/response" WITH METHOD "POST" WITH ENDPOINT "/v1/rest/query" WITH HEADERS ADD "Accept" "application/json" ADD "Content-Type" "application/json; charset=utf-8" ADD "Authorization" "Bearer {value.json.access_token}" ADD "x-ms-app" "Coreflux" ADD "x-ms-client-request-id" "{value.json.client_request_id}" WITH BODY "{value.json.body}" ADD EVENT PostQueryV2 WITH SOURCE_TOPIC "data/azure-data-explorer/query-v2" WITH DESTINATION_TOPIC "data/azure-data-explorer/query-v2/response" WITH METHOD "POST" WITH ENDPOINT "/v2/rest/query" WITH HEADERS ADD "Accept" "application/json" ADD "Content-Type" "application/json; charset=utf-8" ADD "Authorization" "Bearer {value.json.access_token}" ADD "x-ms-app" "Coreflux" ADD "x-ms-client-request-id" "{value.json.client_request_id}" WITH BODY "{value.json.body}" ADD EVENT GetQuery WITH SOURCE_TOPIC "data/azure-data-explorer/query-get" WITH DESTINATION_TOPIC "data/azure-data-explorer/query-get/response" WITH METHOD "GET" WITH ENDPOINT "/v1/rest/query?db={value.json.db}&csl={value.json.csl}" WITH HEADERS ADD "Accept" "application/json" ADD "Authorization" "Bearer {value.json.access_token}" ADD "x-ms-app" "Coreflux" ADD EVENT PostMgmt WITH SOURCE_TOPIC "data/azure-data-explorer/mgmt" WITH DESTINATION_TOPIC "data/azure-data-explorer/mgmt/response" WITH METHOD "POST" WITH ENDPOINT "/v1/rest/mgmt" WITH HEADERS ADD "Accept" "application/json" ADD "Content-Type" "application/json; charset=utf-8" ADD "Authorization" "Bearer {value.json.access_token}" ADD "x-ms-app" "Coreflux" ADD "x-ms-client-request-id" "{value.json.client_request_id}" WITH BODY "{value.json.body}" ADD EVENT PostIngestJson WITH SOURCE_TOPIC "data/azure-data-explorer/ingest" WITH DESTINATION_TOPIC "data/azure-data-explorer/ingest/response" WITH METHOD "POST" WITH ENDPOINT "/v1/rest/ingest/{value.json.database}/{value.json.table}?streamFormat=JSON&mappingName={value.json.mapping_name}" WITH HEADERS ADD "Accept" "application/json" ADD "Content-Type" "application/json; charset=utf-8" ADD "Authorization" "Bearer {value.json.access_token}" ADD "x-ms-app" "Coreflux" ADD "x-ms-client-request-id" "{value.json.client_request_id}" WITH BODY "{value.json.body}" ``` Deploy it with `-addConnector` (do not include `ADD METADATA`). ### Usage example Call `PostQuery` by publishing a JSON payload to its request topic, and read the reply on the response topic: ```bash theme={"theme":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} mosquitto_sub -t 'data/azure-data-explorer/query/response' & mosquitto_pub -t 'data/azure-data-explorer/query' -m '{"access_token": "", "body": {}, "client_request_id": ""}' ``` ## Related * [REST API Connectors](/latest/lot-language/routes/rest-api-routes) * [REST API Apps catalog](/latest/lot-language/routes/rest-api-apps) * [Kusto REST API overview](https://learn.microsoft.com/en-us/kusto/api/rest/?view=azure-data-explorer) * [Query / management request](https://learn.microsoft.com/en-us/kusto/api/rest/request) * [Streaming ingestion](https://learn.microsoft.com/en-us/kusto/api/rest/streaming-ingest) * [Authentication](https://learn.microsoft.com/en-us/kusto/api/rest/authentication) * [Fabric Kusto REST](https://learn.microsoft.com/en-us/kusto/api/rest/?view=microsoft-fabric) # Azure DevOps Source: https://docs.coreflux.org/latest/lot-language/routes/rest-api-apps/azure-devops Connect Coreflux LoT to Azure DevOps to create and update work items from plant alarms, run WIQL queries, start pipelines and builds and add service hooks Technical Preview ## Overview A client-mode connector for the [Azure DevOps Services REST API 7.1](https://learn.microsoft.com/en-us/rest/api/azure/devops/?view=azure-devops-rest-7.1) that connects the plant floor to the engineering backlog and to edge CI/CD. A typical flow creates a Bug or Issue when the broker sees a recurring alarm, adds the readings as a comment, closes the item when the machine recovers, and finds open items with WIQL. On the delivery side it starts a YAML pipeline run or queues a build (for example an edge firmware build or a gateway configuration deployment), polls the run, reads its timeline, logs and artifacts, and cancels or tags builds. Service hook subscriptions send work item, build and pipeline events back to a Coreflux server connector. One connector carries 43 events (32 GET, 8 POST, 2 PATCH, 1 PUT; 9 writes, no DELETE). Topics use `devops/azure-devops//`; each response is published on `/response`. `WiqlQuery` and `WorkItemsBatchGet` are read-only POSTs. ## Setup 1. Create an **organization-scoped** personal access token (User settings > Personal access tokens) with only the scopes you need: **Work Items (Read & write)**, **Build (Read & execute)**, **Project and Team (Read)**, and for service hook subscriptions the scopes of the events you subscribe to. Global PATs stop working on December 1, 2026. For unattended use, prefer a Microsoft Entra service principal or managed identity added to the organization as a user. 2. Build the header value once and keep it in a broker secret (for example `AZURE_DEVOPS_AUTHORIZATION`). Inject it in a LoT action with `GET SECRET`. Never retain topics that carry it. 3. Read the project once with `WorkItemTypesList`, `FieldsList` and `ClassificationNodesList` to get the work item types, field reference names and area paths you will write. ## Authentication Every event sends the payload field `authorization` unchanged as the `Authorization` header: | Credential | `authorization` value | | - | - | | Personal access token | `Basic ` + base64 of `:` + PAT (empty user name) | | Microsoft Entra ID (service principal, managed identity, user) | `Bearer ` + access token for the Azure DevOps resource `499b84ac-1321-427f-aa17-267ca6975798` (URI `https://app.vssps.visualstudio.com`), scope `.../.default` | ```json theme={"theme":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} {"authorization": "Basic OnlvdXItcGF0LWhlcmU=", "organization": "contoso", "project": "Plant%20Ops"} ``` Every event also sends `X-TFS-FedAuthRedirect: Suppress`. Without it, a missing, bad or expired credential gets a `302` redirect to the sign-in page (`203` HTML if the client follows it) instead of a `401` JSON error. > [!WARNING] > > On an organization with **public** projects, Azure DevOps treats a bad credential as anonymous: public reads still answer `200` and only private data or writes answer `401`. If reads must come from a real identity, call `ConnectionData` first and check that `authenticatedUser` is not `Anonymous`. ## Organization, project and api-version The base address is `https://dev.azure.com`; each endpoint starts with `/{organization}`, and project-scoped events continue with `/{project}`. Put both in the payload (URL-encode project names with spaces, for example `Plant%20Ops`). Every endpoint pins its `api-version`: `7.1` (the latest GA version), `7.1-preview.4` for work item comments and `7.1-preview.1` for `ConnectionData`. Preview resources answer `400` to plain `7.1`. Azure DevOps Server (on-premises) is not covered by this template. It uses `https:////` and older API versions. ## Query parameters and bodies Optional query parameters go in `query`, URL-encoded and **starting with `&`**. The connector appends `query` directly after the pinned `api-version`, for example `&statusFilter=inProgress&$top=20`. Leave `query` empty for defaults. Do not send a lone `&`: an empty parameter makes some endpoints fail (`build/builds` answers `400`, `build/definitions` answers `500`). `WorkItemsList` and `PipelineRunArtifactGet` need their query (`&ids=...`, `&artifactName=...`). Writes send `body`. Work item create and update take a **JSON Patch** array and are sent as `application/json-patch+json`; everything else is `application/json`. ## Examples Create a Bug when an alarm repeats (`devops/azure-devops/work-items/create`, `type` = `Bug`; use `&validateOnly=true` in `query` for a dry run): ```json theme={"theme":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} {"authorization": "", "organization": "contoso", "project": "Plant%20Ops", "type": "Bug", "query": "", "body": [{"op": "add", "path": "/fields/System.Title", "value": "Press 7 over-temperature"}, {"op": "add", "path": "/fields/System.AreaPath", "value": "Plant Ops\\Line 2"}, {"op": "add", "path": "/fields/Microsoft.VSTS.TCM.ReproSteps", "value": "94.2 C at 14:02, limit 85 C"}, {"op": "add", "path": "/fields/System.Tags", "value": "coreflux; line-2"}]} ``` The response holds the new work item `id` and `rev`. Keep the id to update it later. Close it when the press recovers, with a link to the trend panel (`devops/azure-devops/work-items/update`): ```json theme={"theme":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} {"authorization": "", "organization": "contoso", "project": "Plant%20Ops", "id": "4711", "query": "", "body": [{"op": "add", "path": "/fields/System.State", "value": "Done"}, {"op": "add", "path": "/fields/System.History", "value": "Back to 71 C at 15:10"}, {"op": "add", "path": "/relations/-", "value": {"rel": "Hyperlink", "url": "https://broker.example.com/panels/press7"}}]} ``` Find open items raised by the broker (`devops/azure-devops/wiql/query`, then `devops/azure-devops/work-items/batch-get` with the returned ids): ```json theme={"theme":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} {"authorization": "", "organization": "contoso", "project": "Plant%20Ops", "query": "&$top=50", "body": {"query": "SELECT [System.Id] FROM WorkItems WHERE [System.TeamProject] = @project AND [System.Tags] CONTAINS 'coreflux' AND [System.State] <> 'Done' ORDER BY [System.ChangedDate] DESC"}} ``` Start an edge deployment pipeline (`devops/azure-devops/pipelines/runs/run`), then poll `devops/azure-devops/pipelines/runs/get` with the returned run id: ```json theme={"theme":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} {"authorization": "", "organization": "contoso", "project": "Plant%20Ops", "pipeline": "12", "query": "", "body": {"resources": {"repositories": {"self": {"refName": "refs/heads/main"}}}, "templateParameters": {"line": "2", "target": "edge-gw-07"}}} ``` List running builds (`devops/azure-devops/builds/list`): ```json theme={"theme":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} {"authorization": "", "organization": "contoso", "project": "Plant%20Ops", "query": "&statusFilter=inProgress&$top=20"} ``` Send failed builds to a Coreflux server connector (`devops/azure-devops/hooks/subscriptions/create`; organization-level, the project goes in `publisherInputs.projectId`): ```json theme={"theme":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} {"authorization": "", "organization": "contoso", "body": {"publisherId": "tfs", "eventType": "build.complete", "resourceVersion": "1.0", "consumerId": "webHooks", "consumerActionId": "httpRequest", "publisherInputs": {"projectId": "", "buildStatus": "failed"}, "consumerInputs": {"url": "https://broker.example.com/azure-devops/events", "httpHeaders": "X-Coreflux-Key:", "resourceDetailsToSend": "minimal"}}} ``` > [!WARNING] > > Service hooks post to a public **HTTPS** URL; they cannot target localhost or private address ranges. The receiving side is a separate `ENABLE_SERVER` REST\_API connector, which is not part of this template. Protect it with `basicAuthCredentials` or a shared header in `httpHeaders` (header values are visible to anyone who can read the subscription). Other useful events: `workitem.created`, `workitem.updated` (publisher `tfs`) and `ms.vss-pipelines.run-state-changed-event` (publisher `pipelines`). List them with `HookEventTypesList`. Check deliveries with `HookNotificationsList`. ## Events | Event | Method | Endpoint | Source topic | Write | Notes | | - | - | - | - | - | - | | `ConnectionData` | `GET` | `/{organization}/_apis/connectionData?api-version=7.1-preview.1` | `devops/azure-devops/connection-data/get` | | Smoke: who the token is (authenticatedUser id / displayName) and the organization instance id. Location service, not in the published specs; needs a -preview api-version (7.1 alone answers 400). Public organizations answer it anonymously. | | `ProjectsList` | `GET` | `/{organization}/_apis/projects?api-version=7.1{query}` | `devops/azure-devops/projects/list` | | Projects the token can see. query e.g. \&stateFilter=wellFormed&\$top=100 (or empty; page with continuationToken). | | `ProjectGet` | `GET` | `/{organization}/_apis/projects/{project}?api-version=7.1{query}` | `devops/azure-devops/projects/get` | | One project by name or id. query e.g. \&includeCapabilities=true (process template, source control) or empty. | | `TeamsList` | `GET` | `/{organization}/_apis/projects/{project}/teams?api-version=7.1{query}` | `devops/azure-devops/teams/list` | | Teams in the project (area-path owners for routing work items). query e.g. &\$top=100 (or empty). | | `WorkItemCreate` | `POST` | `/{organization}/{project}/_apis/wit/workitems/${type}?api-version=7.1{query}` | `devops/azure-devops/work-items/create` | Yes | Catalog 'POST wit/workitems/$Bug'. Create a Bug / Issue / Task from an alarm with a JSON Patch document (Content-Type application/json-patch+json). type: Bug, Task, Issue, User%20Story, ... (URL-encoded, the $ is in the template). body: \[\{"op":"add","path":"/fields/System.Title","value":"Press 7 over-temperature"},\{"op":"add","path":"/fields/System.AreaPath","value":"Plant\Line 2"},\{"op":"add","path":"/fields/Microsoft.VSTS.TCM.ReproSteps","value":"94.2 C at 14:02"},\{"op":"add","path":"/fields/System.Tags","value":"coreflux; line-2"}]. query e.g. \&validateOnly=true for a dry run, \&suppressNotifications=true, &\$expand=fields (or empty). | | `WorkItemGet` | `GET` | `/{organization}/{project}/_apis/wit/workitems/{id}?api-version=7.1{query}` | `devops/azure-devops/work-items/get` | | One work item. query e.g. \&fields=System.State,System.AssignedTo or &$expand=relations (or empty; fields and $expand cannot be combined). | | `WorkItemsList` | `GET` | `/{organization}/{project}/_apis/wit/workitems?api-version=7.1{query}` | `devops/azure-devops/work-items/list` | | Up to 200 work items by id. query (required): \&ids=101,102,103 plus e.g. \&fields=System.Title,System.State\&errorPolicy=omit. | | `WorkItemsBatchGet` | `POST` | `/{organization}/{project}/_apis/wit/workitemsbatch?api-version=7.1` | `devops/azure-devops/work-items/batch-get` | | Read-only POST: up to 200 work items by id with chosen fields (use after WiqlQuery). body: \{"ids":\[101,102],"fields":\["System.Id","System.Title","System.State"],"errorPolicy":"omit"}. | | `WorkItemUpdate` | `PATCH` | `/{organization}/{project}/_apis/wit/workitems/{id}?api-version=7.1{query}` | `devops/azure-devops/work-items/update` | Yes | Update fields, state, assignee, links with a JSON Patch document (application/json-patch+json). body: \[\{"op":"test","path":"/rev","value":3},\{"op":"add","path":"/fields/System.State","value":"Done"},\{"op":"add","path":"/fields/System.History","value":"Press 7 back to 71 C"},\{"op":"add","path":"/relations/-","value":\{"rel":"Hyperlink","url":"[https://broker.example.com/panels/press7](https://broker.example.com/panels/press7)"}}]. query e.g. \&suppressNotifications=true (or empty). | | `WorkItemUpdatesList` | `GET` | `/{organization}/{project}/_apis/wit/workItems/{id}/updates?api-version=7.1{query}` | `devops/azure-devops/work-items/updates/list` | | Change history of a work item (who changed which field). query e.g. &$top=50&$skip=0 (or empty). | | `WiqlQuery` | `POST` | `/{organization}/{project}/_apis/wit/wiql?api-version=7.1{query}` | `devops/azure-devops/wiql/query` | | Read-only POST: run a WIQL query, returns work item ids (then WorkItemsBatchGet). Team segment omitted (optional in the spec). body: \{"query":"SELECT \[System.Id] FROM WorkItems WHERE \[System.TeamProject] = @project AND \[System.Tags] CONTAINS 'coreflux' AND \[System.State] \<> 'Done' ORDER BY \[System.ChangedDate] DESC"}. query e.g. &\$top=50 (or empty). | | `WiqlQueryById` | `GET` | `/{organization}/{project}/_apis/wit/wiql/{wiql_id}?api-version=7.1{query}` | `devops/azure-devops/wiql/run-saved` | | Run a saved query by its id (wiql\_id, from QueriesList). query e.g. &\$top=50 (or empty). | | `WorkItemCommentAdd` | `POST` | `/{organization}/{project}/_apis/wit/workItems/{id}/comments?api-version=7.1-preview.4` | `devops/azure-devops/work-items/comments/add` | Yes | Add a discussion comment (preview resource: api-version 7.1-preview\.4; plain 7.1 answers 400). body: \{"text":"Bearing replaced, line restarted at 15:10"}. | | `WorkItemCommentsList` | `GET` | `/{organization}/{project}/_apis/wit/workItems/{id}/comments?api-version=7.1-preview.4{query}` | `devops/azure-devops/work-items/comments/list` | | Comments on a work item (api-version 7.1-preview\.4). query e.g. &\$top=20\&order=desc (or \&ids=1,2 for specific comments; or empty). | | `WorkItemTypesList` | `GET` | `/{organization}/{project}/_apis/wit/workitemtypes?api-version=7.1` | `devops/azure-devops/work-item-types/list` | | Work item types of the project's process (Bug, Task, Issue, ...) and their fields. | | `WorkItemTypeStatesList` | `GET` | `/{organization}/{project}/_apis/wit/workitemtypes/{type}/states?api-version=7.1` | `devops/azure-devops/work-item-types/states/list` | | Valid states of one work item type (type e.g. Bug), to map machine states to work item states. | | `FieldsList` | `GET` | `/{organization}/{project}/_apis/wit/fields?api-version=7.1{query}` | `devops/azure-devops/fields/list` | | Field reference names (System.Title, Microsoft.VSTS.Common.Priority, Custom.\*) for JSON Patch paths. query e.g. &\$expand=extensionFields (or empty). | | `ClassificationNodesList` | `GET` | `/{organization}/{project}/_apis/wit/classificationnodes?api-version=7.1{query}` | `devops/azure-devops/classification-nodes/list` | | Area and iteration paths (e.g. Plant\Line 2) to connector work items per line or cell. query e.g. &\$depth=2. | | `TagsList` | `GET` | `/{organization}/{project}/_apis/wit/tags?api-version=7.1` | `devops/azure-devops/tags/list` | | Work item tags defined in the project. | | `QueriesList` | `GET` | `/{organization}/{project}/_apis/wit/queries?api-version=7.1{query}` | `devops/azure-devops/queries/list` | | Saved queries and folders (ids for WiqlQueryById). query e.g. &$depth=1&$expand=wiql (or empty). | | `PipelinesList` | `GET` | `/{organization}/{project}/_apis/pipelines?api-version=7.1{query}` | `devops/azure-devops/pipelines/list` | | Pipelines in the project. query e.g. &\$top=50\&orderBy=name%20asc (or empty). | | `PipelineGet` | `GET` | `/{organization}/{project}/_apis/pipelines/{pipeline}?api-version=7.1{query}` | `devops/azure-devops/pipelines/get` | | One pipeline (pipeline = id). query e.g. \&pipelineVersion=4 (or empty). | | `PipelineRun` | `POST` | `/{organization}/{project}/_apis/pipelines/{pipeline}/runs?api-version=7.1{query}` | `devops/azure-devops/pipelines/runs/run` | Yes | Catalog 'POST pipelines/\{id}/runs'. Start a pipeline run, e.g. edge firmware build or deployment to a line gateway. body: \{"resources":\{"repositories":\{"self":\{"refName":"refs/heads/main"}}},"templateParameters":\{"line":"2","target":"edge-gw-07"},"variables":\{"TRIGGERED\_BY":\{"value":"coreflux","isSecret":false}}}; add "previewRun":true to only expand the YAML. query e.g. \&pipelineVersion=4 (or empty). | | `PipelineRunsList` | `GET` | `/{organization}/{project}/_apis/pipelines/{pipeline}/runs?api-version=7.1` | `devops/azure-devops/pipelines/runs/list` | | Latest runs of a pipeline (up to 10000, newest first; no query parameters). | | `PipelineRunGet` | `GET` | `/{organization}/{project}/_apis/pipelines/{pipeline}/runs/{run}?api-version=7.1` | `devops/azure-devops/pipelines/runs/get` | | One run: state (inProgress, completed) and result (succeeded, failed, canceled). Poll after PipelineRun. | | `PipelineRunLogsList` | `GET` | `/{organization}/{project}/_apis/pipelines/{pipeline}/runs/{run}/logs?api-version=7.1{query}` | `devops/azure-devops/pipelines/runs/logs/list` | | Logs of a run. query e.g. &\$expand=signedContent (time-limited download URLs) or empty. | | `PipelineRunArtifactGet` | `GET` | `/{organization}/{project}/_apis/pipelines/{pipeline}/runs/{run}/artifacts?api-version=7.1{query}` | `devops/azure-devops/pipelines/runs/artifacts/get` | | One published artifact of a run, e.g. a firmware image. query (required): \&artifactName=firmware&\$expand=signedContent. | | `BuildsList` | `GET` | `/{organization}/{project}/_apis/build/builds?api-version=7.1{query}` | `devops/azure-devops/builds/list` | | Catalog 'GET build/builds?statusFilter='. Builds and pipeline runs. query e.g. \&statusFilter=inProgress or \&resultFilter=failed\&minTime=2026-10-05T06:00:00Z\&definitions=12&\$top=20\&queryOrder=finishTimeDescending. | | `BuildQueue` | `POST` | `/{organization}/{project}/_apis/build/builds?api-version=7.1{query}` | `devops/azure-devops/builds/queue` | Yes | Queue a build of a definition (classic or YAML). body: \{"definition":\{"id":12},"sourceBranch":"refs/heads/main","templateParameters":\{"line":"2"},"parameters":"\{"system.debug":"false"}","tags":\["coreflux"]}. query e.g. \&ignoreWarnings=true (or empty). | | `BuildGet` | `GET` | `/{organization}/{project}/_apis/build/builds/{build}?api-version=7.1{query}` | `devops/azure-devops/builds/get` | | One build: status, result, sourceVersion, timings. query e.g. \&propertyFilters=... (or empty). | | `BuildUpdate` | `PATCH` | `/{organization}/{project}/_apis/build/builds/{build}?api-version=7.1{query}` | `devops/azure-devops/builds/update` | Yes | Cancel a running build: body \{"status":"cancelling"}. query: \&retry=true (with body \{}) re-runs the failed jobs of a finished build; or empty. (Keep-forever is a retention lease in 7.1, not a Build field; not packaged.) | | `BuildTagsAdd` | `POST` | `/{organization}/{project}/_apis/build/builds/{build}/tags?api-version=7.1` | `devops/azure-devops/builds/tags/add` | Yes | Tag a build, e.g. with the line or gateway it was deployed to. body: \["line-2","deployed-edge-gw-07"]. | | `BuildTimelineGet` | `GET` | `/{organization}/{project}/_apis/build/builds/{build}/timeline?api-version=7.1{query}` | `devops/azure-devops/builds/timeline/get` | | Stages, jobs and tasks of a build with state, result and error counts (timelineId omitted = the build's own timeline). query e.g. \&changeId=5 for incremental polling (or empty). | | `BuildLogsList` | `GET` | `/{organization}/{project}/_apis/build/builds/{build}/logs?api-version=7.1` | `devops/azure-devops/builds/logs/list` | | Log ids and line counts of a build. | | `BuildWorkItemsList` | `GET` | `/{organization}/{project}/_apis/build/builds/{build}/workitems?api-version=7.1{query}` | `devops/azure-devops/builds/work-items/list` | | Work items linked to a build (what a firmware release fixes). query e.g. &\$top=50 (or empty). | | `BuildDefinitionsList` | `GET` | `/{organization}/{project}/_apis/build/definitions?api-version=7.1{query}` | `devops/azure-devops/builds/definitions/list` | | Build definitions (ids for BuildQueue / BuildsList). query e.g. \&name=edge-firmware\*\&includeLatestBuilds=true&\$top=50 (or empty). | | `HookPublishersList` | `GET` | `/{organization}/_apis/hooks/publishers?api-version=7.1` | `devops/azure-devops/hooks/publishers/list` | | Event publishers (tfs = work items, builds, code; pipelines = YAML pipeline runs, stages, approvals). | | `HookEventTypesList` | `GET` | `/{organization}/_apis/hooks/publishers/{publisher}/eventtypes?api-version=7.1` | `devops/azure-devops/hooks/event-types/list` | | Event types of one publisher (publisher e.g. tfs: workitem.created, workitem.updated, build.complete; pipelines: ms.vss-pipelines.run-state-changed-event) with their filter inputs. | | `HookSubscriptionsList` | `GET` | `/{organization}/_apis/hooks/subscriptions?api-version=7.1{query}` | `devops/azure-devops/hooks/subscriptions/list` | | Service hook subscriptions. query e.g. \&consumerId=webHooks\&eventType=build.complete (or empty). | | `HookSubscriptionCreate` | `POST` | `/{organization}/_apis/hooks/subscriptions?api-version=7.1` | `devops/azure-devops/hooks/subscriptions/create` | Yes | Send Azure DevOps events to a Coreflux server connector (Web Hooks consumer). body: \{"publisherId":"tfs","eventType":"build.complete","resourceVersion":"1.0","consumerId":"webHooks","consumerActionId":"httpRequest","publisherInputs":\{"projectId":"\","buildStatus":"failed"},"consumerInputs":\{"url":"[https://broker.example.com/azure-devops/events","httpHeaders":"X-Coreflux-Key:\<shared](https://broker.example.com/azure-devops/events","httpHeaders":"X-Coreflux-Key:\<shared) secret>","resourceDetailsToSend":"minimal"}}. | | `HookSubscriptionGet` | `GET` | `/{organization}/_apis/hooks/subscriptions/{subscription}?api-version=7.1` | `devops/azure-devops/hooks/subscriptions/get` | | One subscription (status enabled / onProbation / disabledBySystem ...). | | `HookSubscriptionReplace` | `PUT` | `/{organization}/_apis/hooks/subscriptions/{subscription}?api-version=7.1` | `devops/azure-devops/hooks/subscriptions/replace` | Yes | Replace a subscription, e.g. re-enable it after the broker was offline (send the full subscription from HookSubscriptionGet with "status":"enabled"). | | `HookNotificationsList` | `GET` | `/{organization}/_apis/hooks/subscriptions/{subscription}/notifications?api-version=7.1{query}` | `devops/azure-devops/hooks/notifications/list` | | Recent deliveries of a subscription with result and HTTP response (debug the broker endpoint). query e.g. \&maxResults=20\&result=failed (or empty). | ## Limits Azure DevOps meters load in throughput units (TSTUs): 200 TSTUs per identity in any sliding five-minute window, and the same per pipeline. Above that, requests are delayed or rejected with `429`. Honor `Retry-After` and watch the `X-RateLimit-*` response headers. Batch reads with `WorkItemsBatchGet` (up to 200 ids) instead of one `WorkItemGet` per item, and poll run state at a sensible interval. ## Not packaged * All DELETE endpoints (work items, comments, builds, subscriptions) on purpose. Close work items by state instead. * Releases (classic release pipelines). They live on a different host (`vsrm.dev.azure.com`) and need a second connector. * Work item attachment upload (binary), Git repositories and pull requests, test plans, artifacts feeds, wikis, approvals and checks, agent pools and service connections. * Build retention (keep forever is a retention lease in 7.1, not a build field). * The incoming service hook direction: a separate `ENABLE_SERVER` connector. * Azure DevOps Server (on-premises) collections. # Azure Digital Twins Source: https://docs.coreflux.org/latest/lot-language/routes/rest-api-apps/azure-digital-twins LoT connectors for Azure Digital Twins: patch and upsert twins and relationships, query the twin graph, send telemetry and get Entra ID tokens Technical Preview ## Overview Two client-mode connectors for [Azure Digital Twins](https://learn.microsoft.com/en-us/azure/digital-twins/overview): * **AzureDigitalTwins** calls your instance's data plane (`https://.api..digitaltwins.azure.net`, api-version `2023-10-31`). Use it to keep a twin graph in step with the plant: patch twin properties from UNS telemetry, create or replace twins and relationships, query the graph, and publish telemetry on a twin so ADT event routes forward it to Event Grid, Event Hubs or Service Bus. * **AzureDigitalTwinsEntraToken** calls Microsoft Entra ID (`login.microsoftonline.com`) with the client-credentials flow and returns an access token for the `https://digitaltwins.azure.net` audience. | Event | Request | Notes | | - | - | - | | UpdateTwin | `PATCH /digitaltwins/{twin_id}` | JSON Patch array, `Content-Type: application/json-patch+json`; returns `204` | | GetTwin | `GET /digitaltwins/{twin_id}` | Twin with `$dtId`, `$etag`, `$metadata` and properties | | UpsertTwin | `PUT /digitaltwins/{twin_id}` | Create or replace; body needs `$metadata.$model` | | QueryTwins | `POST /query` | ADT query language; `{"query": "..."}` or `{"continuationToken": "..."}` | | SendTelemetry | `POST /digitaltwins/{twin_id}/telemetry` | `Message-Id` header required (unique per message); returns `204` | | ListRelationships | `GET /digitaltwins/{twin_id}/relationships` | Outgoing relationships | | UpsertRelationship | `PUT /digitaltwins/{twin_id}/relationships/{relationship_id}` | Body: `$targetId`, `$relationshipName` | | ListModels | `GET /models` | DTDL models in the instance | | GetModel | `GET /models/{model_id}?includeModelDefinition=true` | DTDL definition of one model | | ListEventRoutes | `GET /eventroutes` | Where twin changes and telemetry are forwarded | | GetAccessToken | `POST /{tenant_id}/oauth2/v2.0/token` | Client credentials, scope `https://digitaltwins.azure.net/.default` | Topics use `cloud-iot/azure-digital-twins//`; replies land on `/response`. Before deploying, replace `YOUR_INSTANCE.api.YOUR_REGION` in the `AzureDigitalTwins` connector with your instance host name, shown as **Host name** on the instance overview in the Azure portal or by `az dt show`, for example `plant-porto.api.weu.digitaltwins.azure.net`. > [!WARNING] > > Never commit a client secret or access token. Store the tenant id, client id and client secret as broker secrets and map them into payloads with `GET SECRET`. ## Authentication Every data-plane event sends a **Microsoft Entra ID access token** as `Authorization: Bearer `. The token must be issued for the Azure Digital Twins audience `https://digitaltwins.azure.net`. 1. Register an app in Microsoft Entra ID (or use a managed identity if the broker runs on Azure) and create a client secret. 2. On the Azure Digital Twins instance, assign that identity **Azure Digital Twins Data Owner** for read and write, or **Azure Digital Twins Data Reader** for read only. Owning the Azure subscription is not enough; the data-plane role is required. 3. Publish `tenant_id`, `client_id` and `client_secret` to `cloud-iot/azure-digital-twins/token/get`. The reply carries `access_token` and `expires_in` (about one hour). The client secret must be URL-encoded because it goes into a form body. 4. Pass `access_token` on every data-plane payload. Refresh it before it expires; the helper actions below do this every 45 minutes. A missing or expired token returns `401`. A valid token without the data-plane role returns `403`. ## Basic Syntax ```lot theme={"theme":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} DEFINE CONNECTOR AzureDigitalTwins WITH TYPE REST_API ADD REST_API_CONFIG WITH BASE_ADDRESS "https://YOUR_INSTANCE.api.YOUR_REGION.digitaltwins.azure.net" WITH ENABLE_CLIENT "true" WITH USE_SSL "true" WITH REQUEST_TIMEOUT '30' WITH DEFAULT_CONTENT_TYPE "application/json" ADD EVENT UpdateTwin WITH SOURCE_TOPIC "cloud-iot/azure-digital-twins/twin/update" WITH DESTINATION_TOPIC "cloud-iot/azure-digital-twins/twin/update/response" WITH METHOD "PATCH" WITH ENDPOINT "/digitaltwins/{value.json.twin_id}?api-version=2023-10-31" WITH HEADERS ADD "Accept" "application/json" ADD "Content-Type" "application/json-patch+json" ADD "Authorization" "Bearer {value.json.access_token}" WITH BODY "{value.json.body}" ADD EVENT GetTwin WITH SOURCE_TOPIC "cloud-iot/azure-digital-twins/twin/get" WITH DESTINATION_TOPIC "cloud-iot/azure-digital-twins/twin/get/response" WITH METHOD "GET" WITH ENDPOINT "/digitaltwins/{value.json.twin_id}?api-version=2023-10-31" WITH HEADERS ADD "Accept" "application/json" ADD "Authorization" "Bearer {value.json.access_token}" ADD EVENT UpsertTwin WITH SOURCE_TOPIC "cloud-iot/azure-digital-twins/twin/upsert" WITH DESTINATION_TOPIC "cloud-iot/azure-digital-twins/twin/upsert/response" WITH METHOD "PUT" WITH ENDPOINT "/digitaltwins/{value.json.twin_id}?api-version=2023-10-31" WITH HEADERS ADD "Accept" "application/json" ADD "Content-Type" "application/json" ADD "Authorization" "Bearer {value.json.access_token}" WITH BODY "{value.json.body}" ADD EVENT QueryTwins WITH SOURCE_TOPIC "cloud-iot/azure-digital-twins/query/run" WITH DESTINATION_TOPIC "cloud-iot/azure-digital-twins/query/run/response" WITH METHOD "POST" WITH ENDPOINT "/query?api-version=2023-10-31" WITH HEADERS ADD "Accept" "application/json" ADD "Content-Type" "application/json" ADD "Authorization" "Bearer {value.json.access_token}" WITH BODY "{value.json.body}" ADD EVENT SendTelemetry WITH SOURCE_TOPIC "cloud-iot/azure-digital-twins/telemetry/send" WITH DESTINATION_TOPIC "cloud-iot/azure-digital-twins/telemetry/send/response" WITH METHOD "POST" WITH ENDPOINT "/digitaltwins/{value.json.twin_id}/telemetry?api-version=2023-10-31" WITH HEADERS ADD "Accept" "application/json" ADD "Content-Type" "application/json" ADD "Message-Id" "{value.json.message_id}" ADD "Authorization" "Bearer {value.json.access_token}" WITH BODY "{value.json.body}" ADD EVENT ListRelationships WITH SOURCE_TOPIC "cloud-iot/azure-digital-twins/relationship/list" WITH DESTINATION_TOPIC "cloud-iot/azure-digital-twins/relationship/list/response" WITH METHOD "GET" WITH ENDPOINT "/digitaltwins/{value.json.twin_id}/relationships?api-version=2023-10-31" WITH HEADERS ADD "Accept" "application/json" ADD "Authorization" "Bearer {value.json.access_token}" ADD EVENT UpsertRelationship WITH SOURCE_TOPIC "cloud-iot/azure-digital-twins/relationship/upsert" WITH DESTINATION_TOPIC "cloud-iot/azure-digital-twins/relationship/upsert/response" WITH METHOD "PUT" WITH ENDPOINT "/digitaltwins/{value.json.twin_id}/relationships/{value.json.relationship_id}?api-version=2023-10-31" WITH HEADERS ADD "Accept" "application/json" ADD "Content-Type" "application/json" ADD "Authorization" "Bearer {value.json.access_token}" WITH BODY "{value.json.body}" ADD EVENT ListModels WITH SOURCE_TOPIC "cloud-iot/azure-digital-twins/model/list" WITH DESTINATION_TOPIC "cloud-iot/azure-digital-twins/model/list/response" WITH METHOD "GET" WITH ENDPOINT "/models?api-version=2023-10-31" WITH HEADERS ADD "Accept" "application/json" ADD "Authorization" "Bearer {value.json.access_token}" ADD EVENT GetModel WITH SOURCE_TOPIC "cloud-iot/azure-digital-twins/model/get" WITH DESTINATION_TOPIC "cloud-iot/azure-digital-twins/model/get/response" WITH METHOD "GET" WITH ENDPOINT "/models/{value.json.model_id}?includeModelDefinition=true&api-version=2023-10-31" WITH HEADERS ADD "Accept" "application/json" ADD "Authorization" "Bearer {value.json.access_token}" ADD EVENT ListEventRoutes WITH SOURCE_TOPIC "cloud-iot/azure-digital-twins/event-route/list" WITH DESTINATION_TOPIC "cloud-iot/azure-digital-twins/event-route/list/response" WITH METHOD "GET" WITH ENDPOINT "/eventroutes?api-version=2023-10-31" WITH HEADERS ADD "Accept" "application/json" ADD "Authorization" "Bearer {value.json.access_token}" DEFINE CONNECTOR AzureDigitalTwinsEntraToken WITH TYPE REST_API ADD REST_API_CONFIG WITH BASE_ADDRESS "https://login.microsoftonline.com" WITH ENABLE_CLIENT "true" WITH USE_SSL "true" WITH REQUEST_TIMEOUT '30' WITH DEFAULT_CONTENT_TYPE "application/x-www-form-urlencoded" ADD EVENT GetAccessToken WITH SOURCE_TOPIC "cloud-iot/azure-digital-twins/token/get" WITH DESTINATION_TOPIC "cloud-iot/azure-digital-twins/token/get/response" WITH METHOD "POST" WITH ENDPOINT "/{value.json.tenant_id}/oauth2/v2.0/token" WITH HEADERS ADD "Accept" "application/json" ADD "Content-Type" "application/x-www-form-urlencoded" WITH BODY "grant_type=client_credentials&client_id={value.json.client_id}&client_secret={value.json.client_secret}&scope=https%3A%2F%2Fdigitaltwins.azure.net%2F.default" ``` ## MQTT examples Request bodies go under `body`, so control fields like `access_token`, `twin_id` and `message_id` are not sent to Azure. **Patch a twin from telemetry**: publish to `cloud-iot/azure-digital-twins/twin/update`: ```json theme={"theme":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} { "access_token": "", "twin_id": "press-01", "body": [ {"op": "add", "path": "/Temperature", "value": 71.4}, {"op": "add", "path": "/State", "value": "Running"} ] } ``` A successful patch returns `204` with an empty body. `replace` fails with `400` if the property has never been set on the twin, so use `add` for the first write. Every property must exist in the twin's DTDL model. **Create or replace a twin**: publish to `cloud-iot/azure-digital-twins/twin/upsert`: ```json theme={"theme":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} { "access_token": "", "twin_id": "press-01", "body": {"$metadata": {"$model": "dtmi:coreflux:Machine;1"}, "Temperature": 0, "State": "Idle"} } ``` **Link it to its line**: publish to `cloud-iot/azure-digital-twins/relationship/upsert`: ```json theme={"theme":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} { "access_token": "", "twin_id": "press-01", "relationship_id": "press-01-isPartOf-line-1", "body": {"$targetId": "line-1", "$relationshipName": "isPartOf"} } ``` **Query the graph**: publish to `cloud-iot/azure-digital-twins/query/run`: ```json theme={"theme":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} { "access_token": "", "body": {"query": "SELECT T FROM DIGITALTWINS T WHERE IS_OF_MODEL(T, 'dtmi:coreflux:Machine;1') AND T.Temperature > 80"} } ``` The reply has `value` (the matching twins) and `continuationToken`. Send `{"continuationToken": "..."}` as the body to get the next page. **Send telemetry on a twin**: publish to `cloud-iot/azure-digital-twins/telemetry/send`: ```json theme={"theme":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} { "access_token": "", "twin_id": "press-01", "message_id": "press-01-2026-10-05T03:15:00Z", "body": {"Vibration": 9.2} } ``` Telemetry is not stored on the twin. ADT forwards it to whatever event routes you configured (check them on `cloud-iot/azure-digital-twins/event-route/list`). `message_id` must be unique per message; ADT uses it to drop duplicates. **Read**: publish `{"access_token": "", "twin_id": "press-01"}` to `cloud-iot/azure-digital-twins/twin/get` or `.../relationship/list`, `{"access_token": ""}` to `.../model/list` or `.../event-route/list`, and `{"access_token": "", "model_id": "dtmi%3Acoreflux%3AMachine%3B1"}` to `.../model/get`. URL-encode the model id because it contains `:` and `;`. ## Helper actions Keep the Entra credentials in broker secrets, refresh the token on a timer and keep it in broker-internal state, so machines only publish their raw values: ```lot theme={"theme":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} DEFINE ACTION AzureDigitalTwinsRefreshToken ON EVERY 45 MINUTES DO SET "tenant" WITH GET SECRET "ADT_TENANT_ID" SET "client" WITH GET SECRET "ADT_CLIENT_ID" SET "secret" WITH GET SECRET "ADT_CLIENT_SECRET" PUBLISH TOPIC "cloud-iot/azure-digital-twins/token/get" WITH { "tenant_id": {tenant}, "client_id": {client}, "client_secret": {secret} } DEFINE ACTION AzureDigitalTwinsKeepToken ON TOPIC "cloud-iot/azure-digital-twins/token/get/response" DO SET "token" WITH GET JSON "access_token" IN PAYLOAD AS STRING KEEP TOPIC "state/azure-digital-twins/token" WITH {token} DEFINE ACTION AzureDigitalTwinsSyncTemperature ON TOPIC "plant/porto/line1/+/temperature" DO SET "machine" WITH TOPIC POSITION 4 SET "temp" WITH PAYLOAD AS DOUBLE SET "token" WITH GET TOPIC "state/azure-digital-twins/token" AS STRING PUBLISH TOPIC "cloud-iot/azure-digital-twins/twin/update" WITH { "access_token": {token}, "twin_id": {machine}, "body": [ { "op": "add", "path": "/Temperature", "value": {temp} } ] } ``` Here the fourth topic segment (`press-01` in `plant/porto/line1/press-01/temperature`) is the twin id. Name twins after your UNS asset ids so this mapping stays trivial. Store a client secret that is already URL-encoded, or encode it before publishing. Restrict who can read `cloud-iot/azure-digital-twins/#` with a rule: these payloads carry tokens and secrets. ## Notes * Errors come back on the response topic: `401` for a missing, expired or wrong-audience token, `403` without the Data Owner or Data Reader role, `404` (`DigitalTwinNotFound`, `RelationshipNotFound`, `ModelNotFound`), `400` for a bad JSON Patch, a property that is not in the DTDL model or an invalid query, `412` for a failed `If-Match` / `If-None-Match` precondition, `429` when throttled. * ADT throttles requests per instance and per twin (see [Azure Digital Twins service limits](https://learn.microsoft.com/en-us/azure/digital-twins/reference-service-limits)). Batch high-rate telemetry or use `ON CHANGE` instead of patching on every sample. * `PATCH` and `PUT` here send no `If-Match` / `If-None-Match` header, so they overwrite unconditionally. Add the header to the event if you need optimistic concurrency. * Model upload and decommission, deletes, import and delete jobs, event connector writes, component telemetry and component patches, and the ARM control plane (creating instances) are not part of this template. ## Related * [REST API Connectors](/latest/lot-language/routes/rest-api-routes) * [REST API app templates](/latest/lot-language/routes/rest-api-apps/index) * [Azure IoT Hub](/latest/lot-language/routes/rest-api-apps/azure-iot-hub) * [Azure Data Explorer and Fabric Eventhouse](/latest/lot-language/routes/rest-api-apps/azure-data-explorer-and-fabric-eventhouse) # Azure Event Hubs Source: https://docs.coreflux.org/latest/lot-language/routes/rest-api-apps/azure-event-hubs LoT connector for Azure Event Hubs: send single, partition-keyed, partition-targeted or batched events over HTTPS with SAS or Entra ID auth Technical Preview ## Overview A client-mode connector that forwards broker data into [Azure Event Hubs](https://learn.microsoft.com/en-us/azure/event-hubs/event-hubs-about) over the runtime REST send API. Use it to stream plant telemetry into Azure analytics (Stream Analytics, Fabric Real-Time Intelligence, Data Explorer, Databricks) without running an AMQP or Kafka client. | Event | Request | Notes | | - | - | - | | SendEvent | `POST /{hub}/messages` | One event; `application/atom+xml;type=entry;charset=utf-8` | | SendEventWithPartitionKey | `POST /{hub}/messages` + `BrokerProperties` | Same key lands on the same partition (ordered per device) | | SendPartitionEvent | `POST /{hub}/partitions/{id}/messages` | Explicit partition | | SendBatch | `POST /{hub}/messages` | JSON array of `{"Body", "UserProperties"}`; `application/vnd.microsoft.servicebus.json` | Every request uses `api-version=2014-01` and returns **201 Created** with an empty body. Default `BASE_ADDRESS` is `https://YOUR_NAMESPACE.servicebus.windows.net` (no trailing slash). Topics use `data/azure-event-hubs//`. See the base guide: [REST API Connectors](/latest/lot-language/routes/rest-api-routes). > [!NOTE] > > **Auth:** pass the full `Authorization` value as `{value.json.authorization}`, either `SharedAccessSignature sr=…&sig=…&se=…&skn=…` from a policy with the **Send** claim, or `Bearer {entra_token}` (scope `https://eventhubs.azure.net/.default`, role **Azure Event Hubs Data Sender**). Prefer Hub secrets; never commit keys into the `.lot`. > [!NOTE] > > **Send only.** Event Hubs has no REST receive API; consumers use AMQP or Kafka. The management plane (creating namespaces and hubs) goes through Azure Resource Manager and is not packaged. A **Fabric Eventstream** custom endpoint exposes an Event Hubs-compatible namespace and SAS key, so the same template applies. This template has not yet been verified against a live namespace. ## Basic Syntax ```lot wrap theme={"theme":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} DEFINE CONNECTOR AzureEventHubsRestApi WITH TYPE REST_API ADD REST_API_CONFIG WITH BASE_ADDRESS "https://YOUR_NAMESPACE.servicebus.windows.net" WITH ENABLE_CLIENT "true" WITH USE_SSL "true" WITH REQUEST_TIMEOUT '60' WITH DEFAULT_CONTENT_TYPE "application/json" # Auth: SAS or Entra Bearer via {value.json.authorization} (see header comments). Never commit real keys. ADD EVENT SendEvent WITH SOURCE_TOPIC "data/azure-event-hubs/messages/post" WITH DESTINATION_TOPIC "data/azure-event-hubs/messages/post/response" WITH METHOD "POST" WITH ENDPOINT "/{value.json.hub}/messages?timeout=60&api-version=2014-01" WITH HEADERS ADD "Content-Type" "application/atom+xml;type=entry;charset=utf-8" ADD "Authorization" "{value.json.authorization}" WITH BODY "{value.json.body}" ADD EVENT SendEventWithPartitionKey WITH SOURCE_TOPIC "data/azure-event-hubs/messages/keyed/post" WITH DESTINATION_TOPIC "data/azure-event-hubs/messages/keyed/post/response" WITH METHOD "POST" WITH ENDPOINT "/{value.json.hub}/messages?timeout=60&api-version=2014-01" WITH HEADERS ADD "Content-Type" "application/atom+xml;type=entry;charset=utf-8" ADD "Authorization" "{value.json.authorization}" ADD "BrokerProperties" "{value.json.broker_properties}" WITH BODY "{value.json.body}" ADD EVENT SendPartitionEvent WITH SOURCE_TOPIC "data/azure-event-hubs/partitions/messages/post" WITH DESTINATION_TOPIC "data/azure-event-hubs/partitions/messages/post/response" WITH METHOD "POST" WITH ENDPOINT "/{value.json.hub}/partitions/{value.json.partition_id}/messages?timeout=60&api-version=2014-01" WITH HEADERS ADD "Content-Type" "application/atom+xml;type=entry;charset=utf-8" ADD "Authorization" "{value.json.authorization}" WITH BODY "{value.json.body}" ADD EVENT SendBatch WITH SOURCE_TOPIC "data/azure-event-hubs/messages/batch/post" WITH DESTINATION_TOPIC "data/azure-event-hubs/messages/batch/post/response" WITH METHOD "POST" WITH ENDPOINT "/{value.json.hub}/messages?timeout=60&api-version=2014-01" WITH HEADERS ADD "Content-Type" "application/vnd.microsoft.servicebus.json" ADD "Authorization" "{value.json.authorization}" WITH BODY "{value.json.body}" ``` ## MQTT examples **SendEvent** (`data/azure-event-hubs/messages/post`), expect **201 Created** with an empty body: ```json theme={"theme":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} { "hub": "press-telemetry", "authorization": "SharedAccessSignature sr=https%3A%2F%2FYOUR_NAMESPACE.servicebus.windows.net%2Fpress-telemetry&sig=...&se=1767225600&skn=coreflux-send", "body": { "DeviceId": "press-01", "Temperature": 37.0, "ts": "2026-10-05T00:21:00Z" } } ``` **SendEventWithPartitionKey** (`data/azure-event-hubs/messages/keyed/post`), expect **201 Created** with an empty body: ```json theme={"theme":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} { "hub": "press-telemetry", "authorization": "SharedAccessSignature sr=...&sig=...&se=...&skn=coreflux-send", "broker_properties": "{\"PartitionKey\":\"press-01\"}", "body": { "DeviceId": "press-01", "rpm": 1450 } } ``` **SendPartitionEvent** (`data/azure-event-hubs/partitions/messages/post`), expect **201 Created** with an empty body: ```json theme={"theme":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} { "hub": "press-telemetry", "partition_id": "0", "authorization": "SharedAccessSignature sr=...&sig=...&se=...&skn=coreflux-send", "body": { "DeviceId": "press-02", "state": "RUN" } } ``` **SendBatch** (`data/azure-event-hubs/messages/batch/post`), expect **201 Created** with an empty body: ```json theme={"theme":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} { "hub": "press-telemetry", "authorization": "Bearer ", "body": [ { "Body": "{\"DeviceId\":\"press-01\",\"t\":37.1}", "UserProperties": { "line": "A" } }, { "Body": "{\"DeviceId\":\"press-02\",\"t\":36.8}" } ] } ``` `body` becomes the HTTP body, so control fields (`hub`, `authorization`, `partition_id`, `broker_properties`) are never sent to Azure. ## Generating a SAS token The signature is `HMAC-SHA256(key, url_encode(resource_uri) + "\n" + expiry)`, base64-encoded and URL-encoded. With Azure CLI you can read the policy key: ```bash theme={"theme":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} az eventhubs eventhub authorization-rule keys list -g YOUR_RG --namespace-name YOUR_NAMESPACE --eventhub-name press-telemetry -n coreflux-send ``` Then build the token with the snippet from [Generate SAS token](https://learn.microsoft.com/en-us/rest/api/eventhub/generate-sas-token). Keep expiry short and rotate through Hub secrets. ## Notes * Replace `YOUR_NAMESPACE` in `BASE_ADDRESS` before deploy. * Max event or batch size is 1 MB (Standard and above); 256 KB on Basic. * `UserProperties` in headers are ignored for batches; put them inside each batch item. ## Related * [REST API Connectors](/latest/lot-language/routes/rest-api-routes) * [Send event](https://learn.microsoft.com/en-us/rest/api/eventhub/send-event) * [Send batch events](https://learn.microsoft.com/en-us/rest/api/eventhub/send-batch-events) * [Send partition event](https://learn.microsoft.com/en-us/rest/api/eventhub/send-partition-event) * [Authenticate with SAS](https://learn.microsoft.com/en-us/azure/event-hubs/authenticate-shared-access-signature) # Azure IoT Hub Source: https://docs.coreflux.org/latest/lot-language/routes/rest-api-apps/azure-iot-hub Client-mode REST API connector for a curated Azure IoT Hub slice: registry GetDevice, device-to-cloud SendDeviceEvent, twin GET/PATCH, and InvokeDeviceMethod Technical Preview ## Overview A client-mode connector for a **curated cloud-IoT** slice of [Azure IoT Hub](https://learn.microsoft.com/en-us/azure/iot-hub/iot-hub-devguide-overview) covering registry, device-to-cloud events, device twin, and direct methods: | Catalog connector event | Entity / path | Notes | | - | - | - | | Registry get | `GET /devices/{deviceId}?api-version=…` | Service SAS or Entra | | POST devices/\{id}/messages/events | `POST /devices/{deviceId}/messages/events?api-version=…` | DeviceConnect SAS; HTTP 204 | | GET twins/\{id} | `GET /twins/{deviceId}?api-version=…` | Read twin | | PATCH twins/\{id} | `PATCH /twins/{deviceId}?api-version=…` | Service desired/tags (catalog named reported) | | POST twins/\{id}/methods | `POST /twins/{deviceId}/methods?api-version=…` | Direct method / command to device | Default `BASE_ADDRESS` is `https://YOUR_HUB.azure-devices.net` (no trailing slash). Topics use `cloud-iot/azure-iot-hub//`. Default `api-version` is **`2021-04-12`**. See the base guide: [REST API Connectors](/latest/lot-language/routes/rest-api-routes). > [!NOTE] > > **Auth:** pass `Authorization` as `{value.json.authorization}` — either `SharedAccessSignature sr=…&sig=…&se=…&skn=…` or `Bearer {entra_token}` for **service** APIs. Prefer Hub secrets — never commit real keys into the `.lot`. > [!NOTE] > > **Device SendDeviceEvent** requires a **DeviceConnect** SAS scoped to the device. Microsoft Entra ID is **not** supported for IoT Hub device APIs. ## Basic Syntax ```lot wrap theme={"theme":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} # Coreflux REST_API template — Azure IoT Hub curated slice # Auth: Authorization → {value.json.authorization} # SharedAccessSignature … OR Bearer {entra_token} (service) # BASE_ADDRESS: https://YOUR_HUB.azure-devices.net DEFINE CONNECTOR AzureIotHubRestApi WITH TYPE REST_API ADD REST_API_CONFIG WITH BASE_ADDRESS "https://YOUR_HUB.azure-devices.net" WITH ENABLE_CLIENT "true" WITH USE_SSL "true" WITH REQUEST_TIMEOUT '60' WITH DEFAULT_CONTENT_TYPE "application/json" ADD EVENT GetDevice WITH SOURCE_TOPIC "cloud-iot/azure-iot-hub/devices/get" WITH DESTINATION_TOPIC "cloud-iot/azure-iot-hub/devices/get/response" WITH METHOD "GET" WITH ENDPOINT "/devices/{value.json.device_id}?api-version={value.json.api_version}" WITH HEADERS ADD "Accept" "application/json" ADD "Authorization" "{value.json.authorization}" ADD EVENT SendDeviceEvent WITH SOURCE_TOPIC "cloud-iot/azure-iot-hub/messages/events/post" WITH DESTINATION_TOPIC "cloud-iot/azure-iot-hub/messages/events/post/response" WITH METHOD "POST" WITH ENDPOINT "/devices/{value.json.device_id}/messages/events?api-version={value.json.api_version}" WITH HEADERS ADD "Accept" "application/json" ADD "Content-Type" "application/json" ADD "Authorization" "{value.json.authorization}" WITH BODY "{payload}" ADD EVENT GetTwin WITH SOURCE_TOPIC "cloud-iot/azure-iot-hub/twins/get" WITH DESTINATION_TOPIC "cloud-iot/azure-iot-hub/twins/get/response" WITH METHOD "GET" WITH ENDPOINT "/twins/{value.json.device_id}?api-version={value.json.api_version}" WITH HEADERS ADD "Accept" "application/json" ADD "Authorization" "{value.json.authorization}" ADD EVENT UpdateTwin WITH SOURCE_TOPIC "cloud-iot/azure-iot-hub/twins/update" WITH DESTINATION_TOPIC "cloud-iot/azure-iot-hub/twins/update/response" WITH METHOD "PATCH" WITH ENDPOINT "/twins/{value.json.device_id}?api-version={value.json.api_version}" WITH HEADERS ADD "Accept" "application/json" ADD "Content-Type" "application/json" ADD "Authorization" "{value.json.authorization}" ADD "If-Match" "{value.json.if_match}" WITH BODY "{payload}" ADD EVENT InvokeDeviceMethod WITH SOURCE_TOPIC "cloud-iot/azure-iot-hub/twins/methods/post" WITH DESTINATION_TOPIC "cloud-iot/azure-iot-hub/twins/methods/post/response" WITH METHOD "POST" WITH ENDPOINT "/twins/{value.json.device_id}/methods?api-version={value.json.api_version}" WITH HEADERS ADD "Accept" "application/json" ADD "Content-Type" "application/json" ADD "Authorization" "{value.json.authorization}" WITH BODY "{payload}" ``` ## MQTT examples **GetDevice** (`cloud-iot/azure-iot-hub/devices/get`): ```json theme={"theme":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} { "device_id": "line-a-gw", "api_version": "2021-04-12", "authorization": "SharedAccessSignature sr=YOUR_HUB.azure-devices.net&sig=...&se=...&skn=registryRead" } ``` **SendDeviceEvent** (`cloud-iot/azure-iot-hub/messages/events/post`) — device SAS; expect **204**: ```json theme={"theme":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} { "device_id": "line-a-gw", "api_version": "2021-04-12", "authorization": "SharedAccessSignature sr=YOUR_HUB.azure-devices.net%2Fdevices%2Fline-a-gw&sig=...&se=...&skn=device", "payload": "{\"temp\":72.5}" } ``` **UpdateTwin** (`cloud-iot/azure-iot-hub/twins/update`): ```json theme={"theme":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} { "device_id": "line-a-gw", "api_version": "2021-04-12", "authorization": "SharedAccessSignature sr=YOUR_HUB.azure-devices.net&sig=...&se=...&skn=service", "if_match": "*", "payload": "{\"properties\":{\"desired\":{\"pump\":true}}}" } ``` **InvokeDeviceMethod** (`cloud-iot/azure-iot-hub/twins/methods/post`): ```json theme={"theme":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} { "device_id": "line-a-gw", "api_version": "2021-04-12", "authorization": "SharedAccessSignature sr=YOUR_HUB.azure-devices.net&sig=...&se=...&skn=service", "payload": "{\"methodName\":\"reboot\",\"responseTimeoutInSeconds\":30,\"payload\":{}}" } ``` ## Notes * Replace `YOUR_HUB` in `BASE_ADDRESS` before deploy. * `api-version=2021-04-12` is required on every request (pass `api_version` in MQTT JSON). * Service twin PATCH updates **desired** properties and **tags**. Catalog wording said “reported”; reported properties are typically updated by the device. * Event Grid inbound webhooks are not packaged (future server connector). ## Related * [REST API Connectors](/latest/lot-language/routes/rest-api-routes) * [Microsoft IoT Hub REST](https://learn.microsoft.com/en-us/rest/api/iothub/) * [Control access with SAS](https://learn.microsoft.com/en-us/azure/iot-hub/authenticate-authorize-sas) * [Control access with Entra ID](https://learn.microsoft.com/en-us/azure/iot-hub/authenticate-authorize-azure-ad) # Azure Monitor Source: https://docs.coreflux.org/latest/lot-language/routes/rest-api-apps/azure-monitor LoT connector for Azure Monitor: send logs to a DCR stream, post custom metrics and run Log Analytics KQL queries from MQTT with Entra tokens Technical Preview ## Overview A client-mode connector for a **curated observability / ITSM** slice of [Azure Monitor](https://learn.microsoft.com/en-us/azure/azure-monitor/): | Catalog connector event | Entity / path | Notes | | - | - | - | | POST logs to DCR stream | `POST /dataCollectionRules/{dcr}/streams/{stream}?api-version=2023-01-01` | Logs Ingestion on DCE/DCR host; scope `https://monitor.azure.com/.default`; expect **204** | | POST custom metric | `POST /{resourceId}/metrics` | Host `https://{region}.monitoring.azure.com`; scope `https://monitoring.azure.com/.default`; expect **200** | | POST KQL query | `POST /v1/workspaces/{workspaceId}/query` | Host `https://api.loganalytics.azure.com`; scope `https://api.loganalytics.io/.default`; credential check → **200** | | GET KQL query | `GET /v1/workspaces/{workspaceId}/query` | Same query API via query string | | Action Group / alert webhook | inbound HTTP | **ENABLE\_SERVER companion** (docs only — not in this client `.lot`) | Default `BASE_ADDRESS` is `https://YOUR_DCE.ingest.monitor.azure.com`. Topics use `itsm/azure-monitor//`. Switch BASE per surface (Logs / Metrics / Query) the same way other multi-host connectors do. > [!NOTE] > > Replace `YOUR_DCE` with your Data Collection Endpoint hostname (or use the DCR `logsIngestion` endpoint). Prefer Hub secrets for Entra client credentials and workspace / DCR / resource identifiers. ## Basic Syntax ```lot wrap theme={"theme":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} # Coreflux REST_API template — Azure Monitor (Logs + Metrics + KQL) # Auth: Authorization: Bearer {value.json.access_token} # BASE_ADDRESS: https://YOUR_DCE.ingest.monitor.azure.com DEFINE CONNECTOR AzureMonitorRestApi WITH TYPE REST_API ADD REST_API_CONFIG WITH BASE_ADDRESS "https://YOUR_DCE.ingest.monitor.azure.com" WITH ENABLE_CLIENT "true" WITH USE_SSL "true" WITH REQUEST_TIMEOUT '60' WITH DEFAULT_CONTENT_TYPE "application/json" ADD EVENT Uploadlogs WITH SOURCE_TOPIC "itsm/azure-monitor/log/ingest" WITH DESTINATION_TOPIC "itsm/azure-monitor/log/ingest/response" WITH METHOD "POST" WITH ENDPOINT "/dataCollectionRules/{value.json.dcr_immutable_id}/streams/{value.json.stream_name}?api-version={value.json.api_version}" WITH HEADERS ADD "Accept" "application/json" ADD "Content-Type" "application/json" ADD "Authorization" "Bearer {value.json.access_token}" ADD "x-ms-client-request-id" "{value.json.client_request_id}" WITH BODY "{payload}" ADD EVENT Createcustommetric WITH SOURCE_TOPIC "itsm/azure-monitor/metric/custom" WITH DESTINATION_TOPIC "itsm/azure-monitor/metric/custom/response" WITH METHOD "POST" WITH ENDPOINT "/{value.json.resource_id}/metrics" WITH HEADERS ADD "Accept" "application/json" ADD "Content-Type" "application/json" ADD "Authorization" "Bearer {value.json.access_token}" WITH BODY "{payload}" ADD EVENT Executequery WITH SOURCE_TOPIC "itsm/azure-monitor/query/execute" WITH DESTINATION_TOPIC "itsm/azure-monitor/query/execute/response" WITH METHOD "POST" WITH ENDPOINT "/v1/workspaces/{value.json.workspace_id}/query" WITH HEADERS ADD "Accept" "application/json" ADD "Content-Type" "application/json" ADD "Authorization" "Bearer {value.json.access_token}" WITH BODY "{payload}" ADD EVENT Executequeryget WITH SOURCE_TOPIC "itsm/azure-monitor/query/get" WITH DESTINATION_TOPIC "itsm/azure-monitor/query/get/response" WITH METHOD "GET" WITH ENDPOINT "/v1/workspaces/{value.json.workspace_id}/query?query={value.json.query}×pan={value.json.timespan}" WITH HEADERS ADD "Accept" "application/json" ADD "Authorization" "Bearer {value.json.access_token}" ``` ## Authentication | Mode | Header | Notes | | - | - | - | | Entra OAuth 2.0 client credentials | `Authorization: Bearer {value.json.access_token}` | Default in this template | Acquire tokens from `https://login.microsoftonline.com/{tenant}/oauth2/v2.0/token`. Scopes: `https://monitor.azure.com/.default` (logs), `https://monitoring.azure.com/.default` (custom metrics), `https://api.loganalytics.io/.default` (query). Store tenant / client / secret in Hub secrets (`AZURE_TENANT_ID`, `AZURE_CLIENT_ID`, `AZURE_CLIENT_SECRET`) and resolve via `GET SECRET` — never commit credentials. ## MQTT examples **Logs ingest** (JSON array; BASE = DCE): ```json theme={"theme":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} [ { "Time": "2026-10-04T17:00:00Z", "Computer": "line-a", "AdditionalContext": "vibration_rms=12.4" } ] ``` Publish with `access_token`, `dcr_immutable_id`, `stream_name`, and `api_version` (`2023-01-01`). Expect **HTTP 204**. **Custom metric** (BASE = `{region}.monitoring.azure.com`): ```json theme={"theme":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} { "time": "2026-10-04T17:00:00Z", "data": { "baseData": { "metric": "vibration_rms", "namespace": "Coreflux/Plant", "series": [{"min": 12.4, "max": 12.4, "sum": 12.4, "count": 1}] } } } ``` Expect **HTTP 200**. **KQL query** (BASE = `api.loganalytics.azure.com`): ```json theme={"theme":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} { "query": "AzureActivity | summarize count() by Category", "timespan": "PT12H" } ``` Expect **HTTP 200**. ## Action Group webhook companion Azure Monitor Action Groups can deliver Common Alert Schema (or older schemas) to a webhook URL. Package a separate `ENABLE_SERVER true` REST\_API connector to receive those POSTs on an MQTT-bound path. That inbound surface is **not** included in `AzureMonitorRestApi.lot`. ## Sources * [Logs Ingestion API](https://learn.microsoft.com/en-us/azure/azure-monitor/logs/logs-ingestion-api-overview) * [Custom metrics REST](https://learn.microsoft.com/en-us/azure/azure-monitor/metrics/metrics-store-custom-rest-api) * [Metrics - Custom - Create](https://learn.microsoft.com/en-us/rest/api/monitor/metrics-custom/create) * [Logs query API](https://learn.microsoft.com/en-us/azure/azure-monitor/logs/api/overview) * [API access and authentication](https://learn.microsoft.com/en-us/azure/azure-monitor/logs/api/access-api) * [Action groups](https://learn.microsoft.com/en-us/azure/azure-monitor/alerts/action-groups) ## Package Broker template folder: `src/CorefluxCentral/Routes/Templates/RestApiConnectors/azure-monitor/` Deployable file: `AzureMonitorRestApi.lot` (no `ADD METADATA`). Hub icon/status: `hub-metadata.lot`. # Azure OpenAI and Azure AI Foundry Source: https://docs.coreflux.org/latest/lot-language/routes/rest-api-apps/azure-openai-and-azure-ai-foundry Client-mode REST API connector for Azure OpenAI / Foundry chat completions and embeddings — MQTT into Azure-hosted LLM alarm classification Technical Preview ## Overview A client-mode connector for a **curated** slice of [Azure OpenAI](https://learn.microsoft.com/en-us/azure/foundry/openai/reference) / [Azure AI Foundry](https://learn.microsoft.com/en-us/rest/api/microsoft-foundry/azureopenai/chat) focused on IIoT / edge alarm text into Azure-hosted LLMs (OpenAI-compatible bodies, deployment-based paths): | Catalog connector event | Entity / path | Notes | | - | - | - | | POST chat/completions | `POST /openai/deployments/{deployment}/chat/completions?api-version=…` | Chat Completions — same body template as OpenAI | | POST embeddings | `POST /openai/deployments/{deployment}/embeddings?api-version=…` | Create embeddings on an embeddings deployment | | (supporting) GET models | `GET /openai/models?api-version=…` | List models (checks the token) | | (supporting) GET model | `GET /openai/models/{model}?api-version=…` | Retrieve one model | Default `BASE_ADDRESS` placeholder is `https://YOUR_RESOURCE.openai.azure.com` (no trailing slash) — replace with your real resource host. Default `api-version` in docs/examples: **`2024-10-21`**. Topics use `ai/azure-openai//`. See the base guide: [REST API Connectors](/latest/lot-language/routes/rest-api-routes). > [!NOTE] > > **Auth:** `api-key: <Azure OpenAI resource key>` on every request (template default). Prefer Hub secrets (`GET SECRET "AZURE_OPENAI_API_KEY"` or `AZURE_OPENAI_KEY`) mapped into MQTT `api_token`. **Entra ID alternative:** `Authorization: Bearer <token>` with scope `https://cognitiveservices.azure.com/.default`. Never commit real keys. ## Basic Syntax ```lot wrap theme={"theme":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} # Coreflux REST_API template — Azure OpenAI / Foundry # Auth: api-key {value.json.api_token} # BASE_ADDRESS: https://YOUR_RESOURCE.openai.azure.com DEFINE CONNECTOR AzureOpenaiAndAzureAiFoundryRestApi WITH TYPE REST_API ADD REST_API_CONFIG WITH BASE_ADDRESS "https://YOUR_RESOURCE.openai.azure.com" WITH ENABLE_CLIENT "true" WITH USE_SSL "true" WITH REQUEST_TIMEOUT '60' WITH DEFAULT_CONTENT_TYPE "application/json" ADD EVENT PostChatCompletions WITH SOURCE_TOPIC "ai/azure-openai/chat/completions/post" WITH DESTINATION_TOPIC "ai/azure-openai/chat/completions/post/response" WITH METHOD "POST" WITH ENDPOINT "/openai/deployments/{value.json.deployment}/chat/completions?api-version={value.json.api_version}" WITH HEADERS ADD "Accept" "application/json" ADD "Content-Type" "application/json" ADD "api-key" "{value.json.api_token}" WITH BODY "{payload}" ADD EVENT PostEmbeddings WITH SOURCE_TOPIC "ai/azure-openai/embeddings/post" WITH DESTINATION_TOPIC "ai/azure-openai/embeddings/post/response" WITH METHOD "POST" WITH ENDPOINT "/openai/deployments/{value.json.deployment}/embeddings?api-version={value.json.api_version}" WITH HEADERS ADD "Accept" "application/json" ADD "Content-Type" "application/json" ADD "api-key" "{value.json.api_token}" WITH BODY "{payload}" ``` ## Topics | Event | Source topic | Destination topic | | - | - | - | | PostChatCompletions | `ai/azure-openai/chat/completions/post` | `…/response` | | PostEmbeddings | `ai/azure-openai/embeddings/post` | `…/response` | | GetModels | `ai/azure-openai/models/list` | `…/response` | | GetModel | `ai/azure-openai/models/get` | `…/response` | ## Configuration 1. Set `BASE_ADDRESS` to `https://{your-resource}.openai.azure.com` (no trailing slash). 2. Store the Azure OpenAI API key in Hub secrets (`AZURE_OPENAI_API_KEY` or `AZURE_OPENAI_KEY`); map into MQTT `api_token`. 3. Pass `deployment` (deployment name) and `api_version` (e.g. `2024-10-21`) on inference MQTT JSON. 4. For alarm classification: publish to `ai/azure-openai/chat/completions/post` with an OpenAI-compatible messages body (optional `response_format`). 5. For semantic search / similarity: publish to `ai/azure-openai/embeddings/post` with a tiny `input` string against an embeddings deployment. 6. Use `GetModels` as a cheap credential check before production workloads. ## References * [Azure OpenAI REST reference](https://learn.microsoft.com/en-us/azure/foundry/openai/reference) * [Models — List (2024-10-21)](https://learn.microsoft.com/en-us/rest/api/azureopenai/models/list?view=rest-azureopenai-2024-10-21) * [Foundry — Chat Completions](https://learn.microsoft.com/en-us/rest/api/microsoft-foundry/azureopenai/chat) * [Foundry — Embeddings](https://learn.microsoft.com/en-us/rest/api/microsoft-foundry/azureopenai/embeddings) * Sibling: [OpenAI](/latest/lot-language/routes/rest-api-apps/openai) # Azure Service Bus Source: https://docs.coreflux.org/latest/lot-language/routes/rest-api-apps/azure-service-bus LoT connector for Azure Service Bus: send to queues and topics, peek-lock or receive-and-delete messages, and complete or unlock them from MQTT Technical Preview ## Overview Client-mode REST API connector for the Azure Service Bus runtime REST API: send to queues and topics plus peek-lock and receive-and-delete and complete and unlock. `BASE_ADDRESS` defaults to `https://YOUR_NAMESPACE.servicebus.windows.net`. Topics use `cloud/azure-service-bus//`. | Event | Request | Source topic | | - | - | - | | `QueueSend` | `POST /{value.json.queue}/messages?timeout=60` | `cloud/azure-service-bus/queues/send` | | `TopicSend` | `POST /{value.json.topic}/messages?timeout=60` | `cloud/azure-service-bus/topics/send` | | `QueuePeekLock` | `POST /{value.json.queue}/messages/head?timeout={value.json.timeout}` | `cloud/azure-service-bus/queues/peek-lock` | | `QueueReceiveAndDelete` | `DELETE /{value.json.queue}/messages/head?timeout={value.json.timeout}` | `cloud/azure-service-bus/queues/receive` | | `SubscriptionPeekLock` | `POST /{value.json.topic}/subscriptions/{value.json.subscription}/messages/head?timeout={value.json.timeout}` | `cloud/azure-service-bus/subscriptions/peek-lock` | | `SubscriptionReceiveAndDelete` | `DELETE /{value.json.topic}/subscriptions/{value.json.subscription}/messages/head?timeout={value.json.timeout}` | `cloud/azure-service-bus/subscriptions/receive` | | `MessageComplete` | `DELETE /{value.json.entity_path}/messages/{value.json.messageId}/{value.json.lockToken}` | `cloud/azure-service-bus/messages/complete` | | `MessageUnlock` | `PUT /{value.json.entity_path}/messages/{value.json.messageId}/{value.json.lockToken}` | `cloud/azure-service-bus/messages/unlock` | ## Authentication Authorization: \{value.json.authorization} carries either "SharedAccessSignature sr=...\&sig=...\&se=...\&skn=..." (SAS from a policy with Send / Listen) or "Bearer \" (scope [https://servicebus.azure.net/.default](https://servicebus.azure.net/.default); roles Azure Service Bus Data Sender / Receiver). The connector cannot sign; generate the SAS or token upstream (LoT action with GET SECRET or an external helper). > [!WARNING] > > Keep credentials in broker secrets and never retain topics that carry tokens or client secrets. ## Notes * Topic names here use cloud/azure-service-bus/... (not messaging/...). * BrokerProperties is a JSON string header, e.g. \{"MessageId":"m-1","SessionId":"line-1","TimeToLive":3600}; pass "\{}" when unused. * Peek-lock returns the lock token in the BrokerProperties response header; MessageComplete and MessageUnlock need messageId and lockToken from it. ## Basic Syntax ```lot wrap theme={"theme":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} DEFINE CONNECTOR AzureServiceBusRestApi WITH TYPE REST_API ADD REST_API_CONFIG WITH BASE_ADDRESS "https://YOUR_NAMESPACE.servicebus.windows.net" WITH ENABLE_CLIENT "true" WITH USE_SSL "true" WITH REQUEST_TIMEOUT '90' WITH DEFAULT_CONTENT_TYPE "application/json" ADD EVENT QueueSend WITH SOURCE_TOPIC "cloud/azure-service-bus/queues/send" WITH DESTINATION_TOPIC "cloud/azure-service-bus/queues/send/response" WITH METHOD "POST" WITH ENDPOINT "/{value.json.queue}/messages?timeout=60" WITH HEADERS ADD "Accept" "application/json" ADD "Authorization" "{value.json.authorization}" ADD "Content-Type" "application/json" ADD "BrokerProperties" "{value.json.broker_properties}" WITH BODY "{value.json.body}" ADD EVENT TopicSend WITH SOURCE_TOPIC "cloud/azure-service-bus/topics/send" WITH DESTINATION_TOPIC "cloud/azure-service-bus/topics/send/response" WITH METHOD "POST" WITH ENDPOINT "/{value.json.topic}/messages?timeout=60" WITH HEADERS ADD "Accept" "application/json" ADD "Authorization" "{value.json.authorization}" ADD "Content-Type" "application/json" ADD "BrokerProperties" "{value.json.broker_properties}" WITH BODY "{value.json.body}" ADD EVENT QueuePeekLock WITH SOURCE_TOPIC "cloud/azure-service-bus/queues/peek-lock" WITH DESTINATION_TOPIC "cloud/azure-service-bus/queues/peek-lock/response" WITH METHOD "POST" WITH ENDPOINT "/{value.json.queue}/messages/head?timeout={value.json.timeout}" WITH HEADERS ADD "Accept" "application/json" ADD "Authorization" "{value.json.authorization}" WITH BODY "" ADD EVENT QueueReceiveAndDelete WITH SOURCE_TOPIC "cloud/azure-service-bus/queues/receive" WITH DESTINATION_TOPIC "cloud/azure-service-bus/queues/receive/response" WITH METHOD "DELETE" WITH ENDPOINT "/{value.json.queue}/messages/head?timeout={value.json.timeout}" WITH HEADERS ADD "Accept" "application/json" ADD "Authorization" "{value.json.authorization}" WITH BODY "" ADD EVENT SubscriptionPeekLock WITH SOURCE_TOPIC "cloud/azure-service-bus/subscriptions/peek-lock" WITH DESTINATION_TOPIC "cloud/azure-service-bus/subscriptions/peek-lock/response" WITH METHOD "POST" WITH ENDPOINT "/{value.json.topic}/subscriptions/{value.json.subscription}/messages/head?timeout={value.json.timeout}" WITH HEADERS ADD "Accept" "application/json" ADD "Authorization" "{value.json.authorization}" WITH BODY "" ADD EVENT SubscriptionReceiveAndDelete WITH SOURCE_TOPIC "cloud/azure-service-bus/subscriptions/receive" WITH DESTINATION_TOPIC "cloud/azure-service-bus/subscriptions/receive/response" WITH METHOD "DELETE" WITH ENDPOINT "/{value.json.topic}/subscriptions/{value.json.subscription}/messages/head?timeout={value.json.timeout}" WITH HEADERS ADD "Accept" "application/json" ADD "Authorization" "{value.json.authorization}" WITH BODY "" ADD EVENT MessageComplete WITH SOURCE_TOPIC "cloud/azure-service-bus/messages/complete" WITH DESTINATION_TOPIC "cloud/azure-service-bus/messages/complete/response" WITH METHOD "DELETE" WITH ENDPOINT "/{value.json.entity_path}/messages/{value.json.messageId}/{value.json.lockToken}" WITH HEADERS ADD "Accept" "application/json" ADD "Authorization" "{value.json.authorization}" WITH BODY "" ADD EVENT MessageUnlock WITH SOURCE_TOPIC "cloud/azure-service-bus/messages/unlock" WITH DESTINATION_TOPIC "cloud/azure-service-bus/messages/unlock/response" WITH METHOD "PUT" WITH ENDPOINT "/{value.json.entity_path}/messages/{value.json.messageId}/{value.json.lockToken}" WITH HEADERS ADD "Accept" "application/json" ADD "Authorization" "{value.json.authorization}" WITH BODY "" ``` ## Sources * [https://learn.microsoft.com/en-us/rest/api/servicebus/send-message-to-queue](https://learn.microsoft.com/en-us/rest/api/servicebus/send-message-to-queue) * [https://learn.microsoft.com/en-us/rest/api/servicebus/peek-lock-message-non-destructive-read](https://learn.microsoft.com/en-us/rest/api/servicebus/peek-lock-message-non-destructive-read) * [https://learn.microsoft.com/en-us/rest/api/servicebus/receive-and-delete-message-destructive-read](https://learn.microsoft.com/en-us/rest/api/servicebus/receive-and-delete-message-destructive-read) * [https://learn.microsoft.com/en-us/rest/api/servicebus/delete-message](https://learn.microsoft.com/en-us/rest/api/servicebus/delete-message) * [https://learn.microsoft.com/en-us/rest/api/servicebus/unlock-message](https://learn.microsoft.com/en-us/rest/api/servicebus/unlock-message) * [https://learn.microsoft.com/en-us/azure/service-bus-messaging/service-bus-sas](https://learn.microsoft.com/en-us/azure/service-bus-messaging/service-bus-sas) # REST API Apps Source: https://docs.coreflux.org/latest/lot-language/routes/rest-api-apps/index Catalog of client-mode REST API LoT templates for IIoT workloads and enterprise systems Technical Preview > [!WARNING] > > Every REST API app in this catalog is in technical preview. ## Overview These templates are client-mode `TYPE REST_API` connectors: an MQTT request topic triggers an HTTP call, and the response is published on a matching topic. Start with [REST API Connectors](/latest/lot-language/routes/rest-api-routes) for config, auth, and placeholders. This catalog is the app layer on top of that connector. Every app ships with the broker. The connector signs outbound requests itself through `CLIENT_AUTH`, and reads credentials from broker secrets (`GET SECRET`). Create the secrets a template needs before you deploy it: a connector whose `CLIENT_AUTH` secret is empty does not start. The Hub lists these secrets (`requiredSecrets`) and asks for them before deploying. ## Catalog Apps are grouped by Hub category. An app with several connectors has one app code per connector, each with its own page. * **Auth** is the `CLIENT_AUTH` mode the template uses. *None* means the API is public or takes its key in the request. * **Minimum tier** follows the number of actions the connector registers: up to 24 runs on Community, up to 100 needs Growth, more needs Enterprise. ### Analytics | App | App code | Auth (`CLIENT_AUTH`) | Minimum tier | | - | - | - | - | | [Amazon DynamoDB](/latest/lot-language/routes/rest-api-apps/amazon-dynamodb) | `AMAZON_DYNAMODB` | AWS SigV4 | Community | | [Anaplan](/latest/lot-language/routes/rest-api-apps/anaplan) | `ANAPLAN` | Login token | Community | | [Apache Superset](/latest/lot-language/routes/rest-api-apps/apache-superset) | `APACHE_SUPERSET` | Login token | Growth | | [Azure Cosmos DB](/latest/lot-language/routes/rest-api-apps/azure-cosmos-db) | `AZURE_COSMOS_DB` | API key | Community | | [Azure Data Explorer (Kusto) and Fabric Eventhouse](/latest/lot-language/routes/rest-api-apps/azure-data-explorer-and-fabric-eventhouse) | `AZURE_DATA_EXPLORER_AND_FABRIC_EVENTHOUSE` | Bearer | Community | | [Azure Event Hubs (REST send)](/latest/lot-language/routes/rest-api-apps/azure-event-hubs) | `AZURE_EVENT_HUBS` | API key | Community | | [ClickHouse (OSS and Cloud)](/latest/lot-language/routes/rest-api-apps/clickhouse) | `CLICKHOUSE` | Basic | Community | | [Databricks](/latest/lot-language/routes/rest-api-apps/databricks) | `DATABRICKS` | Bearer | Community | | [Domo](/latest/lot-language/routes/rest-api-apps/domo) | `DOMO` | OAuth 2.0 client credentials | Growth | | [Elasticsearch and Elastic Cloud](/latest/lot-language/routes/rest-api-apps/elasticsearch-and-elastic-cloud) | `ELASTICSEARCH_AND_ELASTIC_CLOUD` | API key | Community | | [Firebase Realtime Database and Cloud Firestore](/latest/lot-language/routes/rest-api-apps/firebase-realtime-database-and-cloud-firestore) | [`CLOUD_FIRESTORE`](/latest/lot-language/routes/rest-api-apps/cloud-firestore) [`FIREBASE_REALTIME_DATABASE`](/latest/lot-language/routes/rest-api-apps/firebase-realtime-database) | Bearer | Community | | [Google BigQuery](/latest/lot-language/routes/rest-api-apps/google-bigquery) | `GOOGLE_BIGQUERY` | Bearer | Community | | [Google Cloud Pub/Sub](/latest/lot-language/routes/rest-api-apps/google-cloud-pub-sub) | `GOOGLE_CLOUD_PUB_SUB` | Bearer | Community | | [Grafana Loki](/latest/lot-language/routes/rest-api-apps/grafana-loki) | `GRAFANA_LOKI` | None | Community | | [InfluxDB (v2, v3 Core/Enterprise, Cloud)](/latest/lot-language/routes/rest-api-apps/influxdb) | `INFLUXDB` | API key | Community | | [Looker](/latest/lot-language/routes/rest-api-apps/looker) | `LOOKER` | OAuth 2.0 client credentials | Growth | | [Metabase](/latest/lot-language/routes/rest-api-apps/metabase) | `METABASE` | API key | Community | | [Microsoft Power BI](/latest/lot-language/routes/rest-api-apps/microsoft-power-bi) | `MICROSOFT_POWER_BI` | Bearer | Community | | [Qlik Cloud](/latest/lot-language/routes/rest-api-apps/qlik-cloud) | `QLIK_CLOUD` | Bearer | Growth | | [QuestDB](/latest/lot-language/routes/rest-api-apps/questdb) | `QUESTDB` | API key | Community | | [Snowflake SQL API](/latest/lot-language/routes/rest-api-apps/snowflake-sql-api) | `SNOWFLAKE_SQL_API` | Bearer | Community | | [Supabase (PostgREST, Realtime, Storage)](/latest/lot-language/routes/rest-api-apps/supabase) | `SUPABASE` | Bearer | Community | | [Tableau (Server and Cloud)](/latest/lot-language/routes/rest-api-apps/tableau) | `TABLEAU` | Login token | Community | | [TDengine](/latest/lot-language/routes/rest-api-apps/tdengine) | `TDENGINE` | Basic | Community | ### CRM | App | App code | Auth (`CLIENT_AUTH`) | Minimum tier | | - | - | - | - | | [Dynamics 365](/latest/lot-language/routes/rest-api-apps/dynamics365) | `DYNAMICS365` | Bearer | Community | | [Freshdesk and Freshservice](/latest/lot-language/routes/rest-api-apps/freshdesk-and-freshservice) | `FRESHDESK_AND_FRESHSERVICE` | Bearer | Community | | [Help Scout](/latest/lot-language/routes/rest-api-apps/help-scout) | `HELP_SCOUT` | OAuth 2.0 client credentials | Growth | | [HubSpot](/latest/lot-language/routes/rest-api-apps/hubspot) | `HUBSPOT` | Bearer | Community | | [Intercom](/latest/lot-language/routes/rest-api-apps/intercom) | `INTERCOM` | Bearer | Growth | | [Pipedrive](/latest/lot-language/routes/rest-api-apps/pipedrive) | `PIPEDRIVE` | API key | Community | | [Salesforce](/latest/lot-language/routes/rest-api-apps/salesforce) | `SALESFORCE` | Bearer | Community | | [SugarCRM](/latest/lot-language/routes/rest-api-apps/sugarcrm) | `SUGARCRM` | Login token | Community | | [Zendesk Support](/latest/lot-language/routes/rest-api-apps/zendesk-support) | `ZENDESK_SUPPORT` | Bearer | Community | | [Zoho CRM](/latest/lot-language/routes/rest-api-apps/zoho-crm) | `ZOHO_CRM` | Bearer | Community | ### Cloud IoT | App | App code | Auth (`CLIENT_AUTH`) | Minimum tier | | - | - | - | - | | [AWS IoT Core](/latest/lot-language/routes/rest-api-apps/aws-iot-core) | `AWS_IOT_CORE` | AWS SigV4 | Community | | [AWS IoT SiteWise](/latest/lot-language/routes/rest-api-apps/aws-iot-sitewise) | `AWS_IOT_SITEWISE` | AWS SigV4 | Community | | [Azure Digital Twins](/latest/lot-language/routes/rest-api-apps/azure-digital-twins) | `AZURE_DIGITAL_TWINS` | OAuth 2.0 client credentials | Community | | [Azure IoT Hub](/latest/lot-language/routes/rest-api-apps/azure-iot-hub) | `AZURE_IOT_HUB` | API key | Community | | [balena](/latest/lot-language/routes/rest-api-apps/balena) | `BALENA` | Bearer | Community | | [Blynk](/latest/lot-language/routes/rest-api-apps/blynk-device-api) | [`BLYNK_DEVICE_API`](/latest/lot-language/routes/rest-api-apps/blynk-device-api) [`BLYNK_PLATFORM_API`](/latest/lot-language/routes/rest-api-apps/blynk-platform-api) | API key, OAuth 2.0 client credentials | Community / Growth | | [ChirpStack v4](/latest/lot-language/routes/rest-api-apps/chirpstack-v4) | `CHIRPSTACK_V4` | API key | Community | | [Cumulocity](/latest/lot-language/routes/rest-api-apps/cumulocity) | `CUMULOCITY` | API key | Community | | [Datacake](/latest/lot-language/routes/rest-api-apps/datacake) | `DATACAKE` | API key | Community | | [Eclipse Ditto (digital twins)](/latest/lot-language/routes/rest-api-apps/eclipse-ditto) | `ECLIPSE_DITTO` | API key | Growth | | [Eclipse hawkBit (OTA)](/latest/lot-language/routes/rest-api-apps/eclipse-hawkbit) | `ECLIPSE_HAWKBIT` | API key | Growth | | [Golioth](/latest/lot-language/routes/rest-api-apps/golioth) | `GOLIOTH` | API key | Growth | | [Home Assistant](/latest/lot-language/routes/rest-api-apps/home-assistant) | `HOME_ASSISTANT` | API key | Community | | [Losant](/latest/lot-language/routes/rest-api-apps/losant) | `LOSANT` | Bearer | Community | | [Memfault](/latest/lot-language/routes/rest-api-apps/memfault) | `MEMFAULT` | Bearer | Growth | | [Mender](/latest/lot-language/routes/rest-api-apps/mender) | `MENDER` | Bearer | Community | | [Particle](/latest/lot-language/routes/rest-api-apps/particle) | `PARTICLE` | Bearer | Growth | | [Siemens Insights Hub (formerly MindSphere)](/latest/lot-language/routes/rest-api-apps/siemens-insights-hub) | `SIEMENS_INSIGHTS_HUB` | API key | Community | | [Soracom](/latest/lot-language/routes/rest-api-apps/soracom) | `SORACOM` | Login token | Growth | | [Teltonika RMS](/latest/lot-language/routes/rest-api-apps/teltonika-rms) | `TELTONIKA_RMS` | API key | Growth | | [The Things Stack (TTN and TTI)](/latest/lot-language/routes/rest-api-apps/the-things-stack) | `THE_THINGS_STACK` | API key | Community | | [ThingsBoard (CE, PE, Cloud)](/latest/lot-language/routes/rest-api-apps/thingsboard) | `THINGSBOARD` | API key | Community | | [Ubidots](/latest/lot-language/routes/rest-api-apps/ubidots) | `UBIDOTS` | API key | Community | ### Cloud storage | App | App code | Auth (`CLIENT_AUTH`) | Minimum tier | | - | - | - | - | | [Amazon S3](/latest/lot-language/routes/rest-api-apps/amazon-s3) | `AMAZON_S3` | AWS SigV4 | Community | | [Azure Blob Storage and Data Lake Gen2](/latest/lot-language/routes/rest-api-apps/azure-blob-storage-and-data-lake-gen2) | `AZURE_BLOB_STORAGE_AND_DATA_LAKE_GEN2` | API key | Community | | [Box](/latest/lot-language/routes/rest-api-apps/box) | [`BOX`](/latest/lot-language/routes/rest-api-apps/box) [`BOX_UPLOAD`](/latest/lot-language/routes/rest-api-apps/box-upload) | OAuth 2.0 client credentials | Community / Growth | | [Dropbox](/latest/lot-language/routes/rest-api-apps/dropbox) | [`DROPBOX`](/latest/lot-language/routes/rest-api-apps/dropbox) [`DROPBOX_CONTENT`](/latest/lot-language/routes/rest-api-apps/dropbox-content) [`DROPBOX_NOTIFY`](/latest/lot-language/routes/rest-api-apps/dropbox-notify) | OAuth 2.0 refresh token | Community / Growth | | [Google Cloud Storage](/latest/lot-language/routes/rest-api-apps/google-cloud-storage) | `GOOGLE_CLOUD_STORAGE` | Bearer | Community | | [Google Drive](/latest/lot-language/routes/rest-api-apps/google-drive) | `GOOGLE_DRIVE` | Bearer | Community | | [Google Sheets](/latest/lot-language/routes/rest-api-apps/google-sheets) | `GOOGLE_SHEETS` | Bearer | Community | | [MinIO](/latest/lot-language/routes/rest-api-apps/minio) | `MINIO` | API key | Community | | [Nextcloud](/latest/lot-language/routes/rest-api-apps/nextcloud) | `NEXTCLOUD` | Basic | Growth | | [SharePoint and OneDrive (Microsoft Graph)](/latest/lot-language/routes/rest-api-apps/sharepoint-and-onedrive) | `SHAREPOINT_AND_ONEDRIVE` | Bearer | Community | ### ERP | App | App code | Auth (`CLIENT_AUTH`) | Minimum tier | | - | - | - | - | | [Acumatica Cloud ERP](/latest/lot-language/routes/rest-api-apps/acumatica-cloud-erp) | `ACUMATICA_CLOUD_ERP` | OAuth 2.0 refresh token | Community | | [Coupa](/latest/lot-language/routes/rest-api-apps/coupa) | `COUPA` | OAuth 2.0 client credentials | Community | | [Dynamics 365 Business Central](/latest/lot-language/routes/rest-api-apps/dynamics365-bc) | `DYNAMICS365_BC` | Bearer | Community | | [Microsoft Dynamics 365 Finance and Supply Chain](/latest/lot-language/routes/rest-api-apps/dynamics365-fscm) | `DYNAMICS365_FSCM` | Bearer | Community | | [Epicor Kinetic](/latest/lot-language/routes/rest-api-apps/epicor-kinetic) | `EPICOR_KINETIC` | Bearer | Community | | [IFS Cloud](/latest/lot-language/routes/rest-api-apps/ifs-cloud) | `IFS_CLOUD` | Bearer | Community | | [Infor CloudSuite via ION API Gateway](/latest/lot-language/routes/rest-api-apps/infor-cloudsuite-via-ion-api-gateway) | `INFOR_CLOUDSUITE_VIA_ION_API_GATEWAY` | Bearer | Community | | [Oracle NetSuite (SuiteTalk REST)](/latest/lot-language/routes/rest-api-apps/netsuite) | `NETSUITE` | Bearer | Community | | [Odoo](/latest/lot-language/routes/rest-api-apps/odoo) | `ODOO` | Bearer | Community | | [Oracle E-Business Suite (Integrated SOA Gateway)](/latest/lot-language/routes/rest-api-apps/oracle-e-business-suite) | `ORACLE_E_BUSINESS_SUITE` | Basic | Community | | [Oracle Fusion Cloud ERP and SCM](/latest/lot-language/routes/rest-api-apps/oracle-fusion-erp) | `ORACLE_FUSION_ERP` | Bearer | Community | | [Oracle JD Edwards EnterpriseOne](/latest/lot-language/routes/rest-api-apps/oracle-jd-edwards) | `ORACLE_JD_EDWARDS` | API key | Community | | [QAD Adaptive ERP](/latest/lot-language/routes/rest-api-apps/qad-adaptive-erp) | `QAD_ADAPTIVE_ERP` | Basic | Growth | | [SAP Ariba](/latest/lot-language/routes/rest-api-apps/sap-ariba-open-api) | `SAP_ARIBA` | OAuth 2.0 client credentials | Community | | [SAP Business One Service Layer](/latest/lot-language/routes/rest-api-apps/sap-b1) | `SAP_B1` | Basic | Community | | [SAP S/4HANA Cloud](/latest/lot-language/routes/rest-api-apps/sap-s4hana-cloud) | `SAP_S4HANA_CLOUD` | Bearer | Community | | [SAP S/4HANA on-premise and ECC (SAP Gateway)](/latest/lot-language/routes/rest-api-apps/sap-s4hana-onprem) | `SAP_S4HANA_ONPREM` | Bearer | Community | ### Energy | App | App code | Auth (`CLIENT_AUTH`) | Minimum tier | | - | - | - | - | | [E-REDES and REN open data (Portugal)](/latest/lot-language/routes/rest-api-apps/eredes-open-data) | [`EREDES_OPEN_DATA`](/latest/lot-language/routes/rest-api-apps/eredes-open-data) [`REN_DATA_HUB`](/latest/lot-language/routes/rest-api-apps/ren-data-hub) | None | Community | | [Electricity Maps](/latest/lot-language/routes/rest-api-apps/electricity-maps) | `ELECTRICITY_MAPS` | API key | Growth | | [Enphase](/latest/lot-language/routes/rest-api-apps/enphase) | `ENPHASE` | Bearer | Community | | [Enphase Enlighten API v4](/latest/lot-language/routes/rest-api-apps/enphase-enlighten-api-v4) | `ENPHASE_ENLIGHTEN_API_V4` | API key | Growth | | [ENTSO-E Transparency Platform](/latest/lot-language/routes/rest-api-apps/entso-e-transparency-platform) | `ENTSO_E_TRANSPARENCY_PLATFORM` | API key | Community | | [Fronius Solar API (local)](/latest/lot-language/routes/rest-api-apps/fronius-solar-api) | `FRONIUS_SOLAR_API` | None | Community | | [OCPI 2.2.1 (EV charging roaming)](/latest/lot-language/routes/rest-api-apps/ocpi-2-2-1) | `OCPI_2_2_1` | API key | Community | | [OpenADR 3.0 (demand response)](/latest/lot-language/routes/rest-api-apps/openadr-3-0) | `OPENADR_3_0` | OAuth 2.0 client credentials | Growth | | [OpenEI](/latest/lot-language/routes/rest-api-apps/openei) | `OPENEI` | API key | Community | | [Project Haystack](/latest/lot-language/routes/rest-api-apps/project-haystack) | `PROJECT_HAYSTACK` | API key | Community | | [Shelly (Gen2+ local RPC and Shelly Cloud)](/latest/lot-language/routes/rest-api-apps/shelly-gen2-rpc) | [`SHELLY_GEN2_RPC`](/latest/lot-language/routes/rest-api-apps/shelly-gen2-rpc) [`SHELLY_CLOUD_CONTROL`](/latest/lot-language/routes/rest-api-apps/shelly-cloud-control) | API key | Community / Growth | | [SolarEdge](/latest/lot-language/routes/rest-api-apps/solaredge) | `SOLAREDGE` | API key | Community | | [SolarEdge Monitoring API](/latest/lot-language/routes/rest-api-apps/solaredge-monitoring-api) | `SOLAREDGE_MONITORING_API` | API key | Community | | [Victron VRM API](/latest/lot-language/routes/rest-api-apps/victron-vrm-api) | `VICTRON_VRM_API` | API key | Community | | [WattTime](/latest/lot-language/routes/rest-api-apps/watttime) | `WATTTIME` | Login token | Community | ### Field service | App | App code | Auth (`CLIENT_AUTH`) | Minimum tier | | - | - | - | - | | [Dynamics 365 Field Service and Connected Field Service](/latest/lot-language/routes/rest-api-apps/dynamics-365-field-service-and-connected-field-service) | `DYNAMICS_365_FIELD_SERVICE_AND_CONNECTED_FIELD_SERVICE` | Bearer | Community | | [Fracttal One](/latest/lot-language/routes/rest-api-apps/fracttal-one-api) | `FRACTTAL_ONE` | OAuth 2.0 client credentials | Community | | [HxGN EAM (Hexagon, formerly Infor EAM)](/latest/lot-language/routes/rest-api-apps/hxgn-eam) | `HXGN_EAM` | API key | Community | | [IBM Maximo Manage (Maximo Application Suite)](/latest/lot-language/routes/rest-api-apps/ibm-maximo-manage) | `IBM_MAXIMO_MANAGE` | API key | Community | | [Limble CMMS](/latest/lot-language/routes/rest-api-apps/limble-cmms) | `LIMBLE_CMMS` | Basic | Growth | | [MaintainX](/latest/lot-language/routes/rest-api-apps/maintainx) | `MAINTAINX` | Bearer | Community | | [SAP Field Service Management](/latest/lot-language/routes/rest-api-apps/sap-field-service-management) | `SAP_FIELD_SERVICE_MANAGEMENT` | OAuth 2.0 client credentials | Community | | [ServiceMax (PTC)](/latest/lot-language/routes/rest-api-apps/servicemax) | `SERVICEMAX` | OAuth 2.0 client credentials | Community | | [ServiceTitan](/latest/lot-language/routes/rest-api-apps/service-titan-api) | `SERVICETITAN` | OAuth 2.0 client credentials | Growth | | [UpKeep](/latest/lot-language/routes/rest-api-apps/upkeep) | `UPKEEP` | API key | Community | ### Finance | App | App code | Auth (`CLIENT_AUTH`) | Minimum tier | | - | - | - | - | | [DocuSign eSignature](/latest/lot-language/routes/rest-api-apps/docu-sign-esignature) | [`DOCU_SIGN_OAUTH`](/latest/lot-language/routes/rest-api-apps/docu-sign-oauth) [`DOCU_SIGN_ESIGNATURE`](/latest/lot-language/routes/rest-api-apps/docu-sign-esignature) | OAuth 2.0 JWT bearer | Community / Growth | | [QuickBooks Online](/latest/lot-language/routes/rest-api-apps/quickbooks-online) | `QUICKBOOKS_ONLINE` | OAuth 2.0 refresh token | Community | | [Xero](/latest/lot-language/routes/rest-api-apps/xero) | `XERO` | OAuth 2.0 client credentials | Community | ### Fleet | App | App code | Auth (`CLIENT_AUTH`) | Minimum tier | | - | - | - | - | | [DHL API Developer Portal](/latest/lot-language/routes/rest-api-apps/dhl-api) | [`DHL_API`](/latest/lot-language/routes/rest-api-apps/dhl-api) [`DHL_LOCATION_FINDER`](/latest/lot-language/routes/rest-api-apps/dhl-location-finder) [`MY_DHL_EXPRESS`](/latest/lot-language/routes/rest-api-apps/my-dhl-express) [`DHL_PARCEL_DE`](/latest/lot-language/routes/rest-api-apps/dhl-parcel-de) | API key, Basic, OAuth 2.0 password | Community | | [EasyPost](/latest/lot-language/routes/rest-api-apps/easypost) | `EASYPOST` | Basic | Growth | | [FedEx Developer Portal](/latest/lot-language/routes/rest-api-apps/fed-ex-auth) | [`FED_EX_AUTH`](/latest/lot-language/routes/rest-api-apps/fed-ex-auth) [`FED_EX_API`](/latest/lot-language/routes/rest-api-apps/fed-ex-api) [`FED_EX_DOCS`](/latest/lot-language/routes/rest-api-apps/fed-ex-docs) | Bearer | Community / Growth | | [Geotab (MyGeotab API)](/latest/lot-language/routes/rest-api-apps/geotab) | `GEOTAB` | None | Community | | [Samsara](/latest/lot-language/routes/rest-api-apps/samsara) | `SAMSARA` | Bearer | Community | | [Traccar (open-source GPS server)](/latest/lot-language/routes/rest-api-apps/traccar) | `TRACCAR` | Bearer | Community | | [Trackunit](/latest/lot-language/routes/rest-api-apps/trackunit-iris) | `TRACKUNIT` | OAuth 2.0 client credentials | Growth | | [UPS Developer Portal](/latest/lot-language/routes/rest-api-apps/ups-api) | `UPS_DEVELOPER_PORTAL` | OAuth 2.0 client credentials | Growth | | [Webfleet (Bridgestone)](/latest/lot-language/routes/rest-api-apps/webfleet) | `WEBFLEET` | Basic | Growth | ### HR | App | App code | Auth (`CLIENT_AUTH`) | Minimum tier | | - | - | - | - | | [SAP SuccessFactors](/latest/lot-language/routes/rest-api-apps/sap-successfactors) | `SAP_SUCCESSFACTORS` | OAuth 2.0 SAML bearer | Growth | | [Workday](/latest/lot-language/routes/rest-api-apps/workday) | `WORKDAY` | Bearer | Community | ### Identity | App | App code | Auth (`CLIENT_AUTH`) | Minimum tier | | - | - | - | - | | [Auth0](/latest/lot-language/routes/rest-api-apps/auth0) | `AUTH0` | Bearer | Community | | [Okta](/latest/lot-language/routes/rest-api-apps/okta) | `OKTA` | API key | Community | ### LLMs | App | App code | Auth (`CLIENT_AUTH`) | Minimum tier | | - | - | - | - | | [Amazon Bedrock](/latest/lot-language/routes/rest-api-apps/amazon-bedrock-runtime) | [`AMAZON_BEDROCK_RUNTIME`](/latest/lot-language/routes/rest-api-apps/amazon-bedrock-runtime) [`AMAZON_BEDROCK_CONTROL`](/latest/lot-language/routes/rest-api-apps/amazon-bedrock-control) | AWS SigV4 | Community | | [Anthropic](/latest/lot-language/routes/rest-api-apps/anthropic) | `ANTHROPIC` | API key | Community | | [Azure OpenAI and Azure AI Foundry](/latest/lot-language/routes/rest-api-apps/azure-openai-and-azure-ai-foundry) | `AZURE_OPENAI_AND_AZURE_AI_FOUNDRY` | API key | Community | | [Google Gemini API and Vertex AI](/latest/lot-language/routes/rest-api-apps/gemini-api) | [`GEMINI_API`](/latest/lot-language/routes/rest-api-apps/gemini-api) [`VERTEX_AI`](/latest/lot-language/routes/rest-api-apps/vertex-ai) | API key, OAuth 2.0 JWT bearer | Community | | [Hugging Face Inference Providers and Endpoints](/latest/lot-language/routes/rest-api-apps/hugging-face-inference-providers) | [`HUGGING_FACE_INFERENCE_PROVIDERS`](/latest/lot-language/routes/rest-api-apps/hugging-face-inference-providers) [`HUGGING_FACE_INFERENCE_ENDPOINTS`](/latest/lot-language/routes/rest-api-apps/hugging-face-inference-endpoints) [`HUGGING_FACE_DEDICATED_ENDPOINT`](/latest/lot-language/routes/rest-api-apps/hugging-face-dedicated-endpoint) [`HUGGING_FACE_HUB`](/latest/lot-language/routes/rest-api-apps/hugging-face-hub) | Bearer | Community | | [Mistral AI](/latest/lot-language/routes/rest-api-apps/mistral-ai) | `MISTRAL_AI` | Bearer | Community | | [Ollama (local models)](/latest/lot-language/routes/rest-api-apps/ollama) | `OLLAMA` | None | Community | | [OpenAI](/latest/lot-language/routes/rest-api-apps/openai) | `OPENAI` | Bearer | Community | ### Manufacturing | App | App code | Auth (`CLIENT_AUTH`) | Minimum tier | | - | - | - | - | | [Aras Innovator](/latest/lot-language/routes/rest-api-apps/aras-innovator-api) | `ARAS_INNOVATOR` | OAuth 2.0 refresh token | Community | | [Autodesk Platform Services (Data Management, Manufacturing Data Model, Fusion)](/latest/lot-language/routes/rest-api-apps/autodesk-platform-services-api) | `AUTODESK_PLATFORM_SERVICES` | OAuth 2.0 client credentials | Community | | [AVEVA MES (formerly Wonderware MES)](/latest/lot-language/routes/rest-api-apps/aveva-mes-api) | `AVEVA_MES` | OAuth 2.0 client credentials | Growth | | [BarTender (Seagull Scientific) label printing](/latest/lot-language/routes/rest-api-apps/bar-tender-actions) | [`BAR_TENDER_ACTIONS`](/latest/lot-language/routes/rest-api-apps/bar-tender-actions) [`BAR_TENDER_CLOUD`](/latest/lot-language/routes/rest-api-apps/bar-tender-cloud) [`BAR_TENDER_PRINT_PORTAL`](/latest/lot-language/routes/rest-api-apps/bar-tender-print-portal) | Bearer | Community | | [Critical Manufacturing MES](/latest/lot-language/routes/rest-api-apps/critical-manufacturing-mes) | `CRITICAL_MANUFACTURING_MES` | OAuth 2.0 client credentials | Growth | | [DELMIA Apriso (Dassault Systèmes)](/latest/lot-language/routes/rest-api-apps/delmia-apriso) | `DELMIA_APRISO` | OAuth 2.0 password | Community | | [MachineMetrics](/latest/lot-language/routes/rest-api-apps/machinemetrics) | `MACHINEMETRICS` | Bearer | Community | | [PTC Windchill (Windchill REST Services)](/latest/lot-language/routes/rest-api-apps/ptc-windchill) | `PTC_WINDCHILL` | API key | Community | | [QAD Redzone](/latest/lot-language/routes/rest-api-apps/qad-redzone) | `QAD_REDZONE` | OAuth 2.0 client credentials | Community | | [Rockwell Plex Smart Manufacturing Platform](/latest/lot-language/routes/rest-api-apps/rockwell-plex-smart-manufacturing-platform) | `ROCKWELL_PLEX_SMART_MANUFACTURING_PLATFORM` | API key | Community | | [SAP Digital Manufacturing](/latest/lot-language/routes/rest-api-apps/sap-digital-manufacturing) | `SAP_DIGITAL_MANUFACTURING` | Bearer | Community | | [Sepasoft MES for Ignition](/latest/lot-language/routes/rest-api-apps/sepasoft-mes-for-ignition) | `SEPASOFT_MES_FOR_IGNITION` | API key | Community | | [Siemens Industrial Edge Management](/latest/lot-language/routes/rest-api-apps/siemens-industrial-edge-management) | `SIEMENS_INDUSTRIAL_EDGE_MANAGEMENT` | Login token | Community | | [Siemens Opcenter Execution](/latest/lot-language/routes/rest-api-apps/siemens-opcenter-execution) | `SIEMENS_OPCENTER_EXECUTION` | API key | Community | | [Siemens Teamcenter](/latest/lot-language/routes/rest-api-apps/siemens-teamcenter) | `SIEMENS_TEAMCENTER` | None | Community | | [Sight Machine](/latest/lot-language/routes/rest-api-apps/sight-machine) | `SIGHT_MACHINE` | API key | Community | | [Tulip](/latest/lot-language/routes/rest-api-apps/tulip) | `TULIP` | Basic | Community | | [Veeva Vault (QMS, QualityDocs)](/latest/lot-language/routes/rest-api-apps/veeva-vault-api) | `VEEVA_VAULT` | Bearer | Community | ### Maps | App | App code | Auth (`CLIENT_AUTH`) | Minimum tier | | - | - | - | - | | [Google Maps Platform](/latest/lot-language/routes/rest-api-apps/google-maps-web-services) | [`GOOGLE_MAPS_WEB_SERVICES`](/latest/lot-language/routes/rest-api-apps/google-maps-web-services) [`GOOGLE_GEOLOCATION`](/latest/lot-language/routes/rest-api-apps/google-geolocation) [`GOOGLE_ROUTES`](/latest/lot-language/routes/rest-api-apps/google-routes) [`GOOGLE_ROADS`](/latest/lot-language/routes/rest-api-apps/google-roads) [`GOOGLE_PLACES`](/latest/lot-language/routes/rest-api-apps/google-places) [`GOOGLE_ADDRESS_VALIDATION`](/latest/lot-language/routes/rest-api-apps/google-address-validation) [`GOOGLE_WEATHER`](/latest/lot-language/routes/rest-api-apps/google-weather) [`GOOGLE_AIR_QUALITY`](/latest/lot-language/routes/rest-api-apps/google-air-quality) | API key | Community | | [HERE Geocoding](/latest/lot-language/routes/rest-api-apps/here-geocoding) | `HERE_GEOCODING` | API key | Community | | [HERE Platform](/latest/lot-language/routes/rest-api-apps/here-geocode) | [`HERE_GEOCODE`](/latest/lot-language/routes/rest-api-apps/here-geocode) [`HERE_REVERSE_GEOCODE`](/latest/lot-language/routes/rest-api-apps/here-reverse-geocode) [`HERE_MULTI_REVERSE_GEOCODE`](/latest/lot-language/routes/rest-api-apps/here-multi-reverse-geocode) [`HERE_DISCOVER`](/latest/lot-language/routes/rest-api-apps/here-discover) [`HERE_BROWSE`](/latest/lot-language/routes/rest-api-apps/here-browse) [`HERE_LOOKUP`](/latest/lot-language/routes/rest-api-apps/here-lookup) [`HERE_ROUTING`](/latest/lot-language/routes/rest-api-apps/here-routing) [`HERE_ISOLINE`](/latest/lot-language/routes/rest-api-apps/here-isoline) [`HERE_MATRIX`](/latest/lot-language/routes/rest-api-apps/here-matrix) [`HERE_TRAFFIC`](/latest/lot-language/routes/rest-api-apps/here-traffic) [`HERE_WAYPOINTS_SEQUENCE`](/latest/lot-language/routes/rest-api-apps/here-waypoints-sequence) [`HERE_ROUTE_MATCHING`](/latest/lot-language/routes/rest-api-apps/here-route-matching) [`HERE_TOUR_PLANNING`](/latest/lot-language/routes/rest-api-apps/here-tour-planning) | API key | Community | | [Mapbox](/latest/lot-language/routes/rest-api-apps/mapbox) | `MAPBOX` | API key | Community | | [OpenStreetMap: Nominatim and OSRM (self-hostable)](/latest/lot-language/routes/rest-api-apps/osm-nominatim) | [`OSM_NOMINATIM`](/latest/lot-language/routes/rest-api-apps/osm-nominatim) [`OSM_OSRM`](/latest/lot-language/routes/rest-api-apps/osm-osrm) | None | Community | ### Messaging | App | App code | Auth (`CLIENT_AUTH`) | Minimum tier | | - | - | - | - | | [Amazon SNS](/latest/lot-language/routes/rest-api-apps/amazon-sns) | `AMAZON_SNS` | AWS SigV4 | Community | | [Apprise API](/latest/lot-language/routes/rest-api-apps/apprise-api) | `APPRISE_API` | None | Community | | [Azure Service Bus](/latest/lot-language/routes/rest-api-apps/azure-service-bus) | `AZURE_SERVICE_BUS` | API key | Community | | [Discord](/latest/lot-language/routes/rest-api-apps/discord) | `DISCORD` | API key | Community | | [Firebase Cloud Messaging (HTTP v1)](/latest/lot-language/routes/rest-api-apps/firebase-cloud-messaging) | `FIREBASE_CLOUD_MESSAGING` | Bearer | Community | | [Mailgun](/latest/lot-language/routes/rest-api-apps/mailgun) | `MAILGUN` | Basic | Growth | | [Mattermost](/latest/lot-language/routes/rest-api-apps/mattermost) | `MATTERMOST` | Bearer | Community | | [Microsoft Teams (Graph and Workflows webhooks)](/latest/lot-language/routes/rest-api-apps/microsoft-teams) | `MICROSOFT_TEAMS` | Bearer | Community | | [ntfy](/latest/lot-language/routes/rest-api-apps/ntfy) | `NTFY` | Bearer | Community | | [Pushover](/latest/lot-language/routes/rest-api-apps/pushover) | `PUSHOVER` | API key | Community | | [SendGrid](/latest/lot-language/routes/rest-api-apps/sendgrid) | `SENDGRID` | Bearer | Community | | [Slack Web API](/latest/lot-language/routes/rest-api-apps/slack) | `SLACK` | Bearer | Community | | [Telegram Bot API](/latest/lot-language/routes/rest-api-apps/telegram-bot-api) | `TELEGRAM_BOT_API` | None | Community | | [Twilio (SMS, Voice, Verify, WhatsApp)](/latest/lot-language/routes/rest-api-apps/twilio) | `TWILIO` | Basic | Community | | [WhatsApp Business Platform (Meta Cloud API)](/latest/lot-language/routes/rest-api-apps/whatsapp-business-platform) | `WHATSAPP_BUSINESS_PLATFORM` | Bearer | Community | ### Monitoring | App | App code | Auth (`CLIENT_AUTH`) | Minimum tier | | - | - | - | - | | [Amazon CloudWatch](/latest/lot-language/routes/rest-api-apps/amazon-cloudwatch) | `AMAZON_CLOUDWATCH` | AWS SigV4 | Community | | [Azure Monitor](/latest/lot-language/routes/rest-api-apps/azure-monitor) | `AZURE_MONITOR` | Bearer | Community | | [BMC Helix ITSM](/latest/lot-language/routes/rest-api-apps/bmc-helix-itsm) | `BMC_HELIX_ITSM` | Login token | Growth | | [Datadog](/latest/lot-language/routes/rest-api-apps/datadog) | `DATADOG` | API key | Community | | [Dynatrace](/latest/lot-language/routes/rest-api-apps/dynatrace) | `DYNATRACE` | API key | Community | | [Grafana (OSS, Enterprise, Cloud)](/latest/lot-language/routes/rest-api-apps/grafana) | `GRAFANA` | Bearer | Community | | [Jira Service Management](/latest/lot-language/routes/rest-api-apps/jira-service-management) | `JIRA_SERVICE_MANAGEMENT` | Bearer | Community | | [New Relic](/latest/lot-language/routes/rest-api-apps/new-relic) | `NEW_RELIC` | API key | Community | | [Opsgenie](/latest/lot-language/routes/rest-api-apps/opsgenie) | `OPSGENIE` | API key | Community | | [PagerDuty](/latest/lot-language/routes/rest-api-apps/pagerduty) | `PAGERDUTY` | API key | Community | | [Prometheus, Alertmanager and Pushgateway](/latest/lot-language/routes/rest-api-apps/prometheus-alertmanager-and-pushgateway) | `PROMETHEUS_ALERTMANAGER_AND_PUSHGATEWAY` | None | Community | | [Sentry](/latest/lot-language/routes/rest-api-apps/sentry) | `SENTRY` | Bearer | Community | | [ServiceNow](/latest/lot-language/routes/rest-api-apps/servicenow) | `SERVICENOW` | Bearer | Community | | [Splunk Enterprise and Cloud](/latest/lot-language/routes/rest-api-apps/splunk-enterprise-and-cloud) | `SPLUNK_ENTERPRISE_AND_CLOUD` | API key | Community | | [Zabbix](/latest/lot-language/routes/rest-api-apps/zabbix) | `ZABBIX` | Bearer | Community | ### Payments | App | App code | Auth (`CLIENT_AUTH`) | Minimum tier | | - | - | - | - | | [Shopify](/latest/lot-language/routes/rest-api-apps/shopify) | `SHOPIFY` | API key | Community | | [Stripe](/latest/lot-language/routes/rest-api-apps/stripe) | `STRIPE` | Bearer | Community | ### Public data | App | App code | Auth (`CLIENT_AUTH`) | Minimum tier | | - | - | - | - | | [Open-Meteo](/latest/lot-language/routes/rest-api-apps/open-meteo) | `OPEN_METEO` | None | Community | | [OpenAQ](/latest/lot-language/routes/rest-api-apps/openaq) | `OPENAQ` | API key | Community | | [OpenWeatherMap](/latest/lot-language/routes/rest-api-apps/openweathermap) | `OPENWEATHERMAP` | API key | Community | | [PurpleAir](/latest/lot-language/routes/rest-api-apps/purpleair) | `PURPLEAIR` | API key | Community | ### SCADA | App | App code | Auth (`CLIENT_AUTH`) | Minimum tier | | - | - | - | - | | [AVEVA CONNECT data services (formerly AVEVA Data Hub / OSIsoft Cloud Services)](/latest/lot-language/routes/rest-api-apps/aveva-connect-data-services) | `AVEVA_CONNECT_DATA_SERVICES` | Bearer | Community | | [AVEVA PI Web API (PI System, FactoryTalk Historian)](/latest/lot-language/routes/rest-api-apps/aveva-pi-web-api) | `AVEVA_PI_WEB_API` | Bearer | Community | | [Canary Historian](/latest/lot-language/routes/rest-api-apps/canary-read) | [`CANARY_READ`](/latest/lot-language/routes/rest-api-apps/canary-read) [`CANARY_WRITE`](/latest/lot-language/routes/rest-api-apps/canary-write) | None | Community | | [Clarify (Searis)](/latest/lot-language/routes/rest-api-apps/clarify) | `CLARIFY` | OAuth 2.0 client credentials | Community | | [GE Vernova Proficy Historian](/latest/lot-language/routes/rest-api-apps/ge-vernova-proficy-historian) | `GE_VERNOVA_PROFICY_HISTORIAN` | OAuth 2.0 client credentials | Community | | [HighByte Intelligence Hub](/latest/lot-language/routes/rest-api-apps/high-byte-data) | [`HIGH_BYTE_DATA`](/latest/lot-language/routes/rest-api-apps/high-byte-data) [`HIGH_BYTE_CONFIG`](/latest/lot-language/routes/rest-api-apps/high-byte-config) | Bearer | Community | | [Ignition (Inductive Automation)](/latest/lot-language/routes/rest-api-apps/ignition) | `IGNITION` | API key | Community | | [Kepware (PTC KEPServerEX, ThingWorx Kepware Server, Kepware+)](/latest/lot-language/routes/rest-api-apps/kepware-config) | [`KEPWARE_CONFIG`](/latest/lot-language/routes/rest-api-apps/kepware-config) [`KEPWARE_IOT_GATEWAY`](/latest/lot-language/routes/rest-api-apps/kepware-iot-gateway) | API key | Community | | [Litmus Edge](/latest/lot-language/routes/rest-api-apps/litmus-edge) | `LITMUS_EDGE` | OAuth 2.0 client credentials | Community | | [N3uron](/latest/lot-language/routes/rest-api-apps/n3uron) | `N3URON` | API key | Community | | [Node-RED (Admin API, HTTP In nodes) and FlowFuse](/latest/lot-language/routes/rest-api-apps/node-red-admin) | [`NODE_RED_ADMIN`](/latest/lot-language/routes/rest-api-apps/node-red-admin) [`NODE_RED_HTTP_IN`](/latest/lot-language/routes/rest-api-apps/node-red-http-in) [`FLOW_FUSE_API`](/latest/lot-language/routes/rest-api-apps/flow-fuse-api) | Bearer | Community | | [Rhize (ISA-95 manufacturing data hub)](/latest/lot-language/routes/rest-api-apps/rhize-api) | `RHIZE` | OAuth 2.0 client credentials | Community | | [Seeq](/latest/lot-language/routes/rest-api-apps/seeq) | `SEEQ` | Login token | Community | | [Siemens WinCC Unified (GraphQL server)](/latest/lot-language/routes/rest-api-apps/siemens-wincc-unified) | `SIEMENS_WINCC_UNIFIED` | Bearer | Community | ### Testing | App | App code | Auth (`CLIENT_AUTH`) | Minimum tier | | - | - | - | - | | [HTTPBin](/latest/lot-language/routes/rest-api-apps/httpbin) | `HTTPBIN` | Bearer | Community | | [JSONPlaceholder](/latest/lot-language/routes/rest-api-apps/jsonplaceholder) | `JSONPLACEHOLDER` | Bearer | Community | | [Postman Echo](/latest/lot-language/routes/rest-api-apps/postman-echo) | `POSTMAN_ECHO` | Bearer | Community | ### Work and DevOps | App | App code | Auth (`CLIENT_AUTH`) | Minimum tier | | - | - | - | - | | [Airtable](/latest/lot-language/routes/rest-api-apps/airtable) | `AIRTABLE` | Bearer | Community | | [Asana](/latest/lot-language/routes/rest-api-apps/asana) | `ASANA` | Bearer | Community | | [Azure DevOps](/latest/lot-language/routes/rest-api-apps/azure-devops) | `AZURE_DEVOPS` | API key | Growth | | [Docker Hub](/latest/lot-language/routes/rest-api-apps/docker-hub) | `DOCKER_HUB` | Bearer | Community | | [Excel Online](/latest/lot-language/routes/rest-api-apps/excel-online) | `EXCEL_ONLINE` | Bearer | Community | | [GitHub REST](/latest/lot-language/routes/rest-api-apps/github) | `GITHUB` | Bearer | Community | | [GitLab](/latest/lot-language/routes/rest-api-apps/gitlab) | `GITLAB` | API key | Community | | [Jira Cloud (Software)](/latest/lot-language/routes/rest-api-apps/jira-cloud) | `JIRA_CLOUD` | Bearer | Community | | [monday.com](/latest/lot-language/routes/rest-api-apps/monday-com) | `MONDAY_COM` | API key | Community | | [Notion](/latest/lot-language/routes/rest-api-apps/notion) | `NOTION` | Bearer | Community | | [Smartsheet](/latest/lot-language/routes/rest-api-apps/smartsheet) | `SMARTSHEET` | Bearer | Growth | | [Trello](/latest/lot-language/routes/rest-api-apps/trello) | `TRELLO` | API key | Growth | ## Related * [REST API Connectors](/latest/lot-language/routes/rest-api-routes) ## Discover connectors The broker ships every REST API app in this catalog as a connector template. List them all with `-listTemplates`, and get one template with `-connectorCode` (`-routeCode` remains a supported alias). Publish both commands to `$SYS/Coreflux/Command`: | Command | What it returns | | - | - | | `-listTemplates` (`--list-templates`) | JSON object keyed by template type, one card per built-in connector template and per REST API connector template. | | `-connectorCode ` (`--connector-code`; alias `-routeCode`) | The JSON card for one template. `` is a connector type such as `REST_API`, or a REST API app code such as `AIRTABLE`, `SAP_S4HANA_CLOUD` or `GEMINI_API`. | An app with one connector is keyed by its folder name, upper-cased with every non-alphanumeric run turned into `_` (`sap-s4hana-cloud` becomes `SAP_S4HANA_CLOUD`). An app with several connectors keys each one by its connector name without the `RestApi` suffix (`GeminiApiRestApi` becomes `GEMINI_API`). The [catalog](/latest/lot-language/routes/rest-api-apps#catalog) lists every app code. Each card has this shape (from `RouteHandler.BuildTemplatePayload`): ```json theme={"theme":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} { "type": "AIRTABLE", "baseType": "REST_API", "connector": true, "template": "DEFINE CONNECTOR #RouteName,string,...# WITH TYPE REST_API ...", "metadata": { "name": "Airtable", "image": "", "status": "beta", "category": "...", "description": "...", "doclink": "https://docs.coreflux.org/.../rest-api-apps/airtable", "license": "community", "minimumTier": "Community", "actionSlots": 5, "availableOnThisBroker": true, "requiredSecrets": ["AIRTABLE_API_TOKEN"] } } ``` * `status` is `beta` for every app today: the broker accepts no other value. The docs call this technical preview. * `license` is `community` when a Community broker can deploy the template, otherwise `licensed`. `minimumTier` is `Community`, `Growth` or `Enterprise`, `actionSlots` is the number of actions the connector registers, and `availableOnThisBroker` says whether the current license can deploy it. * `requiredSecrets` lists the broker secrets the template reads (`GET SECRET` or `{secret.NAME}`). Create them before you deploy. * `connector` is `true` and `baseType` is `REST_API` for every vendor connector. For built-in connector templates `connector` is `false` and `baseType` equals `type`. * An app code is not a connector type: deploy the `template` with `WITH TYPE REST_API`, not `WITH TYPE AIRTABLE`. * `template` is the deployable connector code only. The `ADD METADATA` block is split off into `metadata`, because `-addConnector` rejects it. * `#Name,type,description,regex,required#` markers are Hub placeholders (for example `#RouteName#` and `#BaseUrl#`). Replace them with real values before you deploy with `-addConnector`. ```bash theme={"theme":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} mosquitto_pub -t '$SYS/Coreflux/Command' -m "-listTemplates" mosquitto_pub -t '$SYS/Coreflux/Command' -m "-connectorCode AIRTABLE" ``` # REST API Connectors 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` connector 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 > [!TIP] > > REST API connectors are ideal for integrating with web services, webhooks, cloud APIs, and creating HTTP-to-MQTT bridges. ## Basic Syntax ```lot wrap theme={"theme":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} DEFINE CONNECTOR 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 Connectors Work A REST API connector 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":{"light":"css-variables","dark":"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 > **Client Settings** > > **BASE\_ADDRESS** `string` *required* > > Base URL for HTTP requests (e.g., `https://api.example.com`). > > **ENABLE\_CLIENT** `boolean` > > Enable HTTP client functionality. Default: false. > > **REQUEST\_TIMEOUT** `integer` > > Request timeout in seconds. Default: 30. > > **DEFAULT\_CONTENT\_TYPE** `string` > > Default content type. Default: application/json. > **Server Settings** > > **ENABLE\_SERVER** `boolean` > > Enable HTTP server functionality. Default: false. > > **SERVER\_PORT** `integer` > > HTTP server port. Default: 8080. > **Authentication** > > **USERNAME** `string` > > Basic authentication username. > > **PASSWORD** `string` > > Basic authentication password. > **SSL/Security** > > **USE\_SSL** `boolean` > > Enable SSL/TLS. Default: false. > > **IGNORE\_CERT\_ERRORS** `boolean` > > Ignore certificate validation errors. Default: false. > **CORS** > > **ENABLE\_CORS** `boolean` > > Enable CORS headers. Default: false. > > **CORS\_ORIGINS** `string` > > 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":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} flowchart LR A["MQTT: weather/request
{city: 'London'}"] -->|triggers| B[REST API Connector] B -->|GET /weather?q=London| C[External API] C -->|JSON response| B B -->|publishes| D["MQTT: weather/response
{temp: 15, ...}"] ``` ### Event Configuration > **Trigger & Routing** > > **SOURCE\_TOPIC** `string` > > MQTT topic that triggers the HTTP request. > > **DESTINATION\_TOPIC** `string` > > MQTT topic to publish HTTP response. > **HTTP Request** > > **METHOD** `string` > > HTTP method: `GET`, `POST`, `PUT`, `DELETE`, `PATCH`. > > **ENDPOINT** `string` > > API endpoint path (supports placeholders). > > **BODY** `string` > > 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 | A placeholder in `ENDPOINT` stays on the connector's `BASE_ADDRESS`. A value that points at another host, such as `/evil.example/x` or `https://evil.example/x`, is refused, and the reply topic gets a `RequestRejected` error. An `ENDPOINT` you write as a full URL still uses that URL. ### Client Examples **GET Request** Fetch data from an external API when an MQTT message arrives: ```lot wrap theme={"theme":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} DEFINE CONNECTOR 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 connector calls the API and publishes the weather data to `weather/response`. **POST Request** Send data to an external API: ```lot wrap theme={"theme":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} DEFINE CONNECTOR 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}`. **With Authentication** Use basic authentication for protected APIs: ```lot wrap theme={"theme":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} DEFINE CONNECTOR 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. > [!TIP] > > **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 > **Endpoint & Method** > > **ENDPOINT** `string` > > The HTTP path to expose (e.g., `/api/sensors`). > > **METHOD** `string` > > HTTP method to accept: `GET`, `POST`, `PUT`, `DELETE`. > **MQTT Routing** > > **DESTINATION\_TOPIC** `string` > > MQTT topic where incoming HTTP data is published. ### Server Examples **Accept Sensor Data** Expose an endpoint for devices to submit readings via HTTP: ```lot wrap theme={"theme":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} DEFINE CONNECTOR 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":{"light":"css-variables","dark":"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. **Multiple Endpoints** Expose different endpoints for different data types: ```lot wrap theme={"theme":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} DEFINE CONNECTOR 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 connectors to a different MQTT topic for organized data handling. **Webhook Receiver** Accept webhooks from external services: ```lot wrap theme={"theme":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} DEFINE CONNECTOR 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. **CORS for Web Apps** Enable cross-origin requests for browser-based applications: ```lot wrap theme={"theme":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} DEFINE CONNECTOR 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 connector can operate in both modes—accepting HTTP requests AND calling external APIs: ```lot wrap theme={"theme":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} DEFINE CONNECTOR 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 **IoT Dashboard API** Full API for a web dashboard to read sensor data and send commands: ```lot wrap theme={"theme":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} DEFINE CONNECTOR 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" ``` **Third-Party Integration** Integrate with an external CRM or ERP system: ```lot wrap theme={"theme":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} DEFINE CONNECTOR 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 > **Connection Timeout** > > * Verify BASE\_ADDRESS is correct > * Check firewall allows outbound connections > * Increase REQUEST\_TIMEOUT if API is slow > * Verify network connectivity > **SSL/TLS Errors** > > * Ensure USE\_SSL matches the API requirements > * For self-signed certificates, set IGNORE\_CERT\_ERRORS (not recommended for production) > * Verify certificate chain is complete > **Authentication Failed** > > * Verify USERNAME and PASSWORD are correct > * Check Authorization header format > * Ensure API key is valid and not expired > **CORS Errors** > > * Enable CORS on the server: `WITH ENABLE_CORS "true"` > * Configure specific allowed origins > * Check browser console for specific CORS error messages > **No Response Published** > > * Verify DESTINATION\_TOPIC is configured > * Check MQTT broker connectivity > * Monitor broker logs for errors *** ## Best Practices > **Use HTTPS in Production** > > Always use SSL/TLS for external API connections: > > ```lot wrap theme={"theme":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} > WITH USE_SSL "true" > ``` > **Set Appropriate Timeouts** > > Configure timeouts based on expected API response times: > > ```lot wrap theme={"theme":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} > WITH REQUEST_TIMEOUT '30' > ``` > **Handle Errors Gracefully** > > Subscribe to response topics to monitor API call results and handle failures in your Actions. > **Secure Server Endpoints** > > When exposing HTTP server endpoints: > > * Use authentication where needed > * Limit CORS origins to specific domains > * Consider rate limiting at the network level > **Use Environment-Specific Base URLs** > > Configure different base URLs for development and production environments. *** ## REST API app templates Ready-made client connectors (public test APIs, weather, Slack, GitHub, and planned enterprise systems such as SAP HANA and Salesforce) are listed in the [REST API Apps](/latest/lot-language/routes/rest-api-apps) catalog. Each app page shows the LoT connector definition, the event table, and how auth is passed. > [!NOTE] > > That catalog does not include competitor brokers (HiveMQ, EMQX, ThingWorx) or OEM device connectors. ## Discover templates and REST API apps: -listTemplates and -connectorCode The broker ships every REST API app on this catalog as a template, keyed by its **app code**. Two broker commands, published to `$SYS/Coreflux/Command`, expose them (defined in `Commands/Commands.cs`): | Command | What it returns | | - | - | | `-listTemplates` (`--list-templates`) | JSON object keyed by template type, one card per built-in connector template and per REST API app. | | `-connectorCode ` (`--connector-code`; alias `-routeCode`) | The JSON card for one template. The argument is a connector type such as `REST_API`, or a REST API app code such as `AIRTABLE`, `SAP_S4HANA_CLOUD` or `GEMINI_API`. | An app with one connector is keyed by its folder name, upper-cased with every non-alphanumeric run turned into `_` (`sap-s4hana-cloud` becomes `SAP_S4HANA_CLOUD`). An app with several connectors keys each one by its connector name without the `RestApi` suffix (`GeminiApiRestApi` becomes `GEMINI_API`). The [catalog](/latest/lot-language/routes/rest-api-apps#catalog) lists every app code. Each card has this shape (from `RouteHandler.BuildTemplatePayload`): ```json theme={"theme":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} { "type": "AIRTABLE", "baseType": "REST_API", "connector": true, "template": "DEFINE CONNECTOR #RouteName,string,...# WITH TYPE REST_API ...", "metadata": { "name": "Airtable", "image": "", "status": "beta", "category": "...", "description": "...", "doclink": "https://docs.coreflux.org/.../rest-api-apps/airtable", "license": "community", "minimumTier": "Community", "actionSlots": 5, "availableOnThisBroker": true, "requiredSecrets": ["AIRTABLE_API_TOKEN"] } } ``` * `status` is `beta` for every app today: the broker accepts no other value. The docs call this technical preview. * `license` is `community` when a Community broker can deploy the template, otherwise `licensed`. `minimumTier` is `Community`, `Growth` or `Enterprise`, `actionSlots` is the number of actions the connector registers, and `availableOnThisBroker` says whether the current license can deploy it. * `requiredSecrets` lists the broker secrets the template reads (`GET SECRET` or `{secret.NAME}`). Create them before you deploy. * `connector` is `true` and `baseType` is `REST_API` for every REST API app. For built-in connector templates `connector` is `false` and `baseType` equals `type`. The field keeps the name `connector` for compatibility; it marks a REST API app. * An app code is not a connector type: deploy the `template` with `WITH TYPE REST_API`, not `WITH TYPE AIRTABLE`. * `template` is the deployable connector code only. The `ADD METADATA` block is split off into `metadata`, because `-addConnector` rejects it. * `#Name,type,description,regex,required#` markers are Hub placeholders (for example `#RouteName#` and `#BaseUrl#`). Replace them with real values before you deploy with `-addConnector` (`-addRoute` remains a supported alias). ```bash theme={"theme":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} mosquitto_pub -t '$SYS/Coreflux/Command' -m "-listTemplates" mosquitto_pub -t '$SYS/Coreflux/Command' -m "-connectorCode AIRTABLE" ``` ## Next Steps > **[Streaming Connectors](./streaming/kafka)** > > Stream MQTT messages through Kafka. > **[Connector Examples](./examples)** > > See complete connector configuration examples. # Store and Forward Configuration Source: https://docs.coreflux.org/latest/lot-language/routes/store-and-forward How LoT connectors buffer MQTT messages when a destination is slow, cap the buffer with ADD CONNECTOR_CONFIG, and choose backpressure or drop ## Store and forward in LoT connectors Store and forward means holding messages while the next hop is slow or unavailable, then passing them on when it catches up. In Coreflux, every LoT connector sits behind a shared, **bounded, in-memory queue** between the broker and the connector's handlers. `ADD CONNECTOR_CONFIG` decides what each connector does when that queue is full: wait (backpressure) or drop the newest message. `ADD ROUTE_CONFIG` remains a supported alias of the same block. > [!TIP] > > **Like a ticket window with a limited line.** People already in line are always served, in order. 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. ### The principles, and how LoT handles them | Principle | In LoT connectors | | - | - | | **Buffer while the destination is slow** | Matching messages wait in a shared in-memory queue that one consumer drains in order. | | **Bounded queues** | The shared queue holds `COREFLUX_ROUTE_CHANNEL_CAPACITY` messages (default `100000`). `OVERLOAD_CAPACITY` puts a private cap on one connector. | | **Backpressure or drop** | `lossless` makes the publisher wait. `balanced` waits up to `OVERLOAD_TIMEOUT`, then drops. `realtime` drops at once. Only the **arriving** message is ever dropped. | | **Ordering** | First in, first out. An accepted message is never evicted or reordered, so a sequence such as Sparkplug birth then data stays consistent. | | **Delivery guarantee** | At most once into the connector. `lossless` means "never dropped by this queue", not "delivered to the destination". | | **Survives a restart** | No. The queue lives in memory. Anything still queued when the broker stops is lost. | ### What it does not do > [!WARNING] > > This is not a complete store-and-forward system. The connector queue: > > * **does not write to disk**, so queued messages don't survive a broker restart or crash, > * **does not replay** messages after a destination comes back. Once a connector has taken a message from the queue, retrying a failed write is up to that connector type, and many simply log the failure, > * **does not acknowledge end to end**: a publisher's QoS 1/2 acknowledgement means the broker accepted the message, not that the destination stored it. ### Filling the gaps * **Upstream, use MQTT itself.** Publish with QoS 1 or 2 from clients with persistent sessions (`cleanSession=false`, or a session expiry in MQTT 5), so the broker keeps their messages while they are offline. The broker's [durability](/latest/mqtt-broker/durability) settings decide what survives a broker restart. * **Store first, forward later.** Write each message to a database connector (or a model with `STORE IN`), then forward from that table on a schedule. The table is the durable buffer and can be replayed. * **Let the destination buffer.** Kafka, and a remote MQTT broker with persistent sessions, keep their own durable queues once a message reaches them. * **Watch for drops.** The broker logs when a connector starts dropping. Treat that as an alert. ### When to use it * Leave the default (`balanced`) on most connectors. * 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 connector so it cannot crowd out others. `ADD CONNECTOR_CONFIG` is the only LoT block that works on **every** connector 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 **Never drop a message** Use `lossless` on a forwarding path that must keep every message. The publisher waits until this connector has room — it never drops. ```lot wrap focus={3-4} theme={"theme":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} DEFINE CONNECTOR CloudBridge WITH TYPE MQTT_BRIDGE ADD CONNECTOR_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" ``` **Never stall the publisher** Use `realtime` on high-rate telemetry. If the connector is full, the arriving message is dropped immediately and the publishing client is not delayed. ```lot wrap focus={3-4} theme={"theme":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} DEFINE CONNECTOR SensorDB WITH TYPE POSTGRESQL ADD CONNECTOR_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 a noisy connector** Cap how many of **this connector's** messages may be in flight. Extra messages from this connector drop even while other connectors still have room. ```lot wrap focus={3-4} theme={"theme":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} DEFINE CONNECTOR NoisySensors WITH TYPE MQTT_BRIDGE ADD CONNECTOR_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 CONNECTOR_CONFIG` block inherits the broker default entirely. ```lot wrap focus={3-6} theme={"theme":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} DEFINE CONNECTOR CloudBridge WITH TYPE MQTT_BRIDGE ADD CONNECTOR_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 connector 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 connector's** messages allowed in flight at once. Once reached, further messages from this connector drop even if other connectors still have room. Must be greater than zero. | A message already accepted for a connector is never evicted. Only the arriving message can be dropped. That keeps sequences such as Sparkplug birth then data self-consistent under load. > [!WARNING] > > 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 connector, **ignores the capacity**, and runs the connector as lossless. The same conflict applies if the connector inherits `lossless` from the broker default and then declares a capacity. *** ## Defaults Each setting resolves on its own: 1. **No `ADD CONNECTOR_CONFIG` block** — the connector 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 connector 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 connector's own `ADD CONNECTOR_CONFIG` always overrides these for that connector. | 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 connector 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 connector is a **private** budget for that connector. The shared ceiling is separate: one connector can hit its own cap and start dropping while other connectors 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 connector 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 connector. The broker logs two different saturation conditions (each at most once every five seconds): * The shared connector queue is full — every connector waiting behind it is affected. * A single connector is over the `OVERLOAD_CAPACITY` it declared for itself — the shared queue may still have room. > [!WARNING] > > If two connectors 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` connector on an overlapping pattern can make a `realtime` connector wait, even if only the `realtime` connector would have handled the message. The broker never drops a message because a more lenient connector 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 > **A connector drops messages even though the broker looks idle** > > That connector's own `OVERLOAD_CAPACITY` is exhausted. Look for the broker warning that a connector is over its in-flight budget, not the warning that the shared queue is full. > **Publishers on a lossless connector stall under load** > > 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 connector with `OVERLOAD_CAPACITY` so it drops its own overflow instead. > **OVERLOAD\_CAPACITY seems to have no effect** > > 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. > **A connector with no wildcard still waits longer than its own policy** > > Another connector's overlapping wildcard pattern resolved a stricter policy for that topic. See the overlapping-pattern warning above. > **The connector drops immediately and never waits** > > The resolved policy is `realtime`. That is expected — `realtime` drops without waiting. *** ## Best practices * Start with the default `balanced` policy. Change a connector 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 connector, not on the critical one — the critical connector can stay unbounded while the noisy connector drops its own overflow. * Prefer exact topic mappings when two connectors would otherwise share a broad `+` / `#` pattern with different policies. * Invalid settings never fail the connector silently: read the broker log after deploy if behavior does not match the block you wrote. *** ## Next Steps > **[Connectors Overview](./overview)** > > Define connectors, mappings, and status topics. > **[Broker Configuration](/latest/mqtt-broker/configuration#performance-and-back-pressure)** > > Set broker-wide queue defaults and other limits. # gRPC Connector Source: https://docs.coreflux.org/latest/lot-language/routes/streaming/grpc Call an external gRPC service from the Coreflux broker with TYPE GRPC — unary, streaming, and per-device bidirectional sessions ## Call a gRPC Service from MQTT A gRPC connector opens a channel from the broker to an external gRPC service and maps MQTT topics onto that service's calls. Publish bytes on a source topic and the connector sends them; replies are published back onto a destination topic. > [!WARNING] > > This page is the **gRPC client connector** (`DEFINE CONNECTOR … WITH TYPE GRPC`). The broker connects *out* to a gRPC service. You do not call it from an Action — there is no `CALL GRPC`. > > To let an AI client operate **this** broker, use the [MCP Server](/latest/mqtt-broker/mcp-server). The hosted [documentation MCP](/ai/mcp) searches docs.coreflux.org and does not open a gRPC channel. > [!TIP] > > **Like a phone line to another building.** The connector keeps one line open to the service (the channel). Each conversation on that line is a call: MQTT bytes go in, and the reply bytes come back on another topic. > [!NOTE] > > gRPC connectors are **beta**. Coreflux HUB lists the gRPC template with the other streaming connectors when the broker exposes it. The fields on that template are the same `GRPC_CONFIG` parameters as the LoT on this page. ### When to use it * A service you already run speaks gRPC, and MQTT messages should invoke it without a separate gateway. * Each device needs its own long-lived stream. With the default session scope, one IMEI (or any other id in the source topic) gets its own bidirectional call. * The exchange is one request and one reply, a server stream, a client stream, or a bidirectional stream. Use a [REST API connector](../rest-api-routes) for HTTP. Use the [Kafka connector](./kafka) for an event log. Encode and decode protobuf with [LoT models](/latest/lot-language/models/protobuf) — this connector forwards raw bytes and does not read a `.proto` file. *** Define the channel once, then map the MQTT topics that feed it. ## Quick Start **Unary call** One MQTT message opens one call and publishes the single reply: ```lot wrap theme={"theme":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} DEFINE CONNECTOR EchoGrpc WITH TYPE GRPC ADD GRPC_CONFIG WITH ADDRESS "grpc.example.com" WITH PORT '50051' WITH USE_TLS "false" WITH SERVICE "echo.Echo" WITH METHOD "Unary" WITH RPC_TYPE "UNARY" ADD EVENT CallEcho WITH SOURCE_TOPIC "grpc/echo/in" WITH DESTINATION_TOPIC "grpc/echo/out" ``` A payload on `grpc/echo/in` is the request. The response bytes are published to `grpc/echo/out`. **Server stream** One request, then every message the server sends until it finishes: ```lot wrap theme={"theme":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} DEFINE CONNECTOR TickStream WITH TYPE GRPC ADD GRPC_CONFIG WITH ADDRESS GET ENV "GRPC_HOST" WITH PORT '50051' WITH USE_TLS "false" WITH SERVICE "echo.Echo" WITH METHOD "ServerStream" WITH RPC_TYPE "SERVER_STREAM" ADD EVENT Tick WITH SOURCE_TOPIC "grpc/ticks/request" WITH DESTINATION_TOPIC "grpc/ticks/data" ``` Each server message is published to `grpc/ticks/data` as it arrives. **One call per device** A bidirectional call per device topic. The id in the topic — an IMEI, a serial number, a session id — is the call key: ```lot wrap theme={"theme":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} DEFINE CONNECTOR DeviceBidi WITH TYPE GRPC ADD GRPC_CONFIG WITH ADDRESS GET ENV "GRPC_HOST" WITH PORT '443' WITH USE_TLS "true" WITH SERVICE "echo.Echo" WITH METHOD "Bidi" WITH RPC_TYPE "BIDI" WITH SESSION_SCOPE "TOPIC" WITH DEADLINE '300' WITH IDLE_TIMEOUT '15' ADD EVENT Converse WITH SOURCE_TOPIC "devices/+/uplink" WITH DESTINATION_TOPIC "devices/{topic.2}/downlink" ``` `devices/490154203237518/uplink` keeps its own call. A second IMEI does not wait on that call or close it. Replies for the first device are published to `devices/490154203237518/downlink`. `{topic.N}` is 1-based, so the device id is segment 2. > [!NOTE] > > `USE_TLS "false"` is for a service that speaks plaintext HTTP/2. The default is TLS on port `443`. Use `127.0.0.1` only when that service listens on the broker host. Set `ADDRESS` with [`GET ENV`](/latest/mqtt-broker/secrets-and-env) and credentials with `GET SECRET`. *** ## How a Call Is Chosen One connector is one channel and one RPC method (`SERVICE` + `METHOD`). HTTP/2 carries many calls on that channel. Add another connector when you need a second method. | LoT | What it sets | | - | - | | `ADDRESS`, `PORT`, `USE_TLS` | Channel to the service | | `SERVICE`, `METHOD` | Which RPC to call | | `RPC_TYPE` | Unary, server stream, client stream, or bidirectional | | `SOURCE_TOPIC` | MQTT topic that supplies request bytes | | `DESTINATION_TOPIC` | MQTT topic that receives response bytes | | `SESSION_SCOPE` | Whether streaming calls are per topic or shared by the event | | `CLIENT_AUTH`, `METADATA` | Initial metadata on the call, including `authorization` | There is no session-key setting. Put application ids inside the payload. The connector correlates calls with the MQTT topic match. `ADD CONNECTOR_CONFIG` still controls wait-or-drop when this connector falls behind incoming MQTT (`ADD ROUTE_CONFIG` remains a supported alias). See [Store and Forward Configuration](../store-and-forward). *** ## GRPC\_CONFIG | Parameter | Required | Default | Description | | - | - | - | - | | `ADDRESS` | Yes | — | Host name or IP of the gRPC service. A `dns:///`, `https://`, or `http://` prefix is removed. A `:port` suffix overrides `PORT` (do not append `:port` to a bracketed IPv6 address — set `PORT` instead). | | `PORT` | No | `443` | Channel port, unless `ADDRESS` already includes a port. | | `USE_TLS` | No | `true` | `true` uses TLS. `false` uses plaintext HTTP/2. The scheme in `ADDRESS` does not override this. | | `SERVICE` | Yes | — | Fully qualified service name (`package.Service`). | | `METHOD` | Yes | — | RPC method name. | | `RPC_TYPE` | No | `UNARY` | `UNARY`, `SERVER_STREAM`, `CLIENT_STREAM`, or `BIDI`. | | `DEADLINE` | No | `30` | Call deadline in seconds, counted from when the call opens. Values below `1` are treated as 1 second. The deadline is not extended by later messages on the same call. | | `IDLE_TIMEOUT` | No | `5` | Seconds of silence before the connector half-closes a client stream or bidirectional call. `0` disables that idle close. | | `SESSION_SCOPE` | No | `TOPIC` | `TOPIC`: one streaming call per source topic. `EVENT`: one bidirectional call shared by every match of that event. | | `METADATA` | No | — | Extra initial metadata. A JSON object, or `key=value,key2=value2`. Keys must not start with `grpc-`. | | `CLIENT_AUTH` | No | `NONE` | Outbound authentication. See [Authentication](#authentication). | The parser also accepts `SERVERSTREAM`, `CLIENTSTREAM`, `BIDIRECTIONAL`, and `DUPLEX` for `RPC_TYPE`, and `SOURCE`, `SOURCE_TOPIC`, `SHARED`, and `ROUTE` for `SESSION_SCOPE`. Use the spellings in the table. ### Event parameters | Parameter | Required | Description | | - | - | - | | `SOURCE_TOPIC` | Yes | MQTT topic (wildcards allowed) whose payload is the request bytes. | | `DESTINATION_TOPIC` | Yes | MQTT topic for response bytes. `{topic.N}` expands from the source topic that triggered the call (1-based). | Several `ADD EVENT` blocks share the same channel and the same method. Each event keeps its own calls. Response publishes use QoS 0 and are not retained. *** ## RPC Types | `RPC_TYPE` | MQTT to the service | Service to MQTT | How the client side ends | | - | - | - | - | | `UNARY` | One message, one call | One response | The call completes | | `SERVER_STREAM` | One message, one call | Each response is published until the server finishes | The server finishes the call | | `CLIENT_STREAM` | Many messages on the same call | One response after the client half-closes | Empty payload, or `IDLE_TIMEOUT` | | `BIDI` | Many messages on the same call, while replies arrive | Each response is published as it arrives | Empty payload, or `IDLE_TIMEOUT` | An empty MQTT payload is a real empty request for `UNARY` and `SERVER_STREAM`. For `CLIENT_STREAM` and `BIDI` it is the half-close (no more client messages). The connector then waits up to 30 seconds for the remaining server bytes so the last response can still be published. `PAYLOAD AS BYTES` in an Action does not publish an empty payload. Send the zero-length message from an MQTT client (MQTT Explorer or any MQTT client) on that source topic. Raise `DEADLINE` above the longest call you expect. The default of 30 seconds ends a bidirectional session that runs longer than that, even when messages are still flowing. Raise `IDLE_TIMEOUT` when silence between messages is normal, or set it to `0` so only an empty payload half-closes the call. ### Client stream Publish request bytes to `grpc/upload/in`. Publish an empty payload, or wait for `IDLE_TIMEOUT`, to half-close. The single response is published to `grpc/upload/out`. ```lot wrap theme={"theme":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} DEFINE CONNECTOR UploadGrpc WITH TYPE GRPC ADD GRPC_CONFIG WITH ADDRESS GET ENV "GRPC_HOST" WITH PORT '50051' WITH USE_TLS "false" WITH SERVICE "echo.Echo" WITH METHOD "ClientStream" WITH RPC_TYPE "CLIENT_STREAM" WITH IDLE_TIMEOUT '10' ADD EVENT Upload WITH SOURCE_TOPIC "grpc/upload/in" WITH DESTINATION_TOPIC "grpc/upload/out" ``` ### One shared call (`SESSION_SCOPE EVENT`) `SESSION_SCOPE "EVENT"` keeps a single bidirectional call for every topic that matches the event. A new IMEI does not open its own call. Replies are published with `{topic.N}` taken from the **latest** source topic, not from the topic that opened the call. ```lot wrap focus={11} theme={"theme":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} DEFINE CONNECTOR SharedBidi WITH TYPE GRPC ADD GRPC_CONFIG WITH ADDRESS GET ENV "GRPC_HOST" WITH PORT '443' WITH USE_TLS "true" WITH SERVICE "echo.Echo" WITH METHOD "Bidi" WITH RPC_TYPE "BIDI" WITH DEADLINE '300' WITH IDLE_TIMEOUT '30' WITH SESSION_SCOPE "EVENT" ADD EVENT Converse WITH SOURCE_TOPIC "devices/+/uplink" WITH DESTINATION_TOPIC "devices/{topic.2}/downlink" ``` Leave the default `TOPIC` when each device must stay isolated. Use `EVENT` only when the service expects one shared stream. > [!WARNING] > > After a bidirectional call has already ended, further payloads on that session are dropped until you publish an empty payload. The next non-empty payload then opens a fresh call. A service that ends the turn inside the protobuf, without an empty MQTT message, leaves that topic dropped until the empty payload arrives. The connector logs a warning with the topic and the number of bytes it dropped. That end-of-stream condition (`InvalidArgument` or `Aborted`) does not by itself mark the connector unhealthy. *** ## Authentication `CLIENT_AUTH` attaches credentials as initial metadata on each call. Access tokens are not published on MQTT topics. If the service returns unauthenticated, the connector invalidates the cached token, drops the streaming call, and retries that payload once so the new token is on the new call. | `CLIENT_AUTH` | What you set | How it is sent | | - | - | - | | `NONE` | — | No credential metadata. Default. | | `API_KEY` | `API_KEY`. Optional `AUTH_HEADER` (default `X-API-Key`). | That metadata key | | `BEARER` | `BEARER_TOKEN` | `authorization: Bearer …` | | `OAUTH2_CLIENT_CREDENTIALS` | `CLIENT_ID`, `CLIENT_SECRET`, and `TOKEN_URL` or `ISSUER`. Optional `SCOPE`, `TOKEN_AUTH`. | `authorization: Bearer …` with a cached access token | | `OAUTH2_REFRESH_TOKEN` | `REFRESH_TOKEN`, plus the same token endpoint fields | `authorization: Bearer …` | | `OAUTH2_JWT_BEARER` | `SERVICE_ACCOUNT` (service-account JSON with `client_email` and `private_key`). Optional `TOKEN_URL` and `SCOPE`. | `authorization: Bearer …` | | `GOOGLE_METADATA` | Optional `METADATA_URL` and `SCOPE` | `authorization: Bearer …` from the instance metadata server | `TOKEN_AUTH` chooses how the client authenticates to the token endpoint: `CLIENT_SECRET_BASIC` (default), `CLIENT_SECRET_POST`, or `PRIVATE_KEY_JWT` (`CLIENT_ID` plus `CLIENT_ASSERTION_KEY` as PEM or JWK). `ISSUER` is the OIDC issuer; the connector discovers the token endpoint when `TOKEN_URL` is omitted. `BEARER_TOKEN` and `API_KEY` also accept the setting name `AUTH_SECRET`. For `OAUTH2_JWT_BEARER`, set `TOKEN_URL` when the service-account JSON has no `token_uri`. For `GOOGLE_METADATA`, the default metadata address is `http://metadata.google.internal/computeMetadata/v1/instance/service-accounts/default/token`. Set `METADATA_URL` to point elsewhere. When `SCOPE` is set and that URL has no query string, the connector adds the scope as a `scopes` query parameter. **API key** Send a static key in initial metadata: ```lot wrap focus={9-11} theme={"theme":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} DEFINE CONNECTOR ApiKeyGrpc WITH TYPE GRPC ADD GRPC_CONFIG WITH ADDRESS GET ENV "GRPC_HOST" WITH PORT '443' WITH USE_TLS "true" WITH SERVICE "echo.Echo" WITH METHOD "Unary" WITH RPC_TYPE "UNARY" WITH CLIENT_AUTH "API_KEY" WITH API_KEY GET SECRET "GRPC_API_KEY" WITH AUTH_HEADER "X-API-Key" ADD EVENT CallEcho WITH SOURCE_TOPIC "grpc/echo/in" WITH DESTINATION_TOPIC "grpc/echo/out" ``` **OAuth client** Fetch an access token with client credentials. Store the secret with [`-setSecret`](/latest/mqtt-broker/secrets-and-env), not in the connector text: ```lot wrap focus={10-14} theme={"theme":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} DEFINE CONNECTOR OAuthGrpc WITH TYPE GRPC ADD GRPC_CONFIG WITH ADDRESS GET ENV "GRPC_HOST" WITH PORT '443' WITH USE_TLS "true" WITH SERVICE "acme.Device" WITH METHOD "Bidi" WITH RPC_TYPE "BIDI" WITH DEADLINE '300' WITH CLIENT_AUTH "OAUTH2_CLIENT_CREDENTIALS" WITH TOKEN_URL GET ENV "GRPC_TOKEN_URL" WITH CLIENT_ID GET ENV "GRPC_CLIENT_ID" WITH CLIENT_SECRET GET SECRET "GRPC_CLIENT_SECRET" WITH SCOPE "devices.stream" ADD EVENT Stream WITH SOURCE_TOPIC "devices/+/uplink" WITH DESTINATION_TOPIC "devices/{topic.2}/downlink" ``` **Service account** Sign in with a service-account JSON key: ```lot wrap focus={10-13} theme={"theme":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} DEFINE CONNECTOR JwtGrpc WITH TYPE GRPC ADD GRPC_CONFIG WITH ADDRESS GET ENV "GRPC_HOST" WITH PORT '443' WITH USE_TLS "true" WITH SERVICE "acme.Device" WITH METHOD "Bidi" WITH RPC_TYPE "BIDI" WITH DEADLINE '300' WITH CLIENT_AUTH "OAUTH2_JWT_BEARER" WITH TOKEN_URL GET ENV "GRPC_TOKEN_URL" WITH SERVICE_ACCOUNT GET SECRET "GRPC_SERVICE_ACCOUNT" WITH SCOPE "devices.stream" ADD EVENT Stream WITH SOURCE_TOPIC "devices/+/uplink" WITH DESTINATION_TOPIC "devices/{topic.2}/downlink" ``` Extra metadata is merged with the auth entry. Prefer a JSON object when a value contains a comma: ```lot wrap focus={9-11} theme={"theme":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} DEFINE CONNECTOR MetaGrpc WITH TYPE GRPC ADD GRPC_CONFIG WITH ADDRESS GET ENV "GRPC_HOST" WITH PORT '443' WITH USE_TLS "true" WITH SERVICE "echo.Echo" WITH METHOD "Unary" WITH RPC_TYPE "UNARY" WITH CLIENT_AUTH "BEARER" WITH BEARER_TOKEN GET SECRET "GRPC_BEARER" WITH METADATA "{\"x-tenant\":\"plant-a\"}" ADD EVENT CallEcho WITH SOURCE_TOPIC "grpc/echo/in" WITH DESTINATION_TOPIC "grpc/echo/out" ``` The connector fetches the access token when it starts. A token or channel failure leaves the connector disconnected. Startup waits up to 10 seconds for the channel. If the route has not started, the broker retries in the background, from 2 seconds up to 60 seconds, until the route starts or you remove it. Removing the connector drops the cached token. There is no setting for a client certificate or a private CA file. TLS uses the trust store on the broker host. *** ## Payloads The connector sends and publishes the MQTT payload as raw bytes. It does not encode JSON and it does not compile protobuf. A binary publish stays binary. If the topic cache has no raw bytes, the connector sends the payload text as UTF-8, which corrupts protobuf and audio. Publish protobuf with `PUBLISH MODEL`, or forward an existing blob with `PAYLOAD AS BYTES`. Decode replies with `GET PROTO` on the destination topic. The model describes the request. The Action writes binary protobuf to the topic the connector is listening on: ```lot wrap theme={"theme":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} DEFINE MODEL EchoRequest WITH FORMAT PROTOBUF COLLAPSED ADD STRING "text" PROTO_TAG 1 ``` ```lot wrap theme={"theme":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} DEFINE ACTION SendEcho ON TOPIC "commands/echo" DO PUBLISH MODEL EchoRequest TO "grpc/echo/in" WITH text = (GET JSON "text" IN PAYLOAD AS STRING) ``` `EchoGrpc` from the Quick Start then forwards those bytes and publishes the service's reply on `grpc/echo/out`. Read fields from that reply with `GET PROTO` and a response model. See [Protobuf models](/latest/lot-language/models/protobuf). *** ## Check the Round Trip **1. Deploy the connector** Deploy the LoT, then confirm the connector reaches the service. Health is retained on `$SYS/Coreflux/Routes/EchoGrpc/status`. See [Connector status](../overview#connector-status-monitoring). **2. Subscribe to the reply** In MQTT Explorer, subscribe to the destination topic (`grpc/echo/out`, or `devices/+/downlink` for the per-device example). **3. Publish a request** Publish a payload to the source topic. For a unary echo, the reply is one message. For a bidirectional device topic, further publishes on that same topic reuse the call until an empty payload or the idle timeout. **4. Half-close a stream** For `CLIENT_STREAM` or `BIDI`, publish a zero-length payload on that source topic. The last server bytes are published, and the next non-empty payload opens a new call. *** ## Best Practices * Keep `SESSION_SCOPE "TOPIC"` (the default) so each device topic, such as one IMEI, has its own call. * Set `DEADLINE` to the full length of a streaming call, not the gap between messages. * Store keys with `GET SECRET`. Do not publish tokens on MQTT topics. * Put correlation ids in the payload. The connector will not add a session id for you. * Tell operators that an empty payload half-closes `CLIENT_STREAM` and `BIDI`. * Do not send metadata keys that start with `grpc-`. *** ## Limitations > [!WARNING] > > * **Raw bytes only.** The connector does not load a `.proto` or a protobuf codec. Encode before publish and decode after the reply. > * **One method per connector.** `SERVICE`, `METHOD`, and `RPC_TYPE` are connector-wide. > * **Deadline is fixed when the call opens.** A 30-second deadline stops a longer bidirectional call. > * **Finished bidirectional calls drop new bytes** until an empty payload half-closes that session. > * **`SESSION_SCOPE "EVENT"` shares one call.** Replies follow the latest source topic. > * **No client certificate or private CA field.** TLS uses the broker host trust store. > * **Replies are QoS 0 and not retained.** > * **Empty binary publishes from LoT do not half-close.** `PAYLOAD AS BYTES` does not publish an empty payload. Publish the zero-length message from an MQTT client. ### Troubleshooting > **The connector will not start** > > * `GRPC_CONFIG` needs `ADDRESS`, `SERVICE`, and `METHOD`. Each event needs `SOURCE_TOPIC` and `DESTINATION_TOPIC`. > * The channel must connect within 10 seconds. Use a host and port the broker host can reach, and match `USE_TLS` to the service (plaintext often uses port `50051`; TLS often uses `443`). > * If the route fails to start, the broker retries in the background, from 2 seconds up to 60 seconds, until the route starts or you remove it. > * OAuth modes fetch a token at startup. Check `TOKEN_URL` or `ISSUER`, and the secret names. > **Unauthenticated replies** > > The connector retries once after refreshing the token and opening a new streaming call. If that retry fails, check `CLIENT_AUTH`, the token endpoint, and that the secret resolves. Tokens are not written to MQTT topics. > **Binary payloads look corrupted** > > Publish raw bytes (`PUBLISH MODEL` or `PAYLOAD AS BYTES`). A text payload is sent as UTF-8. > **Metadata is rejected** > > Remove any key that starts with `grpc-`. Use a JSON object for `METADATA` when a value contains a comma. > **The stream never finishes** > > Nothing published an empty payload, and `IDLE_TIMEOUT` is high or `0`. Publish a zero-length payload on the source topic, or lower `IDLE_TIMEOUT`. > **MQTT publishes continue but replies stop** > > The previous bidirectional call already ended. Payloads on that topic are dropped until an empty payload. Publish the empty payload, then send the next request. Check connector logs for the dropped-byte warning. > **Replies land on the wrong device topic** > > `SESSION_SCOPE "EVENT"` expands `{topic.N}` from the latest source topic. Use `TOPIC` when each IMEI needs its own call and its own reply topic. *** ## Next Steps > **[Protobuf Models](/latest/lot-language/models/protobuf)** > > Encode request bytes with `PUBLISH MODEL` and read replies with `GET PROTO`. > **[Kafka Connector](./kafka)** > > Stream MQTT to an event log when the other side is Kafka, not a gRPC method. # Kafka Connector 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` connector connects your Coreflux broker to an Apache Kafka cluster and moves messages in both directions. A single connector 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. > [!TIP] > > **Like a conveyor belt between two warehouses.** Your MQTT broker handles fast-moving device traffic; Kafka is the long-term, replayable record. The Kafka connector 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. A gRPC service uses the [gRPC connector](./grpc) (`TYPE GRPC`). ### Key capabilities | Capability | What it means for you | | - | - | | **Bidirectional** | Produce and consume in one connector — no separate definitions needed | | **Secure** | Supports `PLAINTEXT`, `SSL`, `SASL_PLAINTEXT`, and `SASL_SSL` | | **Reliable delivery** | On consume, the offset is committed only after the broker has dispatched the message (at-least-once) | | **Message keys** | Connector 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 connector is defined, starting with the three common directions. ## Quick Start **Produce (MQTT → Kafka)** Send every sensor reading from the broker into a Kafka topic, keyed by device: ```lot wrap theme={"theme":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} DEFINE CONNECTOR 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`. **Consume (Kafka → MQTT)** Forward Kafka commands into the MQTT broker: ```lot wrap theme={"theme":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} DEFINE CONNECTOR 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`. **Bidirectional** One connector handling both directions — telemetry out, commands in: ```lot wrap theme={"theme":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} DEFINE CONNECTOR 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" ``` > [!WARNING] > > 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 connector 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. An `"in"` event commits it only after the broker has dispatched the message | *** ## Supported Kafka Services The Kafka connector works with any Kafka-compatible cluster, cloud-managed or self-hosted. **Cloud-managed** | 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 | **Self-managed** | 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 connector starts with a `KAFKA_CONFIG` block describing the cluster, security, and throughput settings. Only `BROKER_ADDRESS` is required, but production connectors should always set a `GROUP_ID` and use a secure protocol. ```lot wrap focus={3-4} theme={"theme":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} DEFINE CONNECTOR 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 > **Cluster Connection** > > **BROKER\_ADDRESS** `string` *required* > > Bootstrap servers, e.g. `localhost:9092` or `host1:9092,host2:9092`. List two or three brokers in production so the connector survives an individual broker restart. > > **GROUP\_ID** `string` > > 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. > > **AUTO\_OFFSET\_RESET** `string` (default: Earliest) > > Where to start reading when no committed offset exists: `Earliest` (from the beginning) or `Latest` (new messages only). > > **ENABLE\_AUTO\_COMMIT** `boolean` (default: false) > > Leave `false` so an `"in"` event commits the offset only after the broker has dispatched the message (at-least-once). Set `true` to also enable periodic background commits. > **Security** > > **SECURITY\_PROTOCOL** `string` (default: PLAINTEXT) > > `PLAINTEXT`, `SSL`, `SASL_PLAINTEXT`, or `SASL_SSL`. > > **SASL\_MECHANISM** `string` > > Required with SASL: `PLAIN`, `SCRAM-SHA-256`, or `SCRAM-SHA-512`. > > **SASL\_USERNAME** `string` > > SASL username. Supports `GET ENV`. > > **SASL\_PASSWORD** `string` > > SASL password. Use `GET SECRET` to keep it out of your LoT definition. > > **SSL\_CA\_LOCATION** `string` > > Path to the CA certificate (for `SSL` or `SASL_SSL`). > > **SSL\_CERTIFICATE\_LOCATION** `string` > > Path to the client certificate (mutual TLS). > > **SSL\_KEY\_LOCATION** `string` > > Path to the client key (required with a client certificate). > > **SSL\_KEYSTORE\_PASSWORD** `string` > > Key or keystore password. > **Throughput & Tuning** > > **ACKS** `string` (default: all) > > Producer durability: `0` (no acknowledgement), `1` (leader only), or `all` (all in-sync replicas). > > **LINGER\_MS** `integer` (default: 5) > > 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. > > **BATCH\_SIZE** `integer` > > Maximum producer batch size in bytes. Default is 1 MB. > > **COMPRESSION\_TYPE** `string` (default: none) > > Producer compression: `none`, `gzip`, `snappy`, `lz4`, or `zstd`. Most effective on larger batches — pair with `LINGER_MS`. > > **FETCH\_MAX\_BYTES** `integer` > > Maximum bytes returned per consumer fetch. Default is 50 MB. > > **QUEUED\_MAX\_MESSAGES\_KB** `integer` (default: 65536) > > Maximum KB pre-fetched per partition into the consumer buffer (64 MB by default). Lower it to cap memory use on high-volume topics. > > **FLUSH\_TIMEOUT\_MS** `integer` (default: 5000) > > How long the connector 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 | **PLAINTEXT (development)** No encryption or authentication — local clusters only: ```lot wrap theme={"theme":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} DEFINE CONNECTOR 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" ``` **SSL (TLS only)** Encrypted transport without SASL credentials: ```lot wrap focus={5-6} theme={"theme":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} DEFINE CONNECTOR 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" ``` **SASL\_SSL (production)** Encrypted transport with SCRAM authentication — the recommended production setup: ```lot wrap focus={5-10} theme={"theme":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} DEFINE CONNECTOR 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" ``` > [!NOTE] > > 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 > **Required** > > **SOURCE\_TOPIC** `string` *required* > > Broker MQTT topic when `DIRECTION` is `"out"`; Kafka topic to subscribe to when `DIRECTION` is `"in"`. > > **DESTINATION\_TOPIC** `string` *required* > > Kafka topic when `DIRECTION` is `"out"`; broker MQTT publish topic when `DIRECTION` is `"in"`. > > **DIRECTION** `string` *required* > > `"out"` (MQTT → Kafka) or `"in"` (Kafka → MQTT). > **Optional** > > **QUERY** `string` > > The Kafka record **key** for partitioning (produce events only). See [Message Keys and Partitioning](#message-keys-and-partitioning). > [!NOTE] > > `WITH EVERY` is not supported on a Kafka event. Adding a route that includes it is rejected. A route saved earlier still loads: `EVERY` is ignored, a warning is written to the log, and the event runs from its `SOURCE_TOPIC`. To produce on a schedule, use a LoT action with [`ON EVERY`](/latest/lot-language/actions/events#time-based-triggers) that publishes to the event's `SOURCE_TOPIC`. *** ## 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":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} DEFINE CONNECTOR 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":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} DEFINE CONNECTOR 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 **Production (SASL\_SSL)** Full production connector with TLS, SCRAM authentication, telemetry out, and alarms in: ```lot wrap theme={"theme":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} DEFINE CONNECTOR 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" ``` **High-throughput telemetry** Optimized for sustained high-rate telemetry with batching and compression: ```lot wrap theme={"theme":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} DEFINE CONNECTOR 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. **Multiple topic mappings** Connector different asset types to different Kafka topics, keyed by a JSON field: ```lot wrap theme={"theme":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} DEFINE CONNECTOR 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" ``` **Confluent Cloud** Connect to Confluent Cloud using SASL/PLAIN with an API key and secret: ```lot wrap theme={"theme":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} DEFINE CONNECTOR 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. **High availability** List multiple bootstrap brokers so the connector survives a single broker restart: ```lot wrap theme={"theme":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} DEFINE CONNECTOR 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 commits its offset only after the broker has dispatched the message. That is at-least-once delivery, and it adds a short round-trip per message. For very high-rate consume topics, use one Kafka connector per logical stream rather than stacking many consume events on a single connector. ### Memory use (consume) The connector 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 connector 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 > **Always set a stable GROUP\_ID** > > 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":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} > WITH GROUP_ID "coreflux-plant-v1" > ``` > **Use GET SECRET for credentials** > > Never hardcode passwords in a connector definition. > > ```lot wrap theme={"theme":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} > WITH SASL_PASSWORD GET SECRET "KAFKA_PASSWORD" > ``` > **Use SASL\_SSL in production** > > Reserve `PLAINTEXT` for local development; encrypt and authenticate everything else. > > ```lot wrap theme={"theme":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} > WITH SECURITY_PROTOCOL "SASL_SSL" > WITH SASL_MECHANISM "SCRAM-SHA-512" > ``` > **Set AUTO\_OFFSET\_RESET deliberately** > > Choose where a brand-new consumer starts reading. > > ```lot wrap theme={"theme":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} > WITH AUTO_OFFSET_RESET "Earliest" // replay history > WITH AUTO_OFFSET_RESET "Latest" // new messages only > ``` > **Key messages for per-device ordering** > > Use a message key so all records from one device land on the same partition, in order. > > ```lot wrap theme={"theme":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} > WITH QUERY "{topic.2}" > ``` > **List multiple bootstrap brokers** > > A single broker address is a single point of failure; list several for resilience. > > ```lot wrap theme={"theme":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} > WITH BROKER_ADDRESS "kafka-1:9092,kafka-2:9092,kafka-3:9092" > ``` > **Monitor consumer group lag** > > 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 > **Quick connection check** > > Verify a connector's connection at any time by publishing the following to `$SYS/Coreflux/Command` from any MQTT client: > > ``` > -checkConnectorConnection PlantKafka > ``` > > The connector validates the cluster connection and, when consume events are present, the consumer group join — catching configuration problems early. > **The route does not start** > > A Kafka route that fails to start is retried in the background. The wait begins at 2 seconds and increases up to 60 seconds, until the route starts or you remove it. > **Cannot connect to the cluster** > > * 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. > **Connects, but no messages flow** > > 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` | > **Authentication or SASL errors** > > * 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. > **Consumer receives nothing / replays every restart** > > * 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 connector its own `GROUP_ID` unless you intentionally want them to share partitions. > * An `"in"` event commits the offset only after the broker has dispatched the message. A crash before that dispatch leaves the record to be read again. > **Performance problems** > > | 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 connectors | 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 > **[System Connectors](../system-routes)** > > Configure MQTT bridges and Sparkplug roles. > **[Data Storage Connectors](../data-storage/overview)** > > Persist MQTT data in databases for analytics. # System Connectors Source: https://docs.coreflux.org/latest/lot-language/routes/system-routes Configure MQTT bridges and Sparkplug roles with Coreflux System Connectors ## Build the broker topology around your data System Connectors 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. > [!TIP] > > **Think of System Connectors as the roads around the broker.** Bridges connect MQTT brokers, and Sparkplug connectors define how industrial MQTT traffic enters and leaves the system. > [!NOTE] > > For **high-availability Coreflux clustering** (multi-node broker failover), see [High-Availability Clustering](/latest/mqtt-broker/clustering) (beta). That feature is broker configuration, not a System Connector. ### When to use System Connectors 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 Connector > **[MQTT Bridge](./system/mqtt-bridge)** > > Synchronize selected MQTT topics with another broker. > **[Sparkplug B](./system/sparkplug-b)** > > Decode Sparkplug traffic or publish Coreflux metrics as an edge node. > **[Sparkplug Host](./system/sparkplug-host)** > > Make Coreflux the Primary Host for a Sparkplug namespace. *** Choose the connector by the role Coreflux needs to play. ## Compare System Connectors | Connector | 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 | > [!WARNING] > > A `SPARKPLUG_HOST` connector and a `SPARKPLUG_B` connector in `DECODE` or `BOTH` mode cannot run together on the same broker. Both consume the complete `spBv1.0/#` namespace. You can combine one Host connector with separate `ENCODE`-only connectors. *** ## Next Steps > **[Choose a Sparkplug role](./system/sparkplug-b)** > > Compare decode, edge-node, and Primary Host responsibilities. > **[High-Availability Clustering](/latest/mqtt-broker/clustering)** > > Run multiple Coreflux brokers as a fault-tolerant cluster (beta). # MQTT Bridge Connector 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` connector 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. > [!TIP] > > **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 **Edge to cloud (out)** Forward local sensor data up to a cloud broker over TLS: ```lot wrap theme={"theme":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} DEFINE CONNECTOR 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" ``` **Cloud to edge (in)** Pull commands from a cloud broker down to the local broker: ```lot wrap theme={"theme":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} DEFINE CONNECTOR 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" ``` **Bidirectional (both)** Keep two brokers fully in sync in both directions: ```lot wrap theme={"theme":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} DEFINE CONNECTOR 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. > **Broker Settings** > > **BROKER\_ADDRESS** `string` *required* > > IP address or hostname of the broker. Use `SELF` for the local broker. > > **BROKER\_PORT** `integer` *required* > > MQTT port (typically 1883 for unencrypted, 8883 for TLS). > > **CLIENT\_ID** `string` > > Client identifier for the bridge connection. Optional. When unset, the broker generates one that is unique per host, connector and endpoint (`cf---src` or `cf---dst`). Set it when the remote broker expects a specific client ID, and keep it distinct for each connector. > **Authentication** > > **USERNAME** `string` > > Authentication username. > > **PASSWORD** `string` > > Authentication password. > **TLS/Security** > > **USE\_TLS** `boolean` > > Enable TLS encryption. Default: false. > > **ALLOW\_UNTRUSTED\_CERTS** `boolean` > > Allow untrusted certificates. Default: false. > > **SERVER\_CA\_CERT\_PATH** `string` > > Path to CA certificate file. > > **CLIENT\_CERT\_PATH** `string` > > Path to client certificate for mTLS. > > **CLIENT\_CERT\_PASS** `string` > > Client certificate password. > **Connection Handling** > > **RECONNECTION\_RETRIES** `integer` > > Number of reconnection attempts. Default: 5. *** ## Mapping Configuration Each `ADD MAPPING` block defines which topics are bridged and in which direction. > **Topic Mapping** > > **SOURCE\_TOPIC** `string` *required* > > Topic pattern to subscribe to on the source broker. Supports wildcards. > > **DESTINATION\_TOPIC** `string` *required* > > Topic to publish to on the destination broker. > > **DIRECTION** `string` *required* > > 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":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} DEFINE CONNECTOR 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 > **Use TLS for External Connections** > > Always enable TLS when connecting to external brokers: > > ```lot wrap theme={"theme":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} > WITH USE_TLS "true" > ``` > **Unique Client IDs** > > If you set CLIENT\_ID, keep it unique per bridge connection to avoid conflicts. Two connections with the same client ID on one remote broker disconnect each other. Leave it unset to use the generated default. > > ```lot wrap theme={"theme":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} > WITH CLIENT_ID "EdgeDevice001-SensorBridge" > ``` > **Test with Local Broker First** > > Use `WITH BROKER SELF` during development: > > ```lot wrap theme={"theme":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} > ADD SOURCE_CONFIG > WITH BROKER SELF > ``` > **Secure Credentials** > > Store passwords securely and use app-specific passwords where available. > **Handle Reconnection** > > Configure appropriate retry settings: > > ```lot wrap theme={"theme":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} > WITH RECONNECTION_RETRIES '10' > ``` *** ## Troubleshooting > **Bridge Connection Timeout** > > * 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` > **TLS/Certificate Errors** > > * 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) > **Messages Not Bridging** > > * Verify SOURCE\_TOPIC and DESTINATION\_TOPIC are correct > * Check DIRECTION setting matches your expected data flow > * If you set CLIENT\_ID, ensure it is unique across all connections > * Check broker logs for subscription errors *** ## Next Steps > **[System Connectors](../system-routes)** > > Compare bridging and Sparkplug roles. > **[Sparkplug B](./sparkplug-b)** > > Publish and consume structured industrial MQTT data. # Sparkplug B Connector 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` connector 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 connector. > [!TIP] > > **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 connector when you need to: * consume `spBv1.0/#` traffic in LoT Actions, dashboards, or storage Connectors; * 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 Connector](./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 connector. *** Start by choosing the direction in which Coreflux participates. ## Quick Start **Decode to JSON** Decode every Sparkplug B publisher on the local broker into readable MQTT topics: ```lot wrap focus={3} theme={"theme":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} DEFINE CONNECTOR 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 as an edge node** Publish a local temperature topic as a Sparkplug edge-node metric: ```lot wrap focus={3-5,6-8} theme={"theme":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} DEFINE CONNECTOR 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 connector connects and sends data when the source topic changes. **Decode and publish** Decode the namespace while publishing Coreflux metrics as a gateway edge node: ```lot wrap focus={3-5} theme={"theme":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} DEFINE CONNECTOR 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 connector. *** ## 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. > [!WARNING] > > Only one namespace consumer can run per broker. Deploy either one `SPARKPLUG_B` connector in `DECODE` or `BOTH` mode, or one `SPARKPLUG_HOST` connector. Multiple `ENCODE`-only connectors are allowed. ## Configure the Sparkplug connection The `SPARKPLUG_CONFIG` block controls the connector 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 | > [!NOTE] > > `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 connector 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 connector publishes a node metric and a writable device metric to a remote broker: ```lot wrap focus={10-15} theme={"theme":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} DEFINE CONNECTOR 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":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} DEFINE CONNECTOR 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 connector 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. > [!WARNING] > > 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":{"light":"css-variables","dark":"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":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} { "name": "Inputs/Temperature", "value": 24.6, "type": "Double", "timestamp": 1717160400123, "quality": "GOOD" } ``` If a DATA metric omits its datatype, Coreflux uses the one from NBIRTH or DBIRTH. The connector 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 connector **1. Check connector health** Publish the connection check command to `$SYS/Coreflux/Command`: ```text wrap theme={"theme":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} -checkConnectorConnection PlantDecoder ``` Confirm `$SYS/Coreflux/Routes/PlantDecoder/status` reports the connector as connected or active. **2. Observe Sparkplug traffic** Use any MQTT client to confirm messages arrive under `spBv1.0/#`. **3. Subscribe to decoded output** Subscribe to `sparkplug/decoded/#`. After a BIRTH message, named metric topics and retained `$status` topics appear. ## Troubleshooting > **No decoded topics appear** > > Confirm the connector is active and that a Sparkplug B publisher is sending valid protobuf payloads under `spBv1.0/#`. Decoded output appears under `sparkplug/decoded/#`. > **Metrics appear with unresolved aliases** > > Alias-only DATA arrived before its matching BIRTH. Wait for or request a rebirth and monitor `$error` for `alias_map_unavailable`. > **The edge node remains silent** > > 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`. > **Commands do not reach local topics** > > 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 connector status for repeated alias or decode failures. *** ## Next Steps > **[Configure Sparkplug Host](./sparkplug-host)** > > Make Coreflux the Primary Host and issue commands to edge nodes. > **[Store decoded data](../data-storage/overview)** > > Persist readable Sparkplug metrics in a database. # Sparkplug Host Connector 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` connector 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. > [!TIP] > > **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 connector 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 Connector](./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 **Local broker** Make Coreflux the Primary Host for edge nodes connected to its local broker: ```lot wrap focus={3-4} theme={"theme":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} DEFINE CONNECTOR 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. **Remote plant broker** Connect the Host connector to the broker already used by plant edge nodes: ```lot wrap focus={3-9} theme={"theme":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} DEFINE CONNECTOR 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/#`. **Declared command** Add an explicit command target that works even before Coreflux sees the edge node's BIRTH: ```lot wrap focus={6-9} theme={"theme":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} DEFINE CONNECTOR 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 connector 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. > [!WARNING] > > A `SPARKPLUG_HOST` connector consumes the complete Sparkplug namespace. Do not run it alongside a `SPARKPLUG_B` connector in `DECODE` or `BOTH` mode on the same broker. You can run separate `SPARKPLUG_B` connectors 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 connector 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":{"light":"css-variables","dark":"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 connector supports two command methods. ### Use the convention topic tree Publish a value on the local broker without declaring command metrics: ```text wrap theme={"theme":{"light":"css-variables","dark":"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":{"light":"css-variables","dark":"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 connector feeds the same readable topic tree as a `SPARKPLUG_B` decode connector: ```text wrap theme={"theme":{"light":"css-variables","dark":"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 connector **1. Check connector health** Publish the following command to `$SYS/Coreflux/Command`: ```text wrap theme={"theme":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} -checkConnectorConnection PlantHost ``` Confirm `$SYS/Coreflux/Routes/PlantHost/status` reports the target connection as active. **2. Confirm Host state** Use any MQTT client connected to the target broker and subscribe to `spBv1.0/STATE/CorefluxHost`. Confirm the retained payload reports `online:true`. **3. Confirm decoded output** Subscribe to `sparkplug/decoded/#` on the local Coreflux broker, then connect or rebirth an edge node. **4. Test a command** Publish a correctly typed value to a convention or declared command topic and confirm the edge node receives `NCMD` or `DCMD`. ## Troubleshooting > **Host STATE is not published** > > Confirm `SERVER`, `PORT`, credentials, and TLS settings point to the broker used by the edge nodes. Check connector status for a `HOST_ID_CONFLICT` refusal. > **No decoded data appears** > > Confirm edge nodes publish valid Sparkplug B traffic to the target broker and that no `SPARKPLUG_B` decode consumer conflicts with this Host connector. > **Commands are dropped** > > 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`. > **Repeated rebirth requests occur** > > Persistent alias or sequence gaps usually indicate an unstable edge node. Monitor decoded `$error` topics and the connector status. ## Limitations > [!WARNING] > > * The connector connects to one target `SERVER` and `PORT`; multi-server Primary Host failover is not supported. > * The connector 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 connector'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 > **[Configure Sparkplug B](./sparkplug-b)** > > Decode traffic or publish Coreflux metrics as an edge node. > **[Create LoT Actions](../../actions/overview)** > > Automate decoded metrics and Sparkplug command topics. # High-Availability Clustering Source: https://docs.coreflux.org/latest/mqtt-broker/clustering Run Coreflux MQTT Broker nodes as a fault-tolerant cluster with automatic failover, majority-stored QoS 1/2 messages and delivery from any node > [!WARNING] > > **High-availability clustering is in beta.** It has been tested by repeatedly killing, restarting, and isolating nodes under load, but not yet in production deployments, at production message rates, or across rolling upgrades. Its behavior and configuration options can change in a later release. Test it with your own workload before you rely on it in production, and include your broker version when you report an issue. ## Keep messaging available when a node fails A single broker is a single point of failure. High-availability clustering (beta) runs several Coreflux MQTT Broker nodes as one system. Clients connect to any node, and a message published on one node reaches subscribers on every node. If a node stops, the others keep serving clients and take over its work. A QoS 1 or 2 message is acknowledged to the publisher only once it is stored on a majority of the nodes responsible for it, so losing a minority of the cluster does not lose messages the broker has already accepted. > [!TIP] > > **Like a team that keeps shared minutes.** One member chairs the meeting, and every decision is written into the minutes of most members before it counts. If the chair leaves, the others pick a new one and carry on from the same minutes. ### When to use clustering Use clustering when you need: * MQTT service that continues when one broker stops, crashes, or is restarted for maintenance; * QoS 1/2 messages that survive the loss of a node after the broker has acknowledged them; or * one MQTT endpoint served by several broker nodes, on fixed machines or as Kubernetes pods. Run **3** or **5** nodes. A cluster keeps working as long as a majority of its nodes are up: 2 of 3, or 3 of 5. Clustering requires a license that grants the cluster feature; with clustering enabled and no such license, the broker logs a warning and runs standalone. > [!NOTE] > > Clustering is **broker configuration**, not a LoT Connector. To exchange topics with another, separate MQTT broker (including non-Coreflux brokers), use an [MQTT Bridge](/latest/lot-language/routes/system/mqtt-bridge) instead. *** Setting up a cluster takes the same steps on every node: a service user for traffic between nodes, a `Cluster` section in the broker configuration, and durability turned on. Then load your LoT on any node, and the cluster replicates it to the others. See [LoT in a cluster](#lot-in-a-cluster). ## Quick Start **1. Create the cluster service user on every node** Nodes exchange messages with each other over MQTT, using a dedicated MQTT user. Create the same user, with the same password, on every node, for example with the [`-addUser` broker command](/latest/mqtt-broker/commands#user-management): ```bash wrap theme={"theme":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} -addUser cluster a-strong-password ``` Only this user may send cluster traffic between nodes. **2. Configure the cluster on every node** Add a `Cluster` section to the broker configuration of each node, or set the matching `COREFLUX_CLUSTER_*` environment variables (environment variables override the file). Keep everything identical except `NodeName`, and turn [durability](/latest/mqtt-broker/durability) on as well (see [Durability and clustering](#durability-and-clustering)). **Seeds (typical production)** List the peers' addresses when automatic discovery is not available, which is the case in most cloud networks and across subnets: ```json wrap theme={"theme":{"light":"css-variables","dark":"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-long-random-secret", "DataPlaneUsername": "cluster" }, "Durability": { "Enabled": true } } ``` **Kubernetes** Discover the pods through a headless Service. Use the StatefulSet pod name as `NodeName`, so each node keeps its identity when its pod is rescheduled: ```json wrap theme={"theme":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} { "Cluster": { "Enabled": true, "ClusterId": "k8s-brokers", "NodeName": "coreflux-broker-0", "Discovery": "kubernetes", "KubernetesServiceDns": "coreflux-broker-headless.default.svc.cluster.local", "KubernetesPeerPort": 9200, "SharedSecret": "a-long-random-secret", "DataPlaneUsername": "cluster" }, "Durability": { "Enabled": true } } ``` **Multicast (same LAN)** Let nodes on one local network find each other automatically: ```json wrap theme={"theme":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} { "Cluster": { "Enabled": true, "ClusterId": "lab-cluster", "NodeName": "broker-1", "Discovery": "multicast", "SharedSecret": "a-long-random-secret", "DataPlaneUsername": "cluster" }, "Durability": { "Enabled": true } } ``` **Environment variables (Docker)** The same seeds setup for a container, without editing the configuration file: ```yaml wrap theme={"theme":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} environment: COREFLUX_CLUSTER_ENABLED: "true" COREFLUX_CLUSTER_ID: "factory-floor" COREFLUX_CLUSTER_NODE_NAME: "broker-a" COREFLUX_CLUSTER_DISCOVERY: "seeds" COREFLUX_CLUSTER_SEEDS: "broker-a:9200,broker-b:9200,broker-c:9200" COREFLUX_CLUSTER_SHARED_SECRET: "a-long-random-secret" COREFLUX_CLUSTER_DATAPLANE_USERNAME: "cluster" COREFLUX_DURABILITY_ENABLED: "true" ``` **3. Start the nodes** Start all nodes. On a first start, the nodes wait up to `BootstrapGraceSeconds` (30 s by default) to make sure no cluster is running yet, then form a new one and elect a leader. Later restarts rejoin the existing cluster at once. **4. Verify the cluster** Call `GET /cluster` on the [operations port](/latest/mqtt-broker/operational-endpoints#cluster-status) of any node (`9100` by default): ```bash wrap theme={"theme":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} curl http://broker-a:9100/cluster ``` > [!TIP] > > Every node names the same `leadership.leader`, `membersTotal` equals your number of nodes on every node, and on the leader `membersAlive` equals `membersTotal`. *** ## How a cluster works ### One leader, automatic membership The nodes elect one **leader**. The leader admits new nodes, removes dead ones, and decides which node is responsible for which part of the message traffic. Every node, leader or not, serves MQTT clients in the same way. * **Elections are automatic.** When the leader is lost, the remaining nodes elect a new one within a few election timeouts, typically under a second. That includes a leader that is silent: frozen, or cut off so its cluster port no longer answers. * **Nodes keep their identity.** A node is recognised by its `NodeName`, not by its IP address. A node that restarts on a new address, or on another node's old address, is recognised and its old address is dropped at once. * **Restarts rejoin.** Each node remembers its cluster membership on disk and rejoins its peers after a restart, including when every node restarts at the same time. * **Dead nodes are removed.** A node that stays unreachable for `MemberRemovalGraceSeconds` (5 minutes by default) is removed from the membership, so it no longer counts when the cluster checks for a majority. With `kubernetes` discovery, a pod that the headless Service no longer lists, and whose cluster port fails a TCP probe, is removed after `MemberAbsentFromDiscoverySeconds` (2 minutes, 120 seconds, by default). A new pod is found within seconds. A node is added back when it returns. * **LoT changes go through the leader.** Send them to any node: a follower passes them to the leader. Other commands, such as user management, run on the node that receives them. See [LoT in a cluster](#lot-in-a-cluster). * **No accidental second cluster.** A node that starts with no cluster state first asks its peers whether they already belong to a cluster, and joins that cluster instead of starting a new one. ### Partitions spread the work QoS 1/2 traffic is split into **partitions** (32 by default). A publisher's messages always belong to the same partition, chosen from its client id. Each partition has one **owner**, the node that accepts its writes, and copies on other nodes (3 copies by default, owner included). The leader spreads the owners evenly across the nodes. When a node fails, its partitions move to nodes that already hold their copies, so no accepted message is left behind. ### QoS 1 and 2 are acknowledged once they are safe When a client publishes at QoS 1 or 2, the broker stores the message on a majority of its partition's copies (2 of 3) before it sends the PUBACK (QoS 1) or PUBREC (QoS 2). A client connected to a node that does not own its partition is served the same way: that node passes the message to the owner and acknowledges it when the owner confirms. If a majority cannot be reached in time (`ReplicationTimeoutMs`, 5 s by default), the broker **rejects** the publish instead of acknowledging a message it cannot keep safe, and does not deliver it: | Reason code (MQTT 5) | Meaning | What the client should do | | - | - | - | | `0x97` Quota exceeded | Not enough nodes available for this partition right now (failover in progress, this side of a network split has no majority, or replication is still starting on the receiving node) | Retry after a short delay, on the same or another node | | `0x80` Unspecified error | The node could not write the message to its disk | Retry; check the node's disk | While replication is still starting, that `0x97` refusal uses the reason text "Cluster starting" and lasts at most 30 seconds. Retry; the node accepts the publish once replication is running. > [!WARNING] > > **Use MQTT 5 for QoS 1/2 publishers.** MQTT 3.1.1 has no way to report a rejected publish: a 3.1.1 client receives a normal acknowledgement for a message the cluster did not accept. MQTT 5 clients see the reason code and can retry. ### Subscribers on any node receive every message A message published on one node is delivered to matching subscribers on every node: * **QoS 1/2** messages are queued on disk for each other node and re-sent until that node confirms delivery. A node that is down receives its messages when it returns, including messages for its offline persistent sessions. * **QoS 0** messages are sent to the other nodes once, without storage. A node that is down misses them, as with any QoS 0 message. ### Failover When a node fails, its partitions move to surviving nodes that already hold their copies. The new owner first catches up from the other copies, then re-delivers the messages of the last `ReplicationReplaySeconds` (5 minutes by default) for those partitions, so subscribers do not miss messages the failed node had accepted but not yet passed on. Subscribers can therefore receive some messages twice after a failover. With `ReplicationDedupDelivered` on (the default), a node drops a replay copy of a quorum-durable message it has already delivered. Set it to `false` to deliver every copy. During a failover, publishes to the affected partitions are rejected with `0x97` until the new owner is ready. A new leader is elected within a few election timeouts, typically under a second, including when the lost node is silent. Expect about 15 seconds after a node failure before that owner is ready, and up to about 25 seconds when the failed node was the leader. Publishes to partitions owned by the surviving nodes continue meanwhile. Clients that retry on `0x97` lose nothing. ### Sessions follow the client With [durability](/latest/mqtt-broker/durability) enabled, a client with a persistent session that reconnects to another node gets its subscriptions back. Messages already queued for it while it was offline stay on the node it was connected to, and are delivered only if it reconnects to that node. See [Limitations](#limitations). *** ## Durability and clustering Clustering and [MQTT state durability](/latest/mqtt-broker/durability) protect different parts of a message's path, and a production cluster needs both: * **Clustering** protects the message after the publisher's acknowledgement: it is stored on a majority of nodes and survives the loss of one of them. * **Durability** protects the subscriber side on each node: persistent sessions, their subscriptions, the messages queued for them, and retained messages survive a restart of that node. | What is lost when … | Clustering only | Clustering + durability | | - | - | - | | a node with persistent subscribers restarts | the messages queued for them, and the messages published while the node was down | nothing | | a node that owns partitions fails | nothing | nothing | | a client reconnects to another node | its subscriptions | the messages queued for it on the old node (delivered only if it reconnects there) | | a node restarts (retained messages) | the node's retained messages | nothing | > [!TIP] > > Enable durability on **every** node of a cluster. Clients without persistent sessions (MQTT 3.1.1 clean session, MQTT 5 session expiry 0) are not affected either way. *** ## LoT in a cluster The cluster runs LoT as **one logical broker**. A client can connect to any node and sees the same definitions, the same shared state, and the same results. A broker that isn't clustered behaves as before. ### The three rules 1. **Same definitions everywhere.** Every node runs the same version of every action, model, connector, rule, Python script, and panel. Changes go through the leader and are replicated to every node. 2. **One place per piece of work.** A message runs LoT on the node it **entered**. A client event runs on the node that holds the connection. Timers, startup, and device connections run on one **owner** node that the cluster picks. 3. **A message from a peer never triggers LoT.** It still reaches that node's subscribers and caches, but the action that reacts to it runs once, where the message came in. ### Where LoT runs | Trigger | Examples | Runs on | | - | - | - | | A message | `ON TOPIC`, `ON CHANGE`, a model's trigger fields | The node the message entered. The output reaches subscribers on every node | | A client event | `ON CONNECT`, `ON DISCONNECT`, `ON SUBSCRIBE` | The node holding the connection | | A timer | `ON EVERY`, `ON EVERY WEEKDAY AT` | The action's owner | | Startup | `ON START` | The action's owner, once per cluster session. See [ON START and KEEP](#on-start-and-keep) | | Called code | `CALL ACTION`, `CALL PYTHON` | Wherever the caller runs. Python module state stays on that node | | Access rules | `DEFINE RULE` | Every node | | Shared state | `KEEP TOPIC` | Where the action runs. The value reaches every node | ### Where connectors run | Class | Connector types | Runs on | | - | - | - | | **Client of a service** | PostgreSQL, MySQL, MariaDB, SQL Server, MongoDB, OpenSearch, CrateDB, Snowflake, REST client, email, LLM, MCP, barcode | Every node. Events run where the message entered; a scheduled event (`WITH EVERY`) has one owner | | **Exclusive connection** | Modbus TCP and serial, Siemens S7, OPC UA client, EtherNet/IP, ADS, Allen-Bradley, FINS, BACnet, Sparkplug host and edge node, MQTT bridge to a remote broker, video input, audio input, `FILE_STORAGE`, `MQTT_TO_REST`, streaming gRPC | **Only its owner.** Other nodes hold the definition and report it as **standby** | | **Server** | `MODBUS_SERVER`, `OPCUA_SERVER`, `VIDEO_OUTPUT`, `REST_API` in server mode | Every node, on `0.0.0.0:`. See [Server connectors](#server-connectors) | | **Group consumer** | `KAFKA` | Every node, in one consumer group. An `"in"` Kafka connector must set `GROUP_ID`, or every node reads every message | | **Local transform** | MQTT bridge from `SELF` to `SELF`, Sparkplug decode | Every node | A `MODBUS_SERVER` on a serial port is an exclusive connection: only its owner opens the port. A `TRIGGER` or `STORE IN` on a node that doesn't own the connector is sent to the owner and runs there. A `FETCH` needs the answer where it was asked, so on another node it fails with a message that names the owner. Use an event that writes to a `DESTINATION_TOPIC` instead. ### Pin a connector To keep an exclusive connector on the node that is cabled to the device, set `CLUSTER_NODE` in `CONNECTOR_CONFIG` (`ROUTE_CONFIG` remains a supported alias) to that node's `NodeName`: ```lot wrap focus={7-8} theme={"theme":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} DEFINE CONNECTOR Press1 WITH TYPE MODBUS_TCP ADD MODBUS_CONFIG WITH CONNECTION_TYPE "TCP" WITH IP "192.168.1.100" WITH PORT 502 WITH SLAVE_ID 1 ADD CONNECTOR_CONFIG WITH CLUSTER_NODE "coreflux-node2" ADD MAPPING Poll WITH EVERY 1 SECONDS WITH SOURCE_TOPIC "press1/read" ADD TAG Temperature WITH ADDRESS "0" WITH ADDRESS_TYPE "HOLDING_REGISTER" WITH DATA_TYPE "INT16" ``` While the pinned node is down, the connector runs nowhere. Leave `CLUSTER_NODE` out to let the cluster place and balance it. ### Change definitions Send LoT commands (`-addAction`, `-removeConnector`, `-loadProject`, MCP tools, the HUB) to **any** node. A follower passes the command to the leader, which validates and commits it. The follower answers once its own node has applied the change. * A command that fails changes nothing on any node. A project load is applied as one change. * Changes are refused, with the reason, while a majority of nodes can't be reached, or while a live node runs a version without replicated LoT. **Upgrade every node before you change LoT.** `lot.mixedMode` in [`GET /cluster`](/latest/mqtt-broker/operational-endpoints#cluster-status) shows when this is the case. * Users and credentials are **not** replicated. Create them on every node: the node that receives a command checks your permission locally. **First start.** Until the first change is committed, each node runs its own LoT. Once the leader has been stable for a few seconds, or on the first LoT command, the leader's definitions become the cluster's. A node whose files differ drops what the cluster doesn't have and saves each definition it removes or replaces under `lot-backup///.lot`. **A node that was away** catches up when it returns: it applies what it missed, removes what the cluster removed, and backs up the files it drops. ### Owners and failover Each timer, `ON START` action, exclusive connector, and scheduled event has **one owner and one standby node**. Every node agrees on the map, and the cluster spreads the work over the live nodes. | Event | What happens | | - | - | | A node stops or is cut off | After 6 s of silence, its work moves to the standby nodes. Nothing else moves | | A node comes back | It takes **new** work, not work that is already running elsewhere | | An owner loses contact with a majority | Within 4 s it stops its timers and closes its device connections itself, before another node takes them over, so a device never sees two clients | | The leader fails | The new leader continues from the committed map | Plan on about **10 seconds** without a timer or device connection when its owner fails. After a takeover: * A timer starts a new cycle, and runs missed during the takeover aren't made up. * `DATE_TIME` timers use the node's local time, so **set the same time zone on every node**. * Python module globals, LLM conversations, and the OT publish-on-change state start fresh on the new owner. * The node that loses a connector doesn't clear its tag topics. ### ON START and KEEP `ON START` runs **once per cluster session**, on the action's owner, and again when the action is redeployed with different code. * A session starts when the cluster forms from nothing and lasts while any of its members is alive. Restarting one node, or all but one, runs nothing. Restarting every node starts a new session. * An `ON START` action runs at most once per session and code version, even if its node fails before it runs. * While a definition fails to apply on a node, `ON START` actions wait on that node. `KEEP TOPIC` values are sent to every node's cache, without reaching subscribers or triggering LoT there. * Values are **eventually consistent**: when two nodes keep the same topic at about the same time, the later write wins. A normal publish to the topic replaces a kept value. * A node that was down gets the values when it returns, and a node keeps its values across a restart. Each value can be up to 1 MB, with up to 100,000 kept topics per node. ### Server connectors A server connector (Modbus server, OPC UA server, REST API server, video output) runs on **every node**, so a load balancer or Kubernetes Service can send a client to any of them. Every node serves the current value of every tag. * **OPC UA behind one name.** Set `WITH ADVERTISED_HOST "opcua.example.com"` in `OPCUA_SERVER_CONFIG` (a host name or address, no scheme, port, or path). To present one identity, put the same PKCS#12 certificate with its private key in `/own/private` on each node, or import it with `-connectorCertificates import`. In a container, set `PKI_DIR` to a writable folder. * **Failed listeners are reported.** `GET /cluster` lists every connector listener under `routeListeners` with its state, also on retained `$SYS/Coreflux/Cluster/RouteListeners` and as `coreflux_route_listener_up` on `/metrics`. A failed bind isn't retried. * **Open every port.** The Service or load balancer must list every server connector port, including ones you add later. `MODBUS_SERVER` defaults to port 502, which a non-root container can't bind: map it to an unprivileged port. ### What is guaranteed * **A LoT run is at most once per message.** If a node fails after a message is accepted but before LoT runs, that run is lost. Failover replay delivers the message again but doesn't run LoT again. * **LoT output isn't quorum-durable.** It's forwarded to the other nodes over the reliable link. * **`ON CHANGE` compares against the last value this node saw.** There is no cluster-wide order per topic. * **QoS 1 redelivery runs an `ON TOPIC` action twice**, as on a single broker. * **Definitions match on every node** once a change is applied. Until then, `lot` in `GET /cluster` shows which node is behind and why. *** ## Configuration reference Set these options in the `Cluster` section of the broker configuration, or with the listed environment variables (environment variables win). Use the same values on every node, except `NodeName`, `AdvertisedHost`, and `BindIP`. ### Identity and discovery | Config field | Env var | Default | Description | | - | - | - | - | | `Enabled` | `COREFLUX_CLUSTER_ENABLED` | `false` | Turns clustering on. The license must also grant the cluster feature | | `ClusterId` | `COREFLUX_CLUSTER_ID` | `coreflux-cluster` | Name of the cluster. Required | | `NodeName` | `COREFLUX_CLUSTER_NODE_NAME` | host name | This node's identity in the cluster. Must be unique and must not change; in Kubernetes, use the StatefulSet pod name | | `Discovery` | `COREFLUX_CLUSTER_DISCOVERY` | `multicast` | How nodes find each other: `seeds`, `kubernetes`, or `multicast` | | `Seeds` | `COREFLUX_CLUSTER_SEEDS` | — | Peer addresses as `host:port`, separated by `,`, `;`, or spaces. Required with `seeds` | | `KubernetesServiceDns` | `COREFLUX_CLUSTER_K8S_SERVICE_DNS` | — | DNS name of the headless Service. Required with `kubernetes` | | `KubernetesPeerPort` | `COREFLUX_CLUSTER_K8S_PEER_PORT` | `Port` | Cluster port of the pods found through that Service | | `MulticastGroup` | `COREFLUX_CLUSTER_MULTICAST_GROUP` | `239.63.114.71` | Multicast group for `multicast` discovery | | `MulticastPort` | `COREFLUX_CLUSTER_MULTICAST_PORT` | `9201` | UDP port for `multicast` discovery | | `SharedSecret` | `COREFLUX_CLUSTER_SHARED_SECRET` | — | Signs all traffic between nodes so it cannot be forged; with `multicast`, also keeps foreign nodes out. **Set it in production**; the broker logs a warning when it is missing | ### Network | Config field | Env var | Default | Description | | - | - | - | - | | `BindIP` | `COREFLUX_CLUSTER_BIND_IP` | `0.0.0.0` | Local address of the cluster port | | `Port` | `COREFLUX_CLUSTER_PORT` | `9200` | Cluster TCP port (leader election and membership) | | `AdvertisedHost` | `COREFLUX_CLUSTER_ADVERTISED_HOST` | `NodeName` | Address the other nodes use to reach this node. Set it when the node's own address is not reachable from its peers (containers, NAT) | | `AdvertisedPort` | `COREFLUX_CLUSTER_ADVERTISED_PORT` | `Port` | Port the other nodes use to reach this node | | `DataPlaneUsername` | `COREFLUX_CLUSTER_DATAPLANE_USERNAME` | — | The cluster service user. It must exist, with a password, on every node. Without it, nodes do not exchange messages or LoT changes. If it is set but missing from a node's users, clustering does not start on that node | | `DataPlaneMqttPort` | `COREFLUX_CLUSTER_DATAPLANE_MQTT_PORT` | broker MQTT port | MQTT port nodes use to reach each other | | `DataPlaneEnabled` | `COREFLUX_CLUSTER_DATAPLANE_ENABLED` | `true` | Forwards QoS 0 messages between nodes. When `false`, QoS 0 messages stay on the node they were published on | | `Topology` | `COREFLUX_CLUSTER_TOPOLOGY` | `mesh` | How QoS 0 messages travel: `mesh` (every node to every node, recommended) or `ring` (to two neighbours only; complete only for up to 3 nodes) | | `UseTls` | `COREFLUX_CLUSTER_TLS` | `false` | Uses TLS for the QoS 0 links between nodes. It does not encrypt the cluster port or the QoS 1/2 links (see [Ports and network](#ports-and-network)) | | `DataDir` | `COREFLUX_CLUSTER_DATA_DIR` | `cluster` folder in the broker data path | Where the node keeps its cluster state and partition copies. Must be on persistent storage | | `PublishInternalRoutes` | `COREFLUX_CLUSTER_PUBLISH_INTERNAL_ROUTES` | `false` | Troubleshooting aid: publishes the connectors the cluster generates between nodes, password redacted, on `$SYS/Coreflux/Cluster/DataPlane/Routes/{name}` | ### Replication | Config field | Env var | Default | Description | | - | - | - | - | | `ReplicationEnabled` | `COREFLUX_CLUSTER_REPLICATION_ENABLED` | `true` | Stores QoS 1/2 messages on several nodes, delivers them reliably to other nodes, and lets sessions follow clients. When `false`, QoS 1/2 messages travel between nodes like QoS 0, and a follower can't pass LoT changes or connector calls to other nodes | | `QuorumGateEnabled` | `COREFLUX_CLUSTER_QUORUM_GATE` | `true` | Waits for a majority before acknowledging QoS 1/2 publishes. When `false`, publishes are acknowledged once the receiving node has them, and are no longer protected against that node's loss | | `ReplicationFactor` | `COREFLUX_CLUSTER_REPLICATION_FACTOR` | `0` (= 3) | Copies of each partition, owner included (0–7), limited by the number of nodes up. `1` gives no protection across nodes | | `PartitionCount` | `COREFLUX_CLUSTER_PARTITION_COUNT` | `32` | Number of partitions (1–4096). Choose it once: changing it on a running cluster reassigns every client | | `ReplicationTimeoutMs` | `COREFLUX_CLUSTER_REPLICATION_TIMEOUT_MS` | `5000` | How long a publish may wait for a majority before it is rejected (100–60000) | | `ReplicationWindow` | `COREFLUX_CLUSTER_REPLICATION_WINDOW` | `1024` | QoS 1/2 publishes that may wait for a majority at the same time on a node (1–65536). Beyond it, publishes are rejected at once with `0x97` | | `ReplicationReplaySeconds` | `COREFLUX_CLUSTER_REPLICATION_REPLAY_SECONDS` | `300` | Messages re-delivered after a failover, in seconds (0 = none, up to 86400). Longer means more duplicates, shorter means more risk of a gap | | `ReplicationForwardQueueEntries` | `COREFLUX_CLUSTER_REPLICATION_FORWARD_QUEUE_ENTRIES` | `8192` | QoS 1/2 messages kept for a node that is down (64–1000000). Beyond it, the oldest are dropped for that node | | `ReplicationCompactEveryRecords` | `COREFLUX_CLUSTER_REPLICATION_COMPACT_EVERY` | `50000` | Compacts a partition's stored messages after this many records, to bound disk use (1000–10000000) | | `ReplicationDedupDelivered` | `COREFLUX_CLUSTER_REPLICATION_DEDUP_DELIVERED` | `true` | The node remembers which quorum-durable messages it delivered and drops failover-replay copies of them. `false` delivers every copy | | `ReplicationTrace` | `COREFLUX_CLUSTER_REPLICATION_TRACE` | `false` | Records each QoS 1 or 2 message on the node under `replication/trace`. Read it with [`GET /cluster/trace`](/latest/mqtt-broker/operational-endpoints#replication-trace) on the ops port (`9100`). About 50 MB per hour per node at 100 messages per second | | `ReplicationTraceMaxMegabytes` | `COREFLUX_CLUSTER_REPLICATION_TRACE_MAX_MB` | `512` | Cap on those trace files, in megabytes (8–8192). The oldest files are deleted first | ### Membership and startup | Config field | Env var | Default | Description | | - | - | - | - | | `BootstrapGraceSeconds` | `COREFLUX_CLUSTER_BOOTSTRAP_GRACE_SECONDS` | `30` | How long a node with no cluster state waits to be added to a running cluster before it may start a new one (1–3600). Also how long a node retries resolving its `AdvertisedHost` and looking for peers that moved | | `BootstrapPeerCheck` | `COREFLUX_CLUSTER_BOOTSTRAP_PEER_CHECK` | `true` | Before starting a new cluster, asks each peer's operations endpoint whether it already belongs to one. A peer that doesn't answer counts as one that might, so the node keeps waiting. Turn off only if the operations ports cannot be reached between nodes | | `PeerOpsPort` | `COREFLUX_CLUSTER_PEER_OPS_PORT` | `0` (= this node's) | Operations port of the peers, for that check | | `MemberRemovalGraceSeconds` | `COREFLUX_CLUSTER_MEMBER_REMOVAL_GRACE_SECONDS` | `300` | The leader removes a node that stays unreachable for this long from the membership, so it no longer counts toward the majority (0–86400). It is added back once discovery lists it and it answers. A member that Kubernetes discovery has stopped listing is removed with `MemberAbsentFromDiscoverySeconds` instead. `0` turns off removal of an unreachable node | | `MemberAbsentFromDiscoverySeconds` | `COREFLUX_CLUSTER_MEMBER_ABSENT_FROM_DISCOVERY_SECONDS` | `120` | With Kubernetes discovery, the leader removes a member that DNS has stopped listing and that fails a TCP probe, after this many seconds (10–86400) | | `MemberAddResignSeconds` | `COREFLUX_CLUSTER_MEMBER_ADD_RESIGN_SECONDS` | `60` | The leader resigns if it has tried this long to add a member that discovery lists and that answers a TCP probe, without the member joining (0 = never; 15–3600). The next leader tries again | | `ElectionTimeoutLowerMs` | `COREFLUX_CLUSTER_ELECTION_LOWER_MS` | `150` | Lower bound of the randomised election timeout. Lower values detect a lost leader sooner | | `ElectionTimeoutUpperMs` | `COREFLUX_CLUSTER_ELECTION_UPPER_MS` | `300` | Upper bound; must be greater than the lower bound | | `ElectionRpcTimeoutMs` | `COREFLUX_CLUSTER_ELECTION_RPC_TIMEOUT_MS` | `0` (= 2/3 of the lower election timeout, 100 ms) | How long a node waits for a vote, pre-vote, or heartbeat reply, and to connect. Must be shorter than `ElectionTimeoutLowerMs`; otherwise clustering does not start | | `HeartbeatThreshold` | `COREFLUX_CLUSTER_HEARTBEAT_THRESHOLD` | `0.25` | The leader sends a heartbeat every election timeout multiplied by this value (0.05–0.5) | | `RaftTransport` | `COREFLUX_CLUSTER_RAFT_TRANSPORT` | `Deadline` | `Deadline` gives votes, pre-votes, and heartbeats their own reply deadline. `Tcp` is the fallback | The broker checks these values at start. If one is invalid, clustering does not start, the log names the problem, and `GET /cluster` reports `enabled: false`. *** ## Ports and network | Port | Protocol | Purpose | Open between | | - | - | - | - | | `9200` (`Port`) | TCP | Leader election and membership | All nodes | | Broker MQTT port (`1883` by default) | TCP | Messages between nodes, as the cluster service user | All nodes | | `9100` (operations port) | TCP | Bootstrap check and monitoring | All nodes, and your monitoring system | | `9201` (`MulticastPort`) | UDP | Discovery, `multicast` mode only | All nodes | > [!WARNING] > > **Keep the cluster port on a private network.** Traffic on port `9200` is neither encrypted nor authenticated. `SharedSecret` signs the messages exchanged between nodes, but not this port. Allow it only between cluster nodes. *** ## Delivery guarantees | QoS | With clustering | | - | - | | **0** | Delivered to subscribers on every node that is up. Lost for a node that is down, or if the publishing node fails, as with any QoS 0 message | | **1** | Acknowledged only once stored on a majority of its partition's copies; otherwise rejected (`0x97`/`0x80`) and not delivered. Delivered to subscribers on every node, including nodes that are down when it is published (when they return). At least once: after a failover, some messages can arrive twice | | **2** | The same as QoS 1. Between a client and its node the QoS 2 exchange works as usual, but across a node failure delivery is **at least once**, not exactly once | Design consumers to tolerate duplicates, for example with a message id or sequence number in the payload. For new designs, QoS 1 gives the same guarantee as QoS 2 in a cluster, with one round trip less. *** ## Operations ### Starting, restarting, and scaling * **First start:** expect about `BootstrapGraceSeconds` (30 s) before the cluster forms. Until then, `GET /cluster` reports `enabled: false` or no leader. * **Restarts:** a node rejoins with its previous identity and data. Restart one node at a time, and wait for `GET /cluster` to show it back before restarting the next. * **Adding a node:** start it with the same cluster settings and a new `NodeName`, and add it to `Seeds` on the other nodes if you use seeds. The leader admits it and moves some partitions to it. * **Removing a node:** stop it. After `MemberRemovalGraceSeconds` it leaves the membership and its partitions stay on the remaining nodes. In Kubernetes, a pod the headless Service no longer lists leaves after about 2 minutes (`MemberAbsentFromDiscoverySeconds`, 120 seconds by default), once its cluster port fails a TCP probe. * **Upgrades:** all nodes of a cluster must run the same broker version. To upgrade a cluster from 2.14 or earlier, stop all nodes and upgrade them together. LoT changes are refused until every node runs a version that replicates LoT. ### Monitoring | Signal | What it shows | | - | - | | [`GET /cluster`](/latest/mqtt-broker/operational-endpoints#cluster-status) | The node's full cluster view: leader, members, partition ownership, replication and forwarding counters | | [`GET /cluster/trace`](/latest/mqtt-broker/operational-endpoints#replication-trace) | Each QoS 1 or 2 message on the node, when `ReplicationTrace` is on | | [`GET /info`](/latest/mqtt-broker/operational-endpoints#cluster-and-mcp-summaries) | A short `cluster` summary: role, leader, member count | | [Prometheus `/metrics`](/latest/mqtt-broker/operational-endpoints#when-durability-or-clustering-is-active) | `coreflux_cluster_*`, `coreflux_partition*`, and `coreflux_replication_*` series | | Retained `$SYS` topics | `$SYS/Coreflux/Cluster/Role`, `IsLeader`, `Leader`, `Term`, `Members`, and `Status` (a JSON summary), refreshed every 7 seconds | Watch these values; on a healthy cluster they stay at the healthy value: | Value | Healthy | When it is not | | - | - | - | | Leader (`leadership.leader` in `/cluster`) | the same node on every node | Elections in progress, or a node cut off from the others | | `membersTotal` | your number of nodes, on every node | A larger value means a node's old address is still listed; it is removed within seconds when the node answers at its new address | | `membersAlive` on the leader | equal to `membersTotal` | A node is down or unreachable (followers report other nodes as `unknown`; trust the leader's view) | | Rejected publishes (`coreflux_replication_quorum_failures_total`) | not increasing | Clients are getting `0x97`: a failover is in progress, or there is no majority | | Messages waiting for other nodes (`coreflux_replication_forward_backlog`) | close to `0` | A node is down or slow; the queue empties when it catches up | | Messages dropped for a node that was down too long (`coreflux_replication_forward_dropped_total`) | `0` | Subscribers on that node missed messages; increase `ReplicationForwardQueueEntries` or bring nodes back sooner | Use MQTT Explorer or any MQTT client to subscribe to `$SYS/Coreflux/Cluster/#`. *** ## Best practices > **Do** > > * Run 3 or 5 nodes, and spread them across failure domains (hosts, racks, zones). > * Set `SharedSecret`, and keep the cluster port on a private network. > * Enable durability on every node. > * Use MQTT 5 for QoS 1/2 publishers, and retry on `0x97`. > * Make consumers tolerate duplicates. > * Give each node a stable `NodeName` and persistent storage for `DataDir` and the durability data. > * Deploy LoT through any one node and let the cluster place it; pin an exclusive Connector to a node with `CLUSTER_NODE` only when the device is cabled to that node (see [LoT in a cluster](#lot-in-a-cluster)). > * Create the same users on every node, and use the same time zone and synchronised clocks. > **Don't** > > * Don't run 2 nodes: losing either one stops QoS 1/2 publishing, because 1 of 2 is not a majority. > * Don't set `ReplicationFactor` to `1` when you need messages to survive a node failure. > * Don't change `PartitionCount` on a running cluster. > * Don't restart several nodes at once on a 3-node cluster: QoS 1/2 publishing pauses until a majority is back. *** ## Limitations > [!WARNING] > > **Offline queues stay on their node.** A persistent session's subscriptions follow a client to another node (with durability), but messages already queued for it stay on the node it was connected to, and are delivered only if it reconnects there. > [!WARNING] > > **Users and credentials are kept per node.** Create the same users on every node. LoT definitions, including Rules, are replicated; users are not. > [!WARNING] > > **Retained messages are kept per node.** Each node keeps its own copy of retained messages. A node added later does not receive retained messages published before it joined, and a node that was down misses retained QoS 0 messages published meanwhile. > [!WARNING] > > **At least once, not exactly once.** After a failover, subscribers can receive some messages twice, at QoS 1 and QoS 2. > [!WARNING] > > **MQTT 3.1.1 publishers cannot see rejected publishes.** When the cluster cannot reach a majority, a 3.1.1 client still receives a normal acknowledgement for a message that was not accepted. Use MQTT 5 for QoS 1/2 publishers. > [!WARNING] > > **One publishing connection is limited by the time to reach a majority.** Each QoS 1/2 message waits for its majority before the broker reads the same connection's next message. Total throughput grows with the number of publishing connections. A single connection that publishes at high rate is limited to roughly one message per round trip between nodes. > [!WARNING] > > **Shared subscriptions are per node.** A shared subscription group includes only the clients connected to the same node; each node delivers a message to one member of its own group. See [shared subscriptions](/latest/mqtt-broker/configuration#shared-subscriptions). > [!WARNING] > > **Agents are not replicated.** An agent (`DEFINE AGENT`) exists only on the node where you defined it. It is not copied to the other nodes, does not move when a node fails, and a `-removeAgent` on one node does not remove it from the others. Define an agent on every node that should run it. The LLM Connector an agent uses is an ordinary Connector and is replicated. > [!WARNING] > > **Agents inside a Thing.** Currently: > > * The leader deploys a Thing (`-addThing`). Its definition and its connectors, models, actions, rules, and panels replicate to every node. > * A `DEFINE AGENT` inside the Thing is not replicated. It currently runs only on the node that was leader when the Thing was deployed, which is not necessarily the node that received the command. > * The agent's conversation memory stays on that node and is never replicated. > * The agent does not move on failover. If its node is down, the agent is unavailable until that node returns. > * On the other nodes the Thing still lists the agent, but the agent is not running there, so Thing health on those nodes may show it as not ok. Its status topics are visible on every node. *** ## Next Steps > **[MQTT State Durability](/latest/mqtt-broker/durability)** > > Keep sessions, queued messages, and retained messages across restarts on every node. > **[Operational Endpoints](/latest/mqtt-broker/operational-endpoints)** > > Monitor each node with /cluster, /info, and Prometheus metrics. # 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, Connectors, projects, secrets, and more. > [!TIP] > > **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, Connectors, 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 * Mint, list, and revoke [MCP Server](/latest/mqtt-broker/mcp-server) bearer tokens On a [cluster](/latest/mqtt-broker/clustering) (beta), send a command that adds, changes, removes, or loads LoT (actions, models, connectors, rules, Python scripts, panels, Things, projects) to **any** node, once. A follower passes it to the leader, which commits it on a majority of the nodes, and every node applies it. The node that receives the command checks your permission. Other commands, such as users, agents, licenses, and certificates, run only on the node you are connected to. See [LoT in a cluster](/latest/mqtt-broker/clustering#lot-in-a-cluster). *** Subscribe first, then publish. Replies are always JSON on a single output topic. ## Quick Start **Deploy an Action** 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":{"light":"css-variables","dark":"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":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} { "success": true, "command": "AddAction", "message": "Action 'Heartbeat' added successfully.", "entityType": "Action", "entityName": "Heartbeat", "data": {} } ``` **List Connectors** List commands put the structured result in `data`. `message` stays a short human summary. This example sends `-listConnectors`. The reply's `command` field stays `ListRoutes`, and `-listRoutes` remains a supported alias. ```bash wrap theme={"theme":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} -listConnectors -requestId connectors-1 ``` ```json wrap theme={"theme":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} { "success": true, "command": "ListRoutes", "requestId": "connectors-1", "message": "Listed 1 connector(s).", "data": [ { "name": "CloudBridge", "type": "MQTT_BRIDGE", "connection": "Connected", "health": "Green" } ] } ``` **Correlate replies** Every client shares `$SYS/Coreflux/Command/Output`. Pass `-requestId` so you can match the reply to the request you sent. ```bash wrap theme={"theme":{"light":"css-variables","dark":"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":{"light":"css-variables","dark":"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. > [!WARNING] > > 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":{"light":"css-variables","dark":"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":{"light":"css-variables","dark":"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." } ] } ``` The reply currently names the Route form of the command, even when the client sent `-addConnector`. **Unknown command:** ```json wrap theme={"theme":{"light":"css-variables","dark":"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 (`-addConnector` / `--add-connector`). The table lists the MQTT form you publish. Each Connector command also has a Route spelling (`-addConnector` and `-addRoute` run the same command), kept as an alias. See [Connector command spellings](#connector-command-spellings). | 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`, `-disableAction`, `-enableAction` | | **Connectors** | `-addConnector` (`-addRoute`), `-removeConnector` (`-removeRoute`), `-removeAllConnectors` (`-removeAllRoutes`), `-listConnectors` (`-listRoutes`), `-connectorCode` (`-routeCode`), `-checkConnectorConnection` (`-checkRouteConnection`), `-connectorCertificates` (`-routeCertificates`), `-disableConnector` (`-disableRoute`), `-enableConnector` (`-enableRoute`), `-listTemplates`, `-connectionStatus`, `-discoverAIProviders` | | **Agents** | `-addAgent`, `-removeAgent`, `-listAgents`, `-disableAgent`, `-enableAgent` | | **Things** | `-addThing` (`--dryRun`), `-removeThing`, `-listThings` | | **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` | | **MCP tokens** | `-createMcpToken`, `-listMcpTokens`, `-revokeMcpToken` | | **License** | `-activateLicense`, `-loadLicense`, `-refreshLicense`, `-enroll` | | **Audit** | `-auditStatus`, `-auditQuery`, `-auditFilter`, `-decryptAudit`, `-auditExport` | | **Certificates** | `-brokerCertificates` (admin only) | | **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). These commands manage the local user store. An [OIDC access token](/latest/mqtt-broker/oidc-authentication) is not a local user: there is no command to create one, and `-listUsers` does not show token sessions. ```bash wrap theme={"theme":{"light":"css-variables","dark":"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. > [!NOTE] > > 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":{"light":"css-variables","dark":"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":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} -removeRule RestrictSensorPublish ``` > [!NOTE] > > 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":{"light":"css-variables","dark":"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":{"light":"css-variables","dark":"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":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} -removeModel TemperatureSensor -removeAllModels ``` > [!WARNING] > > `-removeAllModels` removes every Model on the broker. *** ## Actions ```bash wrap theme={"theme":{"light":"css-variables","dark":"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":{"light":"css-variables","dark":"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":{"light":"css-variables","dark":"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":{"light":"css-variables","dark":"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":{"light":"css-variables","dark":"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":{"light":"css-variables","dark":"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":{"light":"css-variables","dark":"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. *** ## Agents Agents are their own LoT entity. `-addConnector` refuses a `DEFINE AGENT` block, and `-listConnectors` does not include agents. See [AI Connectors and Agents](/latest/lot-language/routes/ai-routes). ```bash wrap theme={"theme":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} -addAgent DEFINE AGENT Planner USES LLM CONNECTOR LocalLlama WITH DEFAULT_USER "agent_svc" ADD TOOL "read_lot" -removeAgent Planner -listAgents ``` `-addAgent` on a name that already exists updates that agent and keeps its conversations. `-removeAgent` cancels running tasks and forgets session memory. `-removeConnector` refuses to remove a `TYPE LLM` connector an agent still uses. `-addAgent` requires `AgentManagementCreation`. `-removeAgent` requires `AgentManagementRemove`. `root`, `admin`, and `AllowedSystemConfiguration` pass those gates, as they do for connectors. *** ## Connectors ```bash wrap theme={"theme":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} -addConnector DEFINE CONNECTOR 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":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} -removeConnector CloudBridge -removeAllConnectors -listConnectors -connectorCode POSTGRESQL -listTemplates -checkConnectorConnection CloudBridge -connectionStatus ``` ### Connector command spellings Each Route command has a Connector spelling. The Route names (`-addRoute`, `-listRoutes`, and the rest) stay as aliases. Both forms run the same command, and the reply's `command` field keeps the Route name (`AddRoute`, `ListRoutes`, …), so scripts that check it keep working. | Route spelling | Connector spelling | Reply `command` | | - | - | - | | `-addRoute` / `--add-route` | `-addConnector` / `--add-connector` | `AddRoute` | | `-removeRoute` / `--remove-route` | `-removeConnector` / `--remove-connector` | `RemoveRoute` | | `-removeAllRoutes` / `--remove-all-routes` | `-removeAllConnectors` / `--remove-all-connectors` | `RemoveAllRoutes` | | `-listRoutes` / `--list-routes` | `-listConnectors` / `--list-connectors` | `ListRoutes` | | `-routeCode` / `--route-code` | `-connectorCode` / `--connector-code` | `GetRouteTemplate` | | `-checkRouteConnection` / `--check-route-connection` | `-checkConnectorConnection` / `--check-connector-connection` | `CheckRouteConnection` | | `-routeCertificates` / `--route-certificates` | `-connectorCertificates` / `--connector-certificates` | `RouteCertificates` | `-addConnector` takes the same body as `-addRoute`, written with either `DEFINE CONNECTOR` or `DEFINE ROUTE`: ```bash wrap theme={"theme":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} -addConnector DEFINE CONNECTOR 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" ``` When you start the broker with a Connectors file on the command line, use `--lot-connectors `. `-lotrt` and `--lot-routes` remain supported aliases. `-listConnectors` places entries in `data` (`name`, `type`, `connection`, `health`). Unmonitored connectors report `"Unknown"` for connection and health. `-checkConnectorConnection` tests the named Connector. Details such as `routeType` and `connectionTime` are in `data`. What “connected” means depends on the Connector type — see the Connector 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)). `-connectorCode` returns template LoT for that Connector type, or for a [REST API app](/latest/lot-language/routes/rest-api-routes#rest-api-app-templates) by its app code, in `data`. `-listTemplates` lists available templates. `-connectorCertificates` manages PKI for Connectors 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":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} -connectorCertificates cert64: ``` `-discoverAIProviders` probes configured LLM providers for reachability and models. API keys and host URLs come from secrets and environment variables (`OPENAI_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":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} -discoverAIProviders ``` *** ## Disable and Enable These commands switch an action, connector, or agent off or on without deleting it. Each one redeploys the stored definition with a `DISABLE true` line added right after the `DEFINE` line, or removed, through the normal add path. An entity already in the requested state is left alone and the command still succeeds. | Command | Entity | MCP tool | | - | - | - | | `-disableAction ` / `-enableAction ` | Action | `disable_action` / `enable_action` | | `-disableConnector ` / `-enableConnector ` (aliases `-disableRoute` / `-enableRoute`) | Connector | `disable_route` / `enable_route` | | `-disableAgent ` / `-enableAgent ` | Agent | `disable_agent` / `enable_agent` | ```bash wrap theme={"theme":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} -disableConnector PlantHistorian -enableConnector PlantHistorian ``` They need the same permission as adding that entity (`ActionManagementCreation`, `ConnectorManagementCreation`, `AgentManagementCreation`). `RouteManagementCreation` remains a supported alias of `ConnectorManagementCreation`. With a live project, the change is written back to its notebook cell. A connector, action, or agent that belongs to a Thing is refused: change the Thing instead. `-disableConnector` refuses a `TYPE LLM` connector that agents still use. What a disabled entity does: [Actions](/latest/lot-language/actions/syntax#disable-an-action), [Connectors](/latest/lot-language/routes/overview#disable-a-connector), [Agents](/latest/lot-language/agents/syntax#disable-an-agent). *** ## Things ```bash wrap theme={"theme":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} -addThing --dryRun DEFINE THING TEMPLATE TankLevel … -addThing DEFINE THING TEMPLATE TankLevel … DEFINE THING TankFarm … -listThings -removeThing TankFarm ``` | Command | What it does | Permission | | - | - | - | | `-addThing [--dryRun] ` | Deploys every Thing in the payload as one unit. `--dryRun` returns the plan and each child's expanded code, and changes nothing. `data` holds `things`, `affectedRoots`, and the per-root `plan` | `ThingManagementCreation` | | `-removeThing ` | Removes a root and its children in reverse order, or an unused template. A template still in use is refused | `ThingManagementRemove` | | `-listThings` | Lists every Thing in `data`: `name`, `kind`, `template`, `uses`, `usedBy`, `childCount`, `project` | `CommandCall` | The children a Thing creates are read-only: `-addAction`, `-removeConnector`, and the other per-entity commands refuse them and name the owning Thing. `-removeAllActions`, `-removeAllModels`, and `-removeAllConnectors` skip them. See [LoT THING](/latest/lot-language/things/overview). *** ## Diagnostics ```bash wrap theme={"theme":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} -lotDiagnostic true -lotDiagnostic false -getPendingStatus ``` `-lotDiagnostic` turns global LoT diagnostic tracing on or off. The only argument is `true` or `false`. Anything else returns `Invalid diagnostic value`. *** ## 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":{"light":"css-variables","dark":"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 ``` > [!WARNING] > > **Quote values that contain `//`, a space, or `#`.** The command is cleaned before it runs. Outside double quotes, `//`, `/*` and `-- ` start a comment and cut off the rest of the line, and a space ends the value. Wrap the value in double quotes: `-setEnv API_URL="https://api.example.com/v1"`. The quotes are removed before the value is stored. The same rule applies to `-setSecret`. `-listEnv` returns name/value pairs in `data`. `-listSecrets` returns **names only** — secret values never appear in command output. *** ## Python Integration ```bash wrap theme={"theme":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} -addPython Calculator # Script Name: Calculator def add(a, b): return a + b ``` > [!NOTE] > > 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":{"light":"css-variables","dark":"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":{"light":"css-variables","dark":"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":{"light":"css-variables","dark":"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). > [!NOTE] > > `-loadProject` and `-unloadProject` remove only entities the broker attributes to that project. Actions, Models, Connectors, Python scripts, and panels you added with `-addAction` (and similar) while no project was active stay running. ```bash wrap theme={"theme":{"light":"css-variables","dark":"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. Quote a git URL so the address stays one argument. The branch name is the next argument: `-addProject "https://git.example.com/factory-iot.git" main`. `-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 Connector-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`. *** ## MCP Tokens Bearer tokens for the [MCP Server](/latest/mqtt-broker/mcp-server). These verbs are operator-only (`McpTokenManagement`) and are **not** advertised as MCP tools. ```bash wrap theme={"theme":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} -createMcpToken operator cursor-local -listMcpTokens -listMcpTokens operator -revokeMcpToken cursor-local ``` `-createMcpToken [ttlDays]` returns the plaintext **once** in `data.token`. The username must already exist; tool calls run as that user. Token names cannot contain whitespace. `-listMcpTokens` returns metadata in `data.tokens` — never the secret. MQTT replies always land on `$SYS/Coreflux/Command/Output`. That topic's default audience is wider than `McpTokenManagement`. Prefer the broker console or the [HUB MCP tab](/hub/system/configuration#mcp) when you need the plaintext off that lane. *** ## License Licenses are signed tokens. Register at [portal.coreflux.org](https://portal.coreflux.org) for a license — including a **30-day Growth-type** trial — and manage your fleet of brokers there. Full operator guide: [Licensing and Enrollment](/latest/mqtt-broker/licensing). ```bash wrap theme={"theme":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} -activateLicense CFLX-XXXX-XXXX-XXXX -loadLicense /path/to/license.jws -loadLicense eyJhbGciOiJFUzI1NiIsInR5cCI6IkNGTFhMSUMi.….. -refreshLicense -enroll et_… ``` | Command | What it does | | - | - | | `-activateLicense ` | Exchanges a `CFLX-…` activation code for a lease and applies the plan immediately. Requires `SystemConfiguration`. No restart. | | `-loadLicense ` | Verifies a signed license — compact JWS text, or a path on the broker host — and stages `config/license.jws`. | | `-refreshLicense` | Forces one online lease refresh instead of waiting for the 24-hour timer. Refused when the broker holds no lease. | | `-enroll ` | Stages a single-use enrollment token. The token is never echoed in the reply; the exchange runs in the background. | `-loadLicense` treats the argument as the license itself when it has the shape of a compact JWS, and as a filesystem path otherwise. That is how Coreflux HUB installs an offline license without writing a file on the broker host. ```json wrap theme={"theme":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} { "success": true, "command": "LoadLicense", "message": "Signed license verified and stored at /license.jws", "data": {} } ``` *** ## Broker Certificates `-brokerCertificates` manages the broker's TLS server certificate, its Root CA, and the mTLS client trust store (`ClientCertificateSourcePath`). It is what [HUB Certificates](/hub/system/certificates) uses. It needs administrator rights (`AllowedSystemConfiguration`), and it is not exposed to the MCP server or to agents, because requests carry certificate bytes and passphrases. Changes are audit-logged. The argument is `cert64:` plus base64 of a JSON request with an `op` and an optional `requestId` that is echoed back. ```bash wrap theme={"theme":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} -brokerCertificates cert64: ``` | `op` | Fields | Result | | - | - | - | | `overview` | — | Trust store settings, server certificate, Root CA, TLS port | | `list` | — | Path status, threshold, and every file in the storage path and `revoked/` | | `import` | `fileName`, `dataB64`, `password` (PKCS#12) | Stores and trusts the file. Private keys in PKCS#12 or combined PEM uploads are discarded. 10 MB limit | | `revoke` | `fileName` | Moves the file to `revoked/`. New connections that present it are rejected immediately | | `export` | `fileName`, `revoked` (optional) | `fileName` and `dataB64` of a certificate file. Files with private keys and symbolic links are refused | | `setSettings` | `storagePath`, `resetPath`, `expiryThresholdDays`, `clientCertificationValidation`, `checkCertificateChainValidation` (all optional) | Saves the settings and returns the overview. `clientCertificationValidation` fully applies after a restart | | `replaceServer` | `fileName`, `dataB64`, `keyB64` (optional), `password` (optional) | Replaces the server certificate with a timestamped `.bak-` copy of the old files. Hot-swapped when TLS is already running, otherwise a restart is needed | | `replaceRootCa` | `fileName`, `dataB64` | Replaces the Root CA with a timestamped `.bak-` copy and applies it to chain validation | | `exportServer` / `exportRootCa` | — | The public certificate as PEM | ```json wrap theme={"theme":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} { "op": "revoke", "requestId": "r1", "fileName": "plc-line-a.pem" } ``` The reply's `message` is JSON: `requestId`, `op`, `success`, a human `message`, and `data`. Mutating ops also republish [`$SYS/Coreflux/Certificates/Summary`](/latest/mqtt-broker/sys-topics#syscorefluxcertificatessummary). ## Audit ```bash wrap theme={"theme":{"light":"css-variables","dark":"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":{"light":"css-variables","dark":"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). > [!WARNING] > > Configure access control before any untrusted client can publish to `$SYS/Coreflux/Command`. *** ## Next Steps > **[System (\$SYS) Topics](/latest/mqtt-broker/sys-topics)** > > Subscribe to command output, entity code, and broker telemetry. > **[Command Console](/hub/system/command-console)** > > 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. > [!NOTE] > > Most configuration changes require a broker restart to take effect. Plan those updates during a maintenance window. Updated bytes in the TLS certificate files are the exception: the broker re-reads them on a timer and uses them for new handshakes. See [TLS Server Certificate](#tls-server-certificate). > [!NOTE] > > 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). > [!NOTE] > > To expose the broker as an MCP server for AI clients, configure the separate [`Mcp`](/latest/mqtt-broker/mcp-server) section (or `COREFLUX_MCP_*` environment variables). `TYPE MCP_SERVER` was removed — this is not a LoT connector. > [!NOTE] > > To accept an OIDC access token as the MQTT CONNECT password, configure the [`Oidc`](/latest/mqtt-broker/oidc-authentication) object in this file. There is no `COREFLUX_OIDC_*` variable. Enabling it forces `AnonymousLogin` off. *** ## 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":{"light":"css-variables","dark":"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, "SharedSubscriptions": false, "MaxPendingMessagesPerClient": 20000, "FullDebugLog": false, "CloudLogging": false, "ResendRetainTopics": false, "ClientCertificationValidation": false, "CheckCertificateChainValidation": false, "CheckCertificateRevokationList": false, "ServerCertificatePath": "", "ServerCertificateKeyPath": "", "ServerCertificatePassword": "", "ServerCertificateReloadIntervalSeconds": 3600, "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` | | `SharedSubscriptions` | `false` | > [!WARNING] > > 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: **Port** `integer` (default: 1883) Standard MQTT port for unencrypted connections. This is the default port used by most MQTT clients. **TlsPort** `integer` (default: 8883) MQTT port for TLS-encrypted connections. Requires valid server certificate configuration. **WebsocketPort** `integer` (default: 5000) Port for MQTT over WebSocket connections. Used by browser-based clients and applications that cannot use raw TCP. **WebsocketPortTls** `integer` (default: 443) Port for MQTT over WebSocket with TLS. The default is `443` on the broker host. A port below 1024 needs permission to bind it on the broker host. Linux APT and DNF packages grant that permission. See [Ports below 1024](/latest/mqtt-broker/startup-options#ports-below-1024). > [!WARNING] > > 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: **BindIPForMQTT** `string` 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. **BindIPForMQTTwithTls** `string` IP address to bind for TLS-encrypted MQTT connections. **BindIpForWebSockets** `string` IP address to bind for WebSocket connections. **BindIpForWebSocketsTls** `string` IP address to bind for WebSocket connections with TLS. #### Example: Restrict to Local Network ```json wrap theme={"theme":{"light":"css-variables","dark":"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":{"light":"css-variables","dark":"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 **AnonymousLogin** `boolean` (default: true (first run)) 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. > [!WARNING] > > Never leave `AnonymousLogin` enabled in production environments. Always require authentication for broker access. When [`Oidc.Enabled`](/latest/mqtt-broker/oidc-authentication) is true, the broker forces `AnonymousLogin` off at startup. Passwords that are access tokens are checked with the identity provider. Any other password is still checked against the user store. ### TLS Server Certificate Configure TLS encryption for secure client connections: **ServerCertificatePath** `string` 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. The broker re-reads this file (and `ServerCertificateKeyPath`, when set) on the interval below. A renewed certificate is used for **new** MQTTS and WSS handshakes. Clients that are already connected stay on the certificate they negotiated. **ServerCertificateKeyPath** `string` 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. File contents are re-read on the same interval as the certificate. **ServerCertificatePassword** `string` Password for an encrypted private key or PFX bundle. If `ServerCertificatePath` is set but this is empty, a warning is logged at startup. `$SYS/Coreflux/Config` shows it as `********`. Publishing that mask back to `$SYS/Coreflux/Config/New` keeps the stored password. You need a password only when the PEM key is encrypted. cert-manager writes an unencrypted `tls.key` by default, so leave this empty for that key. A password set beside an unencrypted key does not block loading. **ServerCertificateReloadIntervalSeconds** `integer (seconds)` (default: 3600) How often the broker checks `ServerCertificatePath` and `ServerCertificateKeyPath` for new file contents. Default `3600` (one hour). Set `0` to disable reloading. When the contents change, the next MQTTS or WSS handshake uses the new certificate. Existing sessions stay connected. The broker compares file contents, so a timestamp change with the same bytes does not swap the certificate. A detected change is read again after one second so a file that is still being written is not loaded. If the new files fail to load, the previous certificate stays in use. The first check runs one full interval after startup. Changing this value, the certificate **paths**, listener ports, or mTLS mode still requires a broker restart. Allowed values are `0` or a positive number of seconds, up to `4294966` (about 49.7 days). > [!NOTE] > > Keep the reload interval shorter than your certificate renewal margin. With the default of one hour, a renewed file can wait up to an hour before new handshakes pick it up. cert-manager renews at `renewBefore`, which for a typical 90-day certificate is days ahead of expiry, so the default sits inside that margin. Use a shorter interval (for example `60` to `300`) for short-lived certificates. Each check is two file reads and a hash. > [!WARNING] > > Do not mount the certificate with `subPath`. A `subPath` mount captures the file at mount time and never updates when the Secret changes, so the broker never sees a renewal. Mount the Secret as a directory and point `ServerCertificatePath` at a file inside it. **RootCACertificatePath** `string` Path to the root CA certificate for certificate chain validation. #### Example: TLS Configuration ```json wrap theme={"theme":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} { "ServerCertificatePath": "/certs/server.crt", "ServerCertificateKeyPath": "/certs/server.key", "ServerCertificatePassword": "your-certificate-password", "ServerCertificateReloadIntervalSeconds": 3600, "RootCACertificatePath": "/certs/ca.crt" } ``` ### Mutual TLS (mTLS) For environments requiring client certificate authentication: **ClientCertificateSourcePath** `string` Folder of trusted client certificates (the mTLS trust store). The broker loads every `.pem`, `.crt`, `.cer`, `.der`, and unprotected `.p12`/`.pfx` file in it (not recursive). Certificates in its `revoked/` sub-folder are rejected. Empty means the default trust store under the broker data directory: `$COREFLUX_DATA_PATH/certs`, else `$CONFIG_PATH/certs`. That is `/etc/project/certs` in the Docker and Kubernetes images and `/var/lib/coreflux/certs` for apt/dnf packages; with neither variable set it is `/var/lib/coreflux/certs` on Linux and macOS, and `C:\ProgramData\Coreflux\certs` on Windows. The broker creates the folder at startup if it is missing. If it can't (read-only, or not owned by the broker user), the broker starts with an empty trust store and the HUB shows the path as **Unavailable**. In containers, keep the default so certificates live on the `/etc/project` volume; a custom path must be on a writable, persisted volume owned by UID/GID `10001`. When you set it from the HUB or with `-brokerCertificates`, the path must be absolute, outside system folders and the server certificate's folder, and, on Linux and macOS, under an allowed root (`/var/lib`, `/opt`, `/srv`, `/data`, `/mnt`, `/media`, `/home`, `/Users`, temp folders, or the `COREFLUX_DATA_PATH` / `CONFIG_PATH` folders). Extend the roots with `COREFLUX_CERT_STORAGE_ROOTS`. Symbolic links are resolved before the check. A subfolder of `CONFIG_PATH` is allowed even under a system folder, so `/etc/project/certs` works in Docker. Use a `certs` subfolder on the project volume. The `CONFIG_PATH` folder itself is refused, and so is anything under `/etc` when `CONFIG_PATH` is `/etc`. Uploads through the HUB or `import` accept certificates only (`.crt`, `.pem`, `.cer`, `.der`, `.p12`, `.pfx`); private keys and CSRs are refused. See [HUB Certificates](/hub/system/certificates#trust-store-settings). **CertificateExpiryThresholdDays** `integer (days)` (default: 30) Days before expiry at which a client certificate, the server certificate, or the Root CA is reported as expiring on `$SYS/Coreflux/Certificates/Summary`. 1 to 365. **ClientCertificationValidation** `boolean` (default: false) Enable client certificate validation. When `true`, TLS connections **require** a client certificate (`ClientCertificateMode.RequireCertificate`). When `false`, client certificates are optional (`AllowCertificate`). **ClientCertificateRevocationListPath** `string` Path to the Certificate Revocation List (CRL) file for checking revoked certificates. When it is a folder, certificates in it are also treated as revoked, alongside the trust store's `revoked/` sub-folder. **CheckCertificateRevokationList** `boolean` (default: false) Enable CRL checking for client certificates. **CheckCertificateChainValidation** `boolean` (default: false) Enable full certificate chain validation for client certificates against `RootCACertificatePath`. When `false`, only the trusted list is checked. Changing it from the HUB takes effect immediately. > [!TIP] > > In HUB 2.12, **System Config → Certificates** shows and edits these settings, replaces the server certificate and Root CA without a restart, and uploads or revokes client certificates. See [Certificates](/hub/system/certificates). #### Example: mTLS Configuration ```json wrap theme={"theme":{"light":"css-variables","dark":"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" } ``` > [!TIP] > > 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 **CleanSession** `boolean` (default: false) **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. > [!NOTE] > > **Naming note:** `CleanSession = true` on the broker means "force clean sessions from clients", not "enable clean session". > [!WARNING] > > **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. **SessionExpiryInterval** `integer (seconds)` (default: 0) **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. ### Shared subscriptions **SharedSubscriptions** `boolean` (default: false) Enables MQTT 5 shared subscriptions (`$share/{group}/{filter}`). Each message that matches the filter is delivered to **exactly one** member of that group — round-robin, preferring members that are connected. A client subscribed to the same filter without `$share` still receives every matching message. Use this when a pool of workers should share the load on one topic. `$share/workers/jobs/#` and `$share/other/jobs/#` are separate groups, because the group is the full share filter. * `false` (default) — a `$share/...` SUBSCRIBE is refused with reason code `0x9E` (Shared Subscriptions Not Supported). CONNACK reports Shared Subscription Available = `0`. * `true` — share filters are accepted. CONNACK reports Shared Subscription Available = `1`. A malformed share filter (empty group name, `+` or `#` in the group name, or a missing filter) is refused with `0x8F` (Topic Filter Invalid). `COREFLUX_MQTT_SHARED_SUBSCRIPTIONS` overrides this field: `1` or `true` enables shared subscriptions, `0` or `false` disables them. Restart the broker after changing the field or the variable. In a cluster, in this version, each shared group is local to the broker host the members are connected to. > [!NOTE] > > MQTT 3.1.1 clients can use the same `$share/` prefix once shared subscriptions are enabled. See [Shared subscriptions](/latest/mqtt-broker/protocol#shared-subscriptions). **ResendRetainTopics** `boolean` (default: false) 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 **MaxPendingMessagesPerClient** `integer` (default: 20000) 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. > [!WARNING] > > `MaxPendingMessagesPerClient` must be between `1` and `100000`. A value outside this range prevents the broker from starting. Per-connector wait-or-drop when a LoT connector cannot keep up is configured in LoT with `ADD CONNECTOR_CONFIG` (`ADD ROUTE_CONFIG` remains a supported alias). 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 connector updates the broker holds at once. Values below `1024` are raised to `1024`. | See [Store and Forward Configuration](/latest/lot-language/routes/store-and-forward) for the LoT block, per-connector 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. **ProjectPath** `string (directory path)` 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. **UsersFile** `string (file path)` 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. **EnvsPath** `string (directory path)` 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. **SecretsPath** `string (directory path)` 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 connectors 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 **FullDebugLog** `boolean` (default: false) Enable detailed debug logging: connection validation details, every publish/subscription intercept, TCP health noise, etc. Useful for troubleshooting but may impact performance in production. > [!WARNING] > > Only enable `FullDebugLog` for troubleshooting. Debug logging generates significant log volume and may impact broker performance. **CloudLogging** `boolean` (default: false) 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 **Name** `string` (default: Coreflux DataHub) 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). A partial object is merged into the live configuration: only the fields you include change, and every field you omit stays as it is, including bind addresses. | 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":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} {"Name":"ProductionBroker","Port":1883,"TlsPort":8883,"AnonymousLogin":false} ``` > [!NOTE] > > The runtime handler currently persists only a subset of fields (`AnonymousLogin`, bind IPs, ports, `FullDebugLog`). A restart is required for listener, session, and certificate **path** changes to take effect. The **contents** of the files at `ServerCertificatePath` and `ServerCertificateKeyPath` are picked up on `ServerCertificateReloadIntervalSeconds` and do not require a restart. ### 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` (or default trust store) missing | Folder created at startup; if that fails, an error is logged and the trust store starts empty | ### 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_MQTT_SHARED_SUBSCRIPTIONS` | Overrides `SharedSubscriptions`. `1` or `true` enables shared subscriptions; `0` or `false` disables them. See [Shared subscriptions](#shared-subscriptions). | | `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`) — `/health`, `/ready`, `/info`, `/license`, `/metrics`. See [Operational endpoints](/latest/mqtt-broker/operational-endpoints). | | `COREFLUX_OPS_PORT` | Ops listener port | | `COREFLUX_OPS_BIND_IP` | Ops bind address | | `COREFLUX_ACTIVATION_CODE` | Online license activation (`CFLX-…`). See [Licensing and Enrollment](/latest/mqtt-broker/licensing) | | `COREFLUX_ROUTE_OVERLOAD_POLICY` | Broker-wide default for connector `OVERLOAD_POLICY`. See [Store and Forward Configuration](/latest/lot-language/routes/store-and-forward). | | `COREFLUX_ROUTE_CHANNEL_FULL_TIMEOUT_MS` | Broker-wide default wait budget (ms) for `balanced` connectors | | `COREFLUX_ROUTE_CHANNEL_CAPACITY` | Shared ceiling on queued connector 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 **Docker Run** ```bash wrap theme={"theme":{"light":"css-variables","dark":"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 ``` **Docker Compose** ```yaml wrap theme={"theme":{"light":"css-variables","dark":"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":{"light":"css-variables","dark":"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":{"light":"css-variables","dark":"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":{"light":"css-variables","dark":"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":{"light":"css-variables","dark":"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":{"light":"css-variables","dark":"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":{"light":"css-variables","dark":"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 > **[Broker Commands](/latest/mqtt-broker/commands)** > > Learn how to manage the broker via MQTT commands. > **[\$SYS Topics](/latest/mqtt-broker/sys-topics)** > > 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. > [!TIP] > > **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. > [!NOTE] > > Durability is a **broker-level** setting. It works on a standalone broker and on every cluster node. In a [high-availability cluster](/latest/mqtt-broker/clustering) (beta), enable it on every node: it keeps each node's sessions and queued messages across restarts, and it lets subscriptions 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. **Enable (typical)** Opt in with the default flush policy (group commit every 50 ms): ```json wrap theme={"theme":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} { "Durability": { "Enabled": true, "FsyncPolicy": "interval", "FsyncIntervalMs": 50, "Codec": "json" } } ``` **Strongest fsync** Flush every append when you need the smallest power-loss window on a single node: ```json wrap theme={"theme":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} { "Durability": { "Enabled": true, "FsyncPolicy": "always", "Codec": "json" } } ``` **Environment variables** Tune without editing the config file (useful in containers): ```bash wrap theme={"theme":{"light":"css-variables","dark":"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; every message waits for the disk, so it suits low message rates (and is 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. *** ## Performance Durability is built so that you only pay for the state you choose to protect. | Clients | What durability does for them | Performance impact | | - | - | - | | **Without a persistent session** (MQTT 3.1.1 clean session, MQTT 5 session expiry 0) | Nothing to keep: the broker records nothing for them | None measurable | | **With a persistent session** | Records each QoS 1/2 message queued for them, and its acknowledgement, so nothing is lost if the broker restarts | Grows with the rate of messages delivered to persistent sessions | In our tests with the default flush policy (one broker, QoS 1), average round-trip latency was unchanged, and fan-out to 20 persistent subscribers kept about two-thirds of the delivery rate it reached with durability off. `FsyncPolicy: always` waits for the disk on every write and is meant for low message rates. > [!TIP] > > Give persistent sessions only to the clients that must receive messages sent while they were offline. Traffic that reaches no persistent session costs nothing extra, even with durability on. *** ## Durability and clustering Durability and clustering (beta) protect different parts of a message's path, and a cluster needs both: | Concern | What protects you | | - | - | | **A node restarts** | Durability on that node: its persistent sessions, their queued QoS 1/2 messages, and its retained messages are restored | | **A node fails after acknowledging a QoS ≥ 1 publish** | Cluster [quorum replication](/latest/mqtt-broker/clustering#delivery-guarantees): the message is already stored on a majority of nodes | | **Messages published while a node is down** | Clustering queues them for that node, and durability restores the sessions they are delivered to when it returns | | **A client reconnects to another node** | Session takeover: requires durability so the client's subscriptions follow it | Without durability, a cluster node that restarts comes back with no sessions. Messages that the other nodes kept for its persistent subscribers then have no session to go to, and are lost. See [Durability and clustering](/latest/mqtt-broker/clustering#durability-and-clustering) on the clustering page. > [!WARNING] > > **Offline queues stay on the node that hosted them.** Subscriptions follow a client that reconnects to another node (with durability enabled), but QoS 1/2 messages already queued for it are delivered only if it reconnects to the node that queued them. 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 > [!WARNING] > > **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. > [!WARNING] > > **Persistent sessions add disk work.** Every QoS 1/2 message delivered to a persistent session is written to the log when it is queued and again when it is acknowledged. Plan disk throughput for your persistent-session message rate, and use `FsyncPolicy: always` only for low message rates. See [Performance](#performance). > [!WARNING] > > **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. > [!WARNING] > > **Retained messages are kept per node.** Durability restores each node's own retained messages after a restart; it does not create a cluster-wide retained store. See [clustering limitations](/latest/mqtt-broker/clustering#limitations). *** ## Next Steps > **[High-Availability Clustering](/latest/mqtt-broker/clustering)** > > Run multiple brokers with quorum replication and session failover on top of durability. > **[Broker Configuration](/latest/mqtt-broker/configuration)** > > Ports, TLS, listeners, and other broker settings. # Licensing and Enrollment Source: https://docs.coreflux.org/latest/mqtt-broker/licensing Activate a Coreflux broker with an online code or a signed license, enroll it in an organization, and inspect tier limits ## Unlock the plan you bought A Coreflux broker always starts. With no license it runs as **Community**. A signed license — obtained online with an activation code, or installed offline as a file — is what raises the plan, artifact budget, and premium features. > [!TIP] > > **Like a signed ticket versus a door code.** An activation code is the door code: the broker exchanges it once for a signed ticket (the license) and keeps that ticket. An offline file *is* the ticket, already signed for the broker host. Either way, the broker checks the ticket itself — losing internet does not lock you out. ### When to use this * **Start a 30-day Growth trial** — register at [portal.coreflux.org](https://portal.coreflux.org), then activate the license on your broker * **Manage your fleet** of brokers in the same portal * **Activate a running broker** with a `CFLX-…` code, without restarting * **Install a signed license** on an air-gapped host using the hardware fingerprint * **Enroll** the broker into a Coreflux organization so federated logins can bind to that org * **Check usage and limits** from `$SYS/Coreflux/License` or the ops [`GET /license`](/latest/mqtt-broker/operational-endpoints#license-status) endpoint Licensing and enrollment are **separate**. A license sets the plan. Enrollment binds the broker to an organization. You can license a broker without enrolling it. *** The sections below cover the two license paths, the MQTT commands, environment variables, and how to watch status. ## Quick Start **Activate online** Paste an activation code on a running broker. Limits apply immediately — no restart. ```bash wrap theme={"theme":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} -activateLicense CFLX-XXXX-XXXX-XXXX ``` Get the code by registering at [portal.coreflux.org](https://portal.coreflux.org) (including a 30-day Growth-type license). The broker exchanges the code with the licensing authority, verifies the signed lease, and republishes `$SYS/Coreflux/License`. The code is used once and is not written to disk; the signed lease is what survives a restart. On a broker you cannot reach over MQTT, set `COREFLUX_ACTIVATION_CODE` in the **process** environment and start (or restart) the broker instead. **Install a signed file** For air-gapped hosts, print the broker host's fingerprint, send it to Coreflux, and install the signed file you get back: ```bash wrap theme={"theme":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} ./CorefluxMQTTBroker --show-fingerprint ./CorefluxMQTTBroker --load-license /path/to/license.jws ``` `--load-license` verifies the file and stages it under the data directory as `config/license.jws` before MQTT starts. On a running broker, publish `-loadLicense` with either a path **on the broker host** or the compact JWS text itself (the usual path from Coreflux HUB). **Enroll in an organization** Enrollment binds this broker to a Coreflux organization. It does not change the plan. Stage a one-shot token (`et_…`, typically valid for about 10 minutes): ```bash wrap theme={"theme":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} -enroll et_… ``` The broker exchanges the token in the background and never echoes it in the command reply. Until enrollment succeeds the broker still starts: local users work; federated logins that need an org binding are refused. ## Plans Three plans. With no license and no activation code, the broker is **Community**. The broker does **not** start a trial on its own. ### 30-day Growth trial To try Growth features, **register at [portal.coreflux.org](https://portal.coreflux.org)**. That issues a **30-day Growth-type license**. Activate it on your broker the same way as any other license (`-activateLicense` / `COREFLUX_ACTIVATION_CODE`, or a signed file). When the 30 days end, the broker reverts to Community like any other expired license. Use the same portal to **manage your fleet of brokers**. This page does not walk through portal screens — open the portal and follow the flow there. The same account is how [Coreflux HUB](/hub/account-and-fleet) lists that fleet and connects with an access token. The broker accepts the token only after enrollment has set its organization and [OIDC bearer authentication](/latest/mqtt-broker/oidc-authentication) is enabled. License activation and that MQTT check are separate: a valid license does not by itself turn token login on. > [!NOTE] > > A **Cloud Trial** on [Installation](/latest/quick-start/installation) is a hosted broker and HUB Coreflux runs for you. A Growth trial from the portal is a **license** you apply to a broker you install (Docker, package, or binary). A 30-day trial uses the **Growth** column. Paid plans keep the same limits until the license expires. | Feature | Community | Growth | Enterprise | | - | - | - | - | | Artifact budget | 24 | 100 | 1 000 | | Connectors | 3 | 5 | Unlimited | | MQTT Bridge | No | Yes | Yes | | MCP | No | Yes | Yes | | LLM / Agent | No | Yes | Yes | | Video input / output | No | Yes | Yes | | RBAC / topic access control | No | Yes | Yes | | Cluster | No | No | Yes | | Max connections (display) | 10 000 | 10 000 | 10 000 | Custom ACL rules (`-addRule`) require Growth or Enterprise. Shipped default rules always apply. See [RBAC with LoT Rules](/latest/lot-language/rules/rbac). A `TYPE LLM` connector counts toward the connector cap. A `DEFINE AGENT` needs the same Growth or Enterprise license and does not take a connector slot. License usage reports agents separately from connectors. See [AI Connectors and Agents](/latest/lot-language/routes/ai-routes). ### Artifact budget Models, Actions, and Panels each cost **1**. Python scripts cost **2**. When the budget is full, new definitions are refused with a message that names the limit. When a license expires (past expiry plus any offline grace), the broker reverts to **Community**. Existing artifacts keep running; new artifacts, advanced connectors, and new RBAC rules are blocked. *** ## How the broker picks a license On boot the broker uses the **first** source that succeeds: 1. **Inline signed token** — process env `COREFLUX_LICENSE_TOKEN` (a compact JWS) 2. **License file path** — process env `COREFLUX_LICENSE_FILE` (for a read-only mount) 3. **Working file** — `config/license.jws` in the [data directory](/latest/mqtt-broker/secrets-and-env#config-root) 4. **Online activation** — process env `COREFLUX_ACTIVATION_CODE` 5. **Community** — nothing of the above A verified token is staged to `config/license.jws` so later restarts can load it without the network. An online lease then refreshes about once a day. > [!WARNING] > > Licensing reads the **process** environment at boot (`Docker environment:`, systemd `Environment=`, Kubernetes `env:`). It does **not** read the LoT `.env` file you manage with `-setEnv` / `GET ENV`. Put `COREFLUX_ACTIVATION_CODE` in the process environment, not only in the data-directory `.env`. *** ## Commands Publish to `$SYS/Coreflux/Command` and subscribe to `$SYS/Coreflux/Command/Output`. The same verbs work from [Coreflux HUB Command Console](/hub/system/command-console) and the System Config **License** tab. See [Broker Commands](/latest/mqtt-broker/commands#license). | Command | Alias | Permission | What it does | | - | - | - | - | | `-activateLicense ` | `--activate-license` | `SystemConfiguration` | Exchanges a `CFLX-…` code for a lease and applies it immediately. No restart. | | `-loadLicense ` | `--load-license` | `CommandCall` | Verifies a signed license (compact JWS text, or a path on the broker host) and stages `config/license.jws`. | | `-refreshLicense` | `--refresh-license` | `CommandCall` | Forces one online lease refresh. Refused when the broker holds no lease. | | `-enroll ` | `--enroll` | `CommandCall` | Stages a single-use enrollment token. The exchange runs in the background. | ### Activate ```bash wrap theme={"theme":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} -activateLicense CFLX-XXXX-XXXX-XXXX -requestId license-1 ``` ```json wrap theme={"theme":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} { "success": true, "command": "ActivateLicense", "requestId": "license-1", "message": "Activated: Growth (lic_…), hard seat, expires 2027-04-09. Limits applied now — no restart needed.", "data": {} } ``` On refusal the broker `message` names the remedy (unknown code, no free seat, authority unreachable). The activation code is redacted from broker logs and the audit trail. ### Load a signed license A compact JWS is treated as the license itself. Anything else is treated as a path on the **broker** filesystem: ```bash wrap theme={"theme":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} -loadLicense /path/to/license.jws -loadLicense eyJhbGciOiJFUzI1NiIsInR5cCI6IkNGTFhMSUMi.….. ``` ```json wrap theme={"theme":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} { "success": true, "command": "LoadLicense", "message": "Signed license verified and stored at /license.jws", "data": {} } ``` HUB and other remote clients send the JWS text because they cannot write a file on the broker host. Strip wrapping whitespace before you send it — a compact JWS contains none, but files often arrive with a trailing newline. > [!NOTE] > > Brokers that predate JWS-text `-loadLicense` treat the token as a path and reply `License file not found: eyJ…`. Update the broker, or install on the host with `--load-license /path/to/license.jws`. ### Refresh an online lease The online lease refreshes on a 24-hour timer. Use `-refreshLicense` when a plan change or revocation should apply now: ```bash wrap theme={"theme":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} -refreshLicense ``` Refused when this broker holds no online lease — activate first, or install an offline file instead. ### Enroll ```bash wrap theme={"theme":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} -enroll et_… ``` The reply confirms the token was staged; it never contains the token. Treat enrollment as done when `$SYS/Coreflux/License` shows `Enrollment.Enrolled` as `true` or `Enrollment.BoundOrg` set to the organization id — Coreflux HUB shows **Enrolled** in that case. `Enrollment.Pending` going false is not enough. While enrollment is still pending, `Enrollment.Reason` names the remaining problem. #### When the platform can't be reached If egress to `brokers.coreflux.org` is blocked, the broker keeps serving. Licensing and local users are unaffected. Only federated logins wait for `BoundOrg`. Each platform call times out after 15 seconds, so startup isn't delayed. A lease enrollment with no answer retries at the next licence refresh (daily, or now with `-refreshLicense`). An `et_…` token with no answer is kept and retried with backoff (5 seconds, doubling up to every 5 minutes) while the token is valid. A new token, or pressing **Enroll** in Coreflux HUB, is tried at once. `Enrollment.Reason` says the platform couldn't be reached, and the broker logs one warning instead of an error per attempt. #### When the credential is revoked If the broker's Fleet row is deleted, the broker is disconnected, or the organization is deleted, the broker logs *Platform no longer recognises this credential* and keeps serving. If it holds an online lease, it renames the file to `credential.json.revoked` and enrolls again with the lease within seconds. The portal only accepts that for a broker waiting in Fleet, so a disconnected broker doesn't rejoin on its own. Without a lease, replace or remove the credential and supply a new `et_…` token. *** ## Startup flags These flags are processed at process start (see [Startup Options](/latest/mqtt-broker/startup-options)): | Option | Long form | Description | | - | - | - | | `-showFingerprint` | `--show-fingerprint` | Print the broker host's hardware fingerprint and exit. Send that value to Coreflux to be issued an offline license. Does not start MQTT. | | `-loadLicense` | `--load-license` | Stage a signed license file as `config/license.jws` before MQTT starts. | | `-enroll` | `--enroll` | Stage a single-use `et_…` token for the startup exchange. Also accepted on `$SYS/Coreflux/Command` while running. | *** ## Environment variables Set these on the **broker process**. Restart after changing them unless you use `-activateLicense` / `-enroll` on a live broker instead. | Variable | Role | | - | - | | `COREFLUX_ACTIVATION_CODE` | Online activation (`CFLX-…`). Also turns **enrollment** on. | | `COREFLUX_LICENSE_TOKEN` | Inline signed JWS. Wins over a file on disk. | | `COREFLUX_LICENSE_FILE` | Path to a signed `.jws` file (read-only mounts). | | `COREFLUX_FLEET_METRICS` | `off`, `false`, or `0` stops an enrolled broker sending [fleet performance metrics](#fleet-performance-metrics). Unset leaves them on. This wins over the Portal switch. | | `DO_NOT_TRACK` | `1` turns off fleet performance metrics and the anonymous install census. | A broker onboarded from Coreflux HUB needs none of these. Enrollment also turns on at boot when `config/enrollment/credential.json` exists (from `-enroll`) or `config/license.jws` holds an online lease for this instance (from `-activateLicense`). A successful `-activateLicense` turns it on immediately, without a restart. There is no `COREFLUX_ENROLLMENT_TOKEN` environment variable. Deliver the token with `--enroll` / `-enroll`, or write it to `{data directory}/config/enrollment-token`. Allow outbound HTTPS to `https://license.coreflux.org` for activation and refresh, and to `https://brokers.coreflux.org` for enrollment. *** ## Fleet performance metrics An enrolled broker sends a host description, one metrics point per minute, and up to 25 grouped error reports on the same enrollment heartbeat (every 300 seconds by default). No extra connection is opened. A broker that is not enrolled sends nothing. The Coreflux Portal charts the metrics on that broker's page. The host description includes the process start time, operating system and version, architecture, .NET version, container and orchestrator, core count, cgroup CPU quota and memory limit, and cluster role and size. It does not include a hostname, IP address, user name, or file path. Each minute includes CPU (average and peak), working set and managed heap, garbage-collection pause share and thread-pool queue peak, connected clients, connects and disconnects, messages and bytes in and out (with per-second peaks), refused CONNECTs, ACL denials, rate-limit refusals, route and cluster drops, and route, action, model, and Python errors. It also includes the session count, the retained-message count, the ten routes with the most errors. Error signatures are scrubbed before they leave the broker. Numbers become `#`. GUIDs and hex ids become ``. Anything in single or double quotes becomes `'…'`. The text is cut at 200 characters. Topics and payloads are never sent. Each signature is named by the route, action, model, or script that failed. Errors are grouped by kind, not by source, so 100 connectors with the same fault send one report. Each report lists the sources that hit it (up to 50, busiest first) with a count, and adds: * A category (`comms`, `config`, `auth`, `resource`, `script`, `internal`, or `device`), a severity, whether the broker retries on its own, and a short hint when the broker recognizes the error. * The exception type and the first broker code frame. For an action, the LoT line that failed. * For a connector, its type and name. Never its address. Transient connector errors, such as timeouts and dropped sockets, fold into one **Connection unstable** report per connector type, with the number of reconnects. It is a warning while reads still succeed, and becomes an error once a source has gone more than two report intervals without a good read. The whole error block stays under about 24 KB. The heartbeat also reports why the previous broker process stopped: `graceful`, `signal` (for example a Kubernetes rollout or Ctrl-C), `unhandled` (with the exception type), or `unknown` (killed, out of memory, or power loss), with its start time and uptime. This needs a persistent data directory. On a container without one it stays empty. While the platform is unreachable, the broker keeps up to 60 unsent minutes and sends them when the platform answers again. Older minutes are dropped. Fleet performance metrics are on by default. The Coreflux Portal has a per-broker switch. The broker learns that choice from the heartbeat response, within one interval. These settings on the broker always win over the Portal: 1. `COREFLUX_FLEET_METRICS=off` (or `false` or `0`). 2. `DO_NOT_TRACK=1`. 3. `"Enrollment": { "Metrics": { "Enabled": false } }` in the broker configuration. `COREFLUX_TELEMETRY` affects only the anonymous install census. It does not change fleet performance metrics. The counters behind these metrics are also exposed on the local [`/metrics`](/latest/mqtt-broker/operational-endpoints#metrics) endpoint, whether fleet metrics are on or off. *** ## Watch license status The broker publishes the full status **retained** on: ``` $SYS/Coreflux/License ``` The same status is served on **`GET /license`** on the [operational endpoints](/latest/mqtt-broker/operational-endpoints#license-status) listener (default port `9100`). HTTP property names are camelCase; the samples below match the retained topic. Ops HTTP is unauthenticated — do not expose that port to the public internet. Community brokers still publish the topic, so you can read `Tier`, `EffectiveLimits`, `CurrentUsage`, `Fingerprint`, and `Enrollment`. ```json wrap theme={"theme":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} { "LicensedTo": "N/A", "ExpiresUtc": null, "IsActive": false, "Tier": "Community", "Fingerprint": "…", "FingerprintSource": "machine-id", "EffectiveLimits": { "MaxArtifacts": 24, "MaxRoutes": 3, "MaxConnections": 10000 }, "CurrentUsage": { "ArtifactCount": 4, "Routes": 1 }, "Enrollment": { "Pending": false, "Reason": null, "Enrolled": false, "BoundOrg": null } } ``` An activated Growth license looks like: ```json wrap theme={"theme":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} { "LicensedTo": "Acme Corp", "ExpiresUtc": "2027-04-09T00:00:00Z", "IsActive": true, "Tier": "Growth", "Plan": "growth", "Activation": { "Mode": "online", "BindingOk": true, "Online": { "IsLease": true, "SeatMode": "hard", "LeaseExpiresUtc": "2026-09-28T00:00:00Z" } }, "Enrollment": { "Pending": false, "Reason": null, "Enrolled": false, "BoundOrg": null } } ``` Those two samples are licensed (or not) without an organization binding. A finished enrollment on a current broker looks like this: ```json wrap theme={"theme":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} { "Pending": false, "Reason": null, "Enrolled": true, "BoundOrg": "your-org-id" } ``` JSON field names still say `Routes` for connectors (`MaxRoutes`, `CurrentUsage.Routes`). `isTrial` may appear in lowercase. Older brokers omit `Enrolled` and `BoundOrg` and publish only `{ Pending, Reason }`. ### Fields operators use | Field | Meaning | | - | - | | `Tier`, `IsActive`, `Plan`, `LicensedTo` | Plan in force | | `ExpiresUtc`, `SecondsToExpiry` | Hard expiry of the license | | `Fingerprint`, `FingerprintSource` | Value to send Coreflux for an offline license | | `Activation.Mode` | `online` (lease) or `offline` (signed file) | | `Activation.BindingOk` / `BindingReason` / `TamperDetected` | Whether the broker actually honoured the license | | `Activation.Online.LeaseExpiresUtc` / `LastValidatedUtc` / `SeatMode` | Online lease freshness (`hard` = seat pool enforced, `soft` = Enterprise cluster honor-system) | | `EffectiveLimits`, `CurrentUsage`, `Features` | Quotas and gates | | `Enrollment.Pending` / `Enrollment.Reason` | A problem was recorded, and the broker's sentence for it. `Pending: false` only means no problem was recorded | | `Enrollment.Enrolled` | `true` only while this broker is bound to an organization or heartbeating with its enrollment credential. A credential file on disk alone doesn't count. This is the signal to treat enrollment as done | | `Enrollment.BoundOrg` | Organization id when the broker has one. With `Enrolled`, this is what Coreflux HUB uses for **Enrolled** | > [!WARNING] > > `Enrollment.Pending: false` does **not** mean the broker is enrolled. The broker sets `Pending` when it recorded a problem. A broker that never attempted enrollment reports the same `Pending: false` as one that finished. Treat a missing `Reason` as “no problem reported,” not as proof of an org binding. Treat enrollment as done when `Enrolled` is `true` or `BoundOrg` names the organization (Coreflux HUB shows **Enrolled**). A payload that only has `{ Pending, Reason }` does not report enrollment; HUB shows **Enrollment not reported**. ### Enrollment reasons When `Enrollment.Pending` is `true`, `Reason` is a full sentence. Typical causes: | Cause | What to do | | - | - | | No credential supplied | Set `COREFLUX_ACTIVATION_CODE`, or paste an `et_…` token in the HUB License tab / `-enroll` | | Authority unreachable | Allow egress to `https://license.coreflux.org`; the broker retries on its own | | Platform unreachable | Allow egress to `https://brokers.coreflux.org`; the broker keeps serving and retries | | Seat pool full | Wait for a killed instance's lease to expire, or manage the fleet at [portal.coreflux.org](https://portal.coreflux.org) | | Licence unclaimed | The license is not bound to an organization yet. Manage the fleet at [portal.coreflux.org](https://portal.coreflux.org) — retrying the broker will not help | | Instance mismatch | Expected: a lease cannot move between hosts | *** ## Offline fingerprint Send Coreflux the value from `--show-fingerprint`, or the `Fingerprint` field on the status payload. That value is what the offline license is bound to. A container that cannot read a stable machine id gets a fingerprint that can change when the container is recreated, which drops the broker back to Community. Request an offline license for that container only when that is acceptable. *** ## Persist the data volume The license, install identity, and enrollment credential live under `{data directory}/config/`. Official images use `CONFIG_PATH=/etc/project`. | Path (under the data directory) | Why it must persist | | - | - | | `config/license.jws` | Active license or online lease | | `config/instance-id` | Install identity — one volume is one seat. Restarts reclaim the same seat instead of spending a new one | | `config/enrollment-token` | Drop-file for a one-shot `et_…` token (`--enroll` writes here; the broker deletes it after a successful exchange when the path is writable) | | `config/enrollment/` | Persisted org credential after a successful enroll | > [!WARNING] > > Never copy `config/instance-id` between hosts, and never bake `config/` into an image after first boot. Every broker launched from that image then claims the **same** seat. Build images before first boot, or delete `config/instance-id` and `config/enrollment/` before snapshotting. Online-activated brokers with `COREFLUX_ACTIVATION_CODE` and an **empty** volume mint a new install identity and consume a new seat. Restoring the old volume is cheaper than re-activating. If the volume is gone, manage the fleet at [portal.coreflux.org](https://portal.coreflux.org) before activating on a new volume. Enrolled brokers need `config/enrollment/` on the same volume. An enrollment token is single-use: if the credential is on an emptyDir and the pod restarts, the next exchange fails and will not self-heal. ```yaml wrap theme={"theme":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} volumeMounts: - name: broker-data mountPath: /etc/project volumes: - name: broker-data persistentVolumeClaim: claimName: coreflux-data ``` ```bash wrap theme={"theme":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} docker run --pull=always -d --name coreflux_broker \ -p 1883:1883 -p 5000:5000 -p 9100:9100 \ -e COREFLUX_ACTIVATION_CODE=CFLX-XXXX-XXXX-XXXX \ -v project-volume:/etc/project \ coreflux/coreflux-mqtt-broker:latest ``` *** ## Error messages | Situation | Typical message | | - | - | | Artifact budget full | `Artifact budget exceeded. Max N artifact(s) allowed on this tier.` | | Connector limit reached | `Tier limit exceeded: Maximum allowed routes are N.` | | Advanced connector, wrong tier | `Route type 'X' requires a Growth or Enterprise license.` | | RBAC rule, wrong tier | `RBAC / Topic Access Control rules require a Growth or Enterprise license.` | | Bad signed file | `Signed license failed verification (bad signature, wrong product, or not yet valid).` | | Not a JWS and not a file | `Not a signed license file (expected a compact JWS).` | *** ## Best practices > **Prefer an activation code when the broker can reach the network** > > Online activation refreshes the lease, picks up plan changes, and releases the seat on a graceful shutdown. Get a code (including a 30-day Growth-type license) at [portal.coreflux.org](https://portal.coreflux.org). Keep `COREFLUX_ACTIVATION_CODE` in the process environment (or activate once with `-activateLicense` and persist `config/license.jws`). > **Use a signed file only when the host is air-gapped** > > Offline licenses are hardware-bound. Collect `--show-fingerprint` from the real host — not from a laptop you will not deploy. > **Keep config/ on the same volume across upgrades** > > Replacing the image while keeping `/etc/project` (or `/var/lib/coreflux`) preserves the license, the seat, and enrollment. > **Do not publish the ops port** > > `GET /license` on the [ops listener](/latest/mqtt-broker/operational-endpoints#license-status) is unauthenticated. Bind ops to localhost or a private network. The same status is on `$SYS/Coreflux/License` for MQTT clients that already authenticate. > [!WARNING] > > Losing connectivity never bricks a licensed broker. The signed license is checked locally. An online lease keeps running until its expiry plus grace; if refresh later sees a revocation, the broker reverts to Community. *** ## Next Steps > **[License tab in Coreflux HUB](/hub/system/configuration#license)** > > Activate with a code or signed file, refresh a lease, and enroll from System Config. > **[Broker Commands](/latest/mqtt-broker/commands#license)** > > Publish `-activateLicense`, `-loadLicense`, `-refreshLicense`, and `-enroll` over MQTT. # MCP Server Source: https://docs.coreflux.org/latest/mqtt-broker/mcp-server Expose the Coreflux broker as an MCP server so AI clients can run broker commands and LoT actions over Streamable HTTP ## Let AI Clients Operate the Broker The Coreflux broker can listen as an **MCP (Model Context Protocol) server**. An AI client — Cursor, Claude Desktop, or any MCP-capable assistant — connects over HTTP, presents a broker-issued token, and calls tools that map to broker commands and your LoT (Language of Things) Actions. You stay in the editor; the assistant deploys Actions, lists connectors, and inspects topics **as a named broker user**, with that user's RBAC. This is a **broker configuration feature** (`Mcp.Enabled`), not a LoT connector. There is no `DEFINE CONNECTOR` for it, and `DEFINE ROUTE` is only the alias of that keyword. `TYPE MCP_SERVER` was removed. > [!TIP] > > **Like giving an assistant a badge for the control room.** The badge (a bearer token) is minted for a specific operator. Every tool call is checked against that operator's permissions — not against a shared admin back door. ### When to use this * You want Cursor or Claude Desktop to **deploy LoT, inspect topics, and run broker commands** against a live broker * You need a **least-privilege identity** for the assistant (not `root`) so RBAC still applies * You are **migrating off** a saved `TYPE MCP_SERVER` connector or an old `/sse` client URL ### Three different MCP surfaces Coreflux uses MCP in three places. They are not interchangeable: | Surface | Direction | What it is | Docs | | - | - | - | - | | **MCP Server** | AI client → this broker | The broker's built-in MCP server. Config (`Mcp.Enabled`), Streamable HTTP at `/mcp`, bearer token. | This page | | **Documentation MCP** | AI client → docs.coreflux.org | Hosted docs search for assistants. Does **not** control a broker. | [Documentation MCP](/ai/mcp) | | **MCP connector** | Broker → an external MCP server | LoT `DEFINE CONNECTOR … WITH TYPE MCP` and `CALL MCP` from Actions. | [MCP Connectors](/latest/lot-language/routes/mcp-routes) | The Coreflux HUB AI assistant does **not** talk to this MCP server. HUB is a control plane for enabling it and minting tokens — not an MCP client of the broker. *** Enable the listener, mint a token for a real broker user, point the client at `/mcp`, then verify with `/health` and `$SYS/Coreflux/MCP/Info`. ## Quick Start The listener starts only when `Mcp.Enabled` is on **and the broker has restarted**. Default bind is loopback on the broker host: `127.0.0.1:6278` at path `/mcp`. In a container, that's the container's own loopback. See [Docker and Kubernetes](#docker-and-kubernetes). **Broker config** Add an `Mcp` block to the broker configuration, then restart: ```json wrap theme={"theme":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} { "Mcp": { "Enabled": true, "Port": 6278, "BindIP": "127.0.0.1", "Endpoint": "/mcp" } } ``` Mint a token for an existing broker user. The plaintext is returned **once** in `data.token`: ```bash wrap theme={"theme":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} -createMcpToken operator cursor-local ``` Point the AI client at `http://127.0.0.1:6278/mcp` with `Authorization: Bearer `. **Environment variables** Override the file without editing JSON (useful in containers). Restart the broker after setting them: ```bash wrap theme={"theme":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} export COREFLUX_MCP_ENABLED=true export COREFLUX_MCP_PORT=6278 export COREFLUX_MCP_BIND_IP=127.0.0.1 export COREFLUX_MCP_ENDPOINT=/mcp ``` Then mint a token the same way (`-createMcpToken `) and put it in the client's MCP config. **Coreflux HUB** In **System → System Configuration**, open the **MCP** tab. Turn **Enabled** on, save, and **restart the broker**. Status stays **Pending restart** until the new `Mcp` block is live. Click **Create token**, pick a broker user, give the token a name with no spaces, and copy the value immediately. The dialog offers Cursor, Claude Code, and Claude Desktop snippets with `"type": "http"` and the bearer header already filled in. Older brokers with no `Mcp` key in `$SYS/Coreflux/Config` do not have this tab's editor — update the broker first. See [System Configuration](/hub/system/configuration#mcp). > [!TIP] > > Probe the unauthenticated health path on the MCP port (this is **not** the ops `/health` on port 9100): > > ```bash wrap theme={"theme":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} > curl http://127.0.0.1:6278/health > ``` > > A healthy listener returns `status: "healthy"` and a `tools_count`. Then subscribe in MQTT Explorer to `$SYS/Coreflux/MCP/Info` — the retained payload includes `http_endpoint` and `status`. *** ## Endpoints The MCP listener is a **separate HTTP port** from MQTT and from the [operational endpoints](/latest/mqtt-broker/operational-endpoints) (`/health`, `/ready`, `/info`, `/metrics` on port `9100`). | Path | Auth | Purpose | | - | - | - | | `{Endpoint}` (default `/mcp`) | Bearer token | Streamable HTTP JSON-RPC. Every MCP tool call. | | `/health` | None | Liveness probe. Returns `status`, `server`, `version`, `tools_count`. | | `/info` | Bearer token | Compact server name, version, transport, path, and advertised tool names. | Default URL on the broker host: ```text wrap theme={"theme":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} http://127.0.0.1:6278/mcp ``` The server is **HTTP-only**: * Transport is **stateless Streamable HTTP**. There is no stdio transport. * `/sse` and `/message` are **rejected**. There is no SSE compatibility mode. * Native TLS for this listener is **not** in this release. Bind loopback, or terminate TLS in front of a non-loopback bind. Authenticated requests send: ```http wrap theme={"theme":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} Authorization: Bearer ``` Anonymous MQTT login is **not** inherited. A client with no token gets `401` on `/mcp` and `/info`. The listener still stays up if you enabled it with zero tokens — every request is refused until you mint one. > [!WARNING] > > A bind outside loopback (`0.0.0.0` or a LAN address) sends bearer tokens in the clear. Put TLS in front (a reverse proxy) before exposing the MCP port beyond the broker host. *** ## Docker and Kubernetes The MCP server has its own HTTP listener. It isn't a path on the MQTT port (`1883`) or the WebSocket port (`5000`). A client URL such as `https://broker.example.com/mcp` reaches it only when the proxy in front of that host sends `/mcp` to port `6278`. A proxy that sends every path to port `5000` delivers the request to the WebSocket listener instead. Publishing port `6278` isn't enough on its own. Inside a container, `127.0.0.1` is the container's own loopback, so a Docker port mapping or a Kubernetes Service can't reach a listener bound there. The administrator who deploys the broker sets all of these: | Step | Setting | Why | | - | - | - | | Enable | `Mcp.Enabled` or `COREFLUX_MCP_ENABLED=true` | The listener is off by default. Restart the broker after the change. | | Bind | `Mcp.BindIP` or `COREFLUX_MCP_BIND_IP` set to `0.0.0.0` | The default `127.0.0.1` can't be reached from a published port or from the pod network. | | Publish | A Docker port mapping, or a container port and Service port `6278` | The broker image and the sample Compose file don't publish `6278`. | | Route | A proxy or Ingress path `/mcp` to port `6278` | Send only this path to the MCP port. Keep `/` on the WebSocket port. | | Allow | A firewall or NetworkPolicy rule for `6278`, from the proxy only | A policy that allows only `1883` and `5000` drops MCP traffic. | | TLS | Terminate TLS on the proxy | The listener speaks plain HTTP, and bearer tokens must not cross the network in the clear. | | Token | `-createMcpToken`, then `Authorization: Bearer ` on the client | `/mcp` doesn't accept anonymous MQTT login. | If you changed `Port` or `Endpoint`, use those values instead of `6278` and `/mcp`. **Docker Compose** Add the MCP port and the two variables to the broker service: ```yaml wrap theme={"theme":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} services: broker: image: coreflux/coreflux-mqtt-broker:latest ports: - "1883:1883" - "5000:5000" - "9100:9100" - "127.0.0.1:6278:6278" # MCP, reached only through the TLS proxy environment: - COREFLUX_MCP_ENABLED=true - COREFLUX_MCP_BIND_IP=0.0.0.0 ``` The `127.0.0.1:` prefix keeps the port off the host's public interfaces, so only a proxy on the same host reaches it. A proxy in the same Compose network can use `broker:6278` and needs no published port. **Kubernetes** Name the MCP port on the broker container and the Service, then add an Ingress path for `/mcp` next to the WebSocket path: ```yaml wrap theme={"theme":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} # Broker container ports: - name: mcp containerPort: 6278 env: - name: COREFLUX_MCP_ENABLED value: "true" - name: COREFLUX_MCP_BIND_IP value: "0.0.0.0" # Service ports: - name: mcp port: 6278 targetPort: mcp # Ingress rule on the public host, next to the "/" path to port 5000 paths: - path: /mcp pathType: Prefix backend: service: name: coreflux-broker port: number: 6278 ``` Let the Ingress terminate TLS. If a NetworkPolicy guards the broker, allow TCP `6278` from the ingress controller's namespace. `/health` and `/info` are on the same listener. Route them to `6278` too if operators should reach them through the public host. `/info` needs a token. The AI client needs only `/mcp`. ### Check the route From outside the cluster or host, call `/mcp` without a token: ```bash wrap theme={"theme":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} curl -i https://broker.example.com/mcp ``` The MCP listener answers `401` with `WWW-Authenticate: Bearer` and asks for a token from `-createMcpToken`. Any other answer, such as a `404` with an empty body, means the request reached a different listener, usually the WebSocket port. Then point the client at the same URL with its bearer token. *** ## Configuration reference Environment variables override the JSON block. Changes apply on **restart**. ```json wrap theme={"theme":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} { "Mcp": { "Enabled": true, "Port": 6278, "BindIP": "127.0.0.1", "Endpoint": "/mcp", "EnableAllTools": true, "EnabledTools": [], "TokenLifetimeDays": 0 } } ``` | Setting | Required | Default | Description | | - | - | - | - | | `Enabled` | Yes, to start | `false` | Master switch. Applied at startup. | | `Port` | No | `6278` | Listening port for Streamable HTTP. | | `BindIP` | No | `127.0.0.1` | Bind address. Non-loopback needs TLS in front. | | `Endpoint` | No | `/mcp` | Streamable HTTP path. Must be an absolute path. `/sse` and `/message` are rejected. | | `ServerName` | No | `CorefluxBroker` | MCP protocol server name (HUB: **Advanced**). | | `ServerVersion` | No | Broker version | Sent to the client during discovery. | | `EnableAllTools` | No | `true` | When `true`, advertise the full MCP surface (registry tools, handwritten tools, capability tools, and `action_*`). When `false`, only names in `EnabledTools`. | | `EnabledTools` | When `EnableAllTools` is `false` | `[]` | Allow-list of **advertised MCP names** (`add_route`, `get_route_template`). Command tokens such as `-addConnector` and `-addRoute` are also accepted. An empty list advertises nothing. | | `TokenLifetimeDays` | No | `0` | Default lifetime for newly minted tokens, in days. `0` means no expiry. Per-token `ttlDays` on `-createMcpToken` overrides this. | ### Environment variables | Variable | Config key | | - | - | | `COREFLUX_MCP_ENABLED` | `Enabled` (`1` or `true`) | | `COREFLUX_MCP_PORT` | `Port` | | `COREFLUX_MCP_BIND_IP` | `BindIP` | | `COREFLUX_MCP_ENDPOINT` | `Endpoint` | | `COREFLUX_MCP_ENABLE_ALL_TOOLS` | `EnableAllTools` | | `COREFLUX_MCP_ENABLED_TOOLS` | `EnabledTools` (comma- or semicolon-separated) | | `COREFLUX_MCP_TOKEN_LIFETIME_DAYS` | `TokenLifetimeDays` | HUB **Advanced** also exposes `ServerName` and `ServerVersion`. Restrict the tool list from HUB with **Expose all tools** off and the allow-list picker — write advertised names (`get_route_template`), not internal command tokens (`route_code`). Add `action_*` names with **Custom tool**. *** ## Bearer tokens Every MCP HTTP request except `GET /health` requires a broker-issued bearer token. Tool calls run as **the broker user the token was minted for** and are checked against that user's RBAC. These three commands are operator-only. They are **not** advertised as MCP tools. | Command | Permission | What it does | | - | - | - | | `-createMcpToken [ttlDays]` | `McpTokenManagement` | Mint a token. Plaintext returned **once** in `data.token`. | | `-listMcpTokens [username]` | `McpTokenManagement` | Metadata only: `id`, `name`, `userName`, `created`, `expires`, `lastUsed`, `state`. Never the secret or its hash. | | `-revokeMcpToken ` | `McpTokenManagement` | Immediate refuse for clients using that token. | The username must already exist. Removing a broker user also deletes that user's tokens. Token names must not contain whitespace (the command parser splits on spaces). The locked built-in `AllowMcpTokenManagement` admits `root`, `AllowedUserManagement`, and `AllowedSystemConfiguration` — the same flags as other user-management gates. You cannot override that gate with a custom rule; set permission flags instead. See [Operation scopes](/latest/lot-language/rules/syntax#operation-scopes). ### Create a token Publish to `$SYS/Coreflux/Command` (or run it from [Command Console](/hub/system/command-console) / the broker console): ```bash wrap theme={"theme":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} -createMcpToken operator cursor-local ``` Optional third argument is lifetime in days (`0` = no expiry), overriding `Mcp.TokenLifetimeDays`. Success envelope (secret redacted here): ```json wrap theme={"theme":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} { "success": true, "command": "CreateMcpToken", "entityType": "McpToken", "entityName": "cursor-local", "data": { "token": "cfx_mcp_…", "id": "…", "name": "cursor-local", "userName": "operator", "expires": "never" } } ``` Store `data.token` immediately. `-listMcpTokens` will never show it again. > [!WARNING] > > MQTT command replies always go to `$SYS/Coreflux/Command/Output`. The default `AllowSubscribeCommandOutput` audience is **wider** than `McpTokenManagement`, so a user who cannot mint tokens may still read a mint reply. To keep the plaintext off that topic, mint from the broker console or from the HUB MCP tab (HUB keeps the value in the create dialog only). If a token may have been observed, `-revokeMcpToken` and mint again. Mint for a **least-privilege** account. Do not mint for `root` unless the client should have full operator access. *** ## Point a client at the broker Put the same JSON shape in the file your client actually reads. `"type": "http"` is optional in Cursor (it infers HTTP from `url`) but **required** in Claude Code and Claude Desktop — without it they skip the entry with no error. **Cursor** Project file: `.cursor/mcp.json` (filename is `mcp.json`, not `.mcp.json`). Global fallback: `~/.cursor/mcp.json`. ```json wrap theme={"theme":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} { "mcpServers": { "coreflux-broker": { "type": "http", "url": "http://127.0.0.1:6278/mcp", "headers": { "Authorization": "Bearer " } } } } ``` **Claude Code** Project file: `.mcp.json` at the **repository root** (not under `.claude/`). ```json wrap theme={"theme":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} { "mcpServers": { "coreflux-broker": { "type": "http", "url": "http://127.0.0.1:6278/mcp", "headers": { "Authorization": "Bearer " } } } } ``` **Claude Desktop** OS app config (`claude_desktop_config.json`), not a file in the repo. `"type": "http"` is required. ```json wrap theme={"theme":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} { "mcpServers": { "coreflux-broker": { "type": "http", "url": "http://127.0.0.1:6278/mcp", "headers": { "Authorization": "Bearer " } } } } ``` Do not commit a minted token. After changing the file, start a new Claude Code session. In Cursor, if the server shows resources but **0 tools**, remove the entry, save, and add it back (or quit Cursor) — an empty `tools/list` result can stay cached in memory; reloading the server is not enough. When the broker binds `0.0.0.0`, `$SYS/Coreflux/MCP/Info` still prints that bind in `http_endpoint`. HUB substitutes the host it is connected to when offering snippets; a loopback bind is copied as written. *** ## What the tools do A connected client calls `tools/list` on `/mcp`. The exact set is whatever this broker advertises (`/health` `tools_count` and `$SYS/Coreflux/MCP/Tools`). It is not a second copy of `$SYS/Coreflux/Command`. Typical surface when `EnableAllTools` is `true`: | Kind | Examples | Notes | | - | - | - | | Broker command wrappers | `add_action`, `list_routes`, `load_project` | Most management verbs. Each call runs as the token's user. | | Things | `add_thing`, `remove_thing`, `list_things` | Wrap `-addThing`, `-removeThing`, and `-listThings`. `add_thing` takes one or more `DEFINE THING` blocks; a leading `--dryRun` returns the plan and changes nothing. | | Disable and enable | `disable_action`, `enable_action`, `disable_route`, `enable_route`, `disable_agent`, `enable_agent` | Switch an entity off or on without deleting it. See [Disable and enable](/latest/mqtt-broker/commands#disable-and-enable). | | Discovery helpers | `get_broker_info`, `get_available_commands`, `list_action_tools` | Handwritten; not MQTT command names. | | Keep-list names | `get_route_template`, `list_route_templates`, `get_panel_code` | Use these in `EnabledTools`, not `route_code` / `list_templates` / `panel_code`. | | Capability tools | `explore_topics`, `diagnose`, `provision_data_source`, `deploy_lot` | An allow-list must name them **exactly**. `explore` does not enable `explore_topics`. | | LoT Actions | `action_{name}` | Registered when the MCP server **starts**. Restart the broker after deploying new Actions if they should appear as tools. | Token, license, and password commands are **never** advertised (`-createMcpToken`, `-listMcpTokens`, `-revokeMcpToken`, `-changeUserPassword`, `-activateLicense`, `-loadLicense`, `-refreshLicense`, `-enroll`). User-management tools (`add_user`, `remove_user`, `change_user_settings`, `add_user_to_group`, `remove_user_from_group`) **are** on the default surface. `add_user` takes a password in its arguments — chat transcripts will see it. Mint the token for a user without user-management permission, or turn `EnableAllTools` off and omit those names. List-shaped tools default to a concise page of 50 items (`items`, `total`, `next_cursor`). Pass `response_format=detailed` for full objects. > **Capability tools** > > | Tool | Purpose | > | - | - | > | `explore_topics` | Paginated MQTT topic listing with JSON schema inference. Does not wait for live publishes. | > | `diagnose` | Connection status, Action errors, and pending status. Takes **no** arguments. | > | `provision_data_source` | Add an OT or database connector from LoT (`route_type`, `name`; optional `definition`). | > | `deploy_lot` | Deploy `DEFINE ACTION` / `MODEL` / `CONNECTOR` (or `ROUTE`) / `AGENT` / `RULE` / `THING` blocks in one call. Agent blocks go through `-addAgent`, and consecutive `DEFINE THING` blocks are sent as one `-addThing`. On first failure it rolls back entities **this call** created or overwrote. | > > `lot_diagnostic` is a **process-wide** tracing toggle (`true` / `false`), not a per-entity report. Use `diagnose` for health and Action error topics for compile failures. > **Resources and prompts** > > Clients can call `resources/list` and `resources/read`. There is no `resources/subscribe` (the server is stateless). Unknown URIs return JSON-RPC `-32602` (Invalid params). > > | URI | Contents | > | - | - | > | `coreflux://topics` | First concise page of the topic tree | > | `coreflux://topics/{topic}` | One topic snapshot (`/` in the topic is URI-escaped as `%2F`) | > | `coreflux://lot/{kind}` | LoT inventory (`actions`, `models`, `routes`, `agents`, `rules`, `things`) | > | `coreflux://lot/{kind}/{name}` | One entity definition | > | `coreflux://skills` | Agent skill catalogue | > | `coreflux://skills/{name}` | Full skill body | > > To install the public skill set straight into a coding agent instead, see [Agent Skills](/ai/agent-skills). > > Reads require `CommandCall` on the token's user. Topic URIs are filtered again by that user's subscribe rules (`Subscribe` / `SubscribeSys`). > > `prompts/list` advertises `lot_authoring` (optional `entity_type`: `action`, `model`, `route`, `rule`). *** ## System topics When the MCP host starts or stops, it republishes two **retained** topics. Neither payload includes token values, hashes, or the usernames tokens map to. ### `$SYS/Coreflux/MCP/Info` The live payload lists every advertised tool name. The sample below is abbreviated. ```json wrap theme={"theme":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} { "server_name": "CorefluxBroker", "server_version": "2.22.0", "transport_type": "http", "http_endpoint": "http://127.0.0.1:6278/mcp", "http_port": 6278, "bind_ip": "127.0.0.1", "total_tools": 2, "tools": ["add_action", "list_routes"], "tokens_active": 1, "auth": "bearer", "status": "running", "connection_info": "Connect via Streamable HTTP at http://127.0.0.1:6278/mcp with an Authorization: Bearer header.", "timestamp": "2026-09-21T10:00:00.000Z" } ``` `status` is `running` or `stopped`. A clean shutdown republishes `stopped` so a stale retain does not claim the server is up. ### `$SYS/Coreflux/MCP/Tools` An array of `{ "name": "…" }` objects — currently advertised MCP names only, not the full possible catalogue. When `EnableAllTools` is false and `EnabledTools` is empty, this topic is an empty list. > [!NOTE] > > These topics are `$SYS`. Subscribe with an identity that is allowed to read system topics (MQTT Explorer as an admin user). HUB maps `status: "running"` to **Listening**. If Info is missing after a saved, applied `Mcp.Enabled: true`, HUB still treats the server as Listening rather than as another pending restart. The ops HTTP `/info` on port **9100** (separate from MCP `/info` on **6278**) includes a compact `mcp` object when the feature is enabled (`enabled`, `endpoint`, `bind`, `port`, `toolsCount`, `tokensActive`, `auth`) — still no token secrets. See [Operational endpoints](/latest/mqtt-broker/operational-endpoints#cluster-and-mcp-summaries). *** ## Moving off `TYPE MCP_SERVER` and SSE A saved project that still carries `DEFINE CONNECTOR … WITH TYPE MCP_SERVER` or `DEFINE ROUTE … WITH TYPE MCP_SERVER` **fails to load** with a message that the type was removed. The MCP *client* connector (`TYPE MCP`, `ADD MCP_CONFIG`, `CALL MCP`) is unchanged. **Action:** 1. Remove the `TYPE MCP_SERVER` definition from the project. 2. Enable `Mcp.Enabled` (or `COREFLUX_MCP_ENABLED=1`) and restart the broker. 3. Mint a token: `-createMcpToken `. 4. Point the client at `http://127.0.0.1:6278/mcp` (or your bind, port, and endpoint) with `Authorization: Bearer `. | Before (broken) | After | | - | - | | URL `http://127.0.0.1:6278/sse` | URL `http://127.0.0.1:6278/mcp` | | Separate `/message` URL | Same `/mcp` path for every JSON-RPC POST | | `WITH TRANSPORT_TYPE "sse"` on `TYPE MCP_SERVER` | `Mcp.Enabled`; transport is HTTP-only | | `WITH SSE_ENDPOINT` / `WITH MESSAGE_ENDPOINT` | `Mcp.Endpoint` (default `/mcp`) | There is no compatibility mode. Every existing `/sse` client configuration stops working. *** ## Best practices > **Least-privilege tokens** > > Mint a dedicated broker user for the assistant. Leave user-management and `AllowedSystemConfiguration` off unless the client must administer the broker. Prefer `EnableAllTools: false` plus an allow-list of the tools that job needs. > **Loopback first** > > Keep `BindIP` at `127.0.0.1` when the AI client runs on the same host as the broker. Only bind a LAN address when a remote client must connect, and terminate TLS in front. In a container, bind `0.0.0.0` and reach the port only through the TLS proxy. See [Docker and Kubernetes](#docker-and-kubernetes). > **Treat mint replies as secrets** > > Prefer the HUB create-token dialog or the broker console over MQTT when other administrators share `$SYS/Coreflux/Command/Output`. Revoke and re-mint if a token may have leaked into a chat log. > **Do not confuse health ports** > > MCP `/health` is on the MCP port (default `6278`). Orchestrator probes for the broker process still use ops `/health` on port `9100`. See [Operational endpoints](/latest/mqtt-broker/operational-endpoints). > [!WARNING] > > The MCP server does not speak OAuth 2.0 / OIDC. Identity is the minted bearer token only. An [OIDC access token](/latest/mqtt-broker/oidc-authentication) used as an MQTT password is a different credential and is not accepted on `/mcp`. Don't publish the MCP port directly on the internet. Reach it only through a proxy that terminates TLS. *** ## Next Steps > **[MCP Connectors](/latest/lot-language/routes/mcp-routes)** > > Connect the broker as a client to external MCP servers and call their tools from LoT Actions. > **[System Configuration](/hub/system/configuration#mcp)** > > Enable the listener and mint tokens from the Coreflux HUB MCP tab. # OIDC and Bearer Authentication Source: https://docs.coreflux.org/latest/mqtt-broker/oidc-authentication Let MQTT clients connect with an OIDC access token as the password, and authorize them with the same LoT rules as local users MQTT clients can sign in with an access token from an OIDC identity provider instead of a password stored on the broker. The broker checks the token, then applies the same LoT (Language of Things) rules it uses for a local user. > [!TIP] > > **Like a badge that expires.** The client shows a badge (the access token) at the door. The broker checks the badge with the issuer, lets the person in as the identity printed on it, and asks them to leave when the badge expires. ## When to use token authentication Use it when people and services already have identities outside the broker: * **Coreflux accounts** — Operators sign in to [Coreflux HUB](/hub/account-and-fleet) with their organization account and connect to a broker in the fleet. The HUB presents that account's access token. The broker accepts it only after [enrollment](/latest/mqtt-broker/licensing) has bound the broker to the same organization. * **A provider you run** — Point `Issuers` at an identity provider that publishes a discovery document over HTTPS and signs access tokens with RS256. Leave it off when every client uses a username and password in the broker's user store. It is **off by default**. Local users keep working. A CONNECT whose password is not a token is still checked against the user store. *** Token checks run at MQTT CONNECT. Enabling them is a broker configuration change, and the broker reads that configuration at startup. ## Quick Start **Coreflux organization** A broker enrolled in a Coreflux organization accepts account tokens for that organization. Enrollment fills `BoundOrg`; until that succeeds, token logins are refused and local users still connect. ```json wrap theme={"theme":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} { "AnonymousLogin": false, "Oidc": { "Enabled": true, "Issuers": ["https://accounts.example.com"], "ClientIdAllowlist": ["hub-desktop"], "RequiredScope": "coreflux-broker/connect" } } ``` Replace `Issuers` with the issuer URL of the account tokens you accept. List every `client_id` exactly — `hub-desktop` is the id the broker's account-token contract uses for the desktop HUB. There is no wildcard. With enrollment turned on and `BoundOrg` still empty, the broker starts and refuses token logins until enrollment writes the organization id. With enrollment off, set `BoundOrg` yourself or the broker will not start. Restart after editing the file. **Your own identity provider** When the issuer serves only this broker's users, declare it in `DedicatedIssuers`. `BoundOrg` can stay empty, and group names on the token are used as written. ```json wrap theme={"theme":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} { "AnonymousLogin": false, "Oidc": { "Enabled": true, "Issuers": ["https://idp.example.com"], "DedicatedIssuers": ["https://idp.example.com"], "ClientIdAllowlist": ["plant-mqtt"], "RequiredScope": "", "RequiredClaimsVersion": "" } } ``` The token must still be an **access** token (`token_use` = `access`), signed with RS256, and its `client_id` claim must be in the allowlist. Empty `RequiredScope` and `RequiredClaimsVersion` skip those checks. Group names are read from `coreflux:groups` as a space-separated string. On a dedicated issuer, a name with no organization prefix is kept as-is. Restart the broker after editing the file. > [!NOTE] > > There is no `COREFLUX_OIDC_*` environment variable on the broker. These fields live in the broker configuration file (the same JSON as [ports and TLS](/latest/mqtt-broker/configuration)). `BoundOrg` is the exception: a successful [enrollment](/latest/mqtt-broker/licensing#quick-start) writes the organization id into it. ## What the client sends On CONNECT, put the access token in the **password**. An optional `Bearer ` prefix is accepted. The broker does not read the CONNECT username as the identity — the identity is the token's subject claim (`sub` unless you change `UsernameClaim`). | CONNECT field | What to send | | - | - | | Password | The access token, or `Bearer ` followed by the token | | Username | Any value your client requires. It is not the broker username | A password that is not shaped like a token (three non-empty segments separated by `.`) stays on the local user-store path. The Coreflux HUB does this when you **Connect** on a fleet row. See [Account, fleet, and Cloud mode](/hub/account-and-fleet). Use MQTT over TLS (`mqtts://` or `wss://`) whenever the token leaves the broker host. OIDC does not encrypt the MQTT connection. A plaintext listener carries the access token in the CONNECT password. Mutual TLS is a separate check — it does not replace the token, and the token does not replace it. See [TLS Server Certificate](/latest/mqtt-broker/configuration#tls-server-certificate). ## How the broker decides When `Oidc.Enabled` is true and the password looks like a token: 1. The broker loads the issuer's discovery document over HTTPS and checks the RS256 signature, the issuer, and the expiry. 2. It requires `token_use` = `access`. An ID token is refused. 3. It requires the `client_id` claim to be in `ClientIdAllowlist`. 4. If `RequiredScope` is set, that exact scope must appear in the token's space-separated `scope` list. Coreflux account tokens from the HUB request `coreflux-broker/connect`. 5. If `RequiredClaimsVersion` is set (default `1`), the `coreflux:claims_version` claim must match it. 6. If `BoundOrg` is set, that organization id must appear in the space-separated `coreflux:orgs` claim. An empty `BoundOrg` refuses the token unless the issuer is listed in `DedicatedIssuers`. 7. Groups and roles are read from `coreflux:groups` and `coreflux:role`. On a bound broker they must be prefixed with the organization id (`org_…:engineers`). The broker strips that prefix and authorizes the plain name. A discovery or key outage **refuses** the connection. It does not fall through to anonymous access. ### Identity and rules The session is not written to the local user store. HUB User Management and `-listUsers` still show local accounts only. | What a rule sees | Source | | - | - | | `USER IS "…"` | `UsernameClaim`, default `sub` | | `USER IN GROUP "engineers"` | De-qualified name from `coreflux:groups` for `BoundOrg` | | `USER HAS AllowedSystemConfiguration` | True only when `AdminRoles` or `AdminGroups` matches | The display name (`preferred_username` by default) is written to login logs. Rules never match it. A rule written against an email address does not match a subject id. ```lot wrap theme={"theme":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} DEFINE RULE EngineersPublish WITH PRIORITY 200 FOR Publish TO TOPIC "plant/status/#" IF USER IN GROUP "engineers" THEN ALLOW ELSE DENY ``` Custom rules like this one require a Growth or Enterprise license. On Community, set `AdminRoles` or `AdminGroups` instead: a matching token receives `AllowedSystemConfiguration`, `AllowedUserManagement`, and `AllowedLogManagement`, which the shipped rules already allow. Both lists are empty by default, so a Coreflux account is **not** an administrator until you name the role or group. Use the plain name (`owner`), not `org_…:owner`. Group and role changes apply on the next token. The live session keeps the groups it connected with until it is disconnected. See [RBAC with LoT Rules](/latest/lot-language/rules/rbac#federated-accounts). ### Token lifetime `DisconnectOnExpiry` is on by default. When the access token expires, the broker waits `ExpiryGraceSeconds` (default 30) and then disconnects the client. On MQTT 5 the disconnect reason is `NotAuthorized`. The client connects again with a fresh token. That window is how a revoked account loses MQTT access: there is no separate revocation list. Signature checks allow `ClockSkewSeconds` of clock difference (default 60). ## Configuration reference All fields sit under `Oidc` in the broker configuration file. Restart the broker after a change. Turning `Enabled` on forces `AnonymousLogin` off, even if the file still says `true`. | Field | Default | Required when enabled | Purpose | | - | - | - | - | | `Enabled` | `false` | — | Turn token authentication on | | `Issuers` | empty | Exactly one HTTPS issuer URL | Discovery and signature keys. More than one issuer refuses to start | | `DedicatedIssuers` | empty | No | Issuers that serve a single organization. Only these may run with an empty `BoundOrg` | | `ClientIdAllowlist` | empty | Yes, at least one id | Exact `client_id` values. Empty refuses to start | | `RequiredScope` | empty | No | Exact scope string. Empty skips the check | | `RequiredClaimsVersion` | `1` | No | Expected `coreflux:claims_version`. Empty skips the check | | `BoundOrg` | empty | Yes for a shared issuer | Organization id that must appear in `coreflux:orgs`. Enrollment sets this | | `OrgsClaim` | `coreflux:orgs` | No | Claim holding organization ids | | `GroupsClaim` | `coreflux:groups` | No | Claim holding `orgId:group` names | | `RoleClaim` | `coreflux:role` | No | Claim holding `orgId:role` names. Used only for `AdminRoles` | | `ClaimsVersionClaim` | `coreflux:claims_version` | No | Claim holding the contract version | | `UsernameClaim` | `sub` | No | Session identity for `USER IS` | | `DisplayNameClaim` | `preferred_username` | No | Label in logs. Empty disables it. Never used for authorization | | `AdminRoles` | empty | No | Plain role names that receive the three admin permission flags | | `AdminGroups` | empty | No | Plain group names that receive the same flags. Compared case-insensitively | | `GroupsSeparator` | space | No | Separator for the list claims | | `ClockSkewSeconds` | `60` | No | Allowed clock difference for expiry | | `DisconnectOnExpiry` | `true` | No | Disconnect when the access token expires | | `ExpiryGraceSeconds` | `30` | No | Extra seconds after expiry before disconnect | > [!WARNING] > > An enabled configuration with no issuer, more than one issuer, an empty `ClientIdAllowlist`, or a shared issuer and an empty `BoundOrg` **refuses to start**. The exception is enrollment: if enrollment is on and `BoundOrg` is not known yet, the broker starts, serves local users, and refuses token logins until enrollment writes the organization id. See [Licensing and Enrollment](/latest/mqtt-broker/licensing). ## Failure modes | What you see | Cause | | - | - | | Broker will not start, log says the allowlist is empty or `BoundOrg` is not set | Fix `Oidc` or finish enrollment. A shared issuer with no organization binding is rejected on purpose | | MQTT 5 `BadUserNameOrPassword` | The password looked like a token and validation failed: bad signature, expired, wrong issuer, ID token, `client_id` not allowed, scope missing, claims version mismatch, organization mismatch, or a group entry naming an organization the token does not list | | MQTT 5 `ServerUnavailable` | The broker could not fetch the issuer's discovery document or signing keys. The connection is denied | | MQTT 5 `NotAuthorized` after a successful token check | A `FOR Connect` rule denied the session, or the broker required a clean session the client did not request | | Disconnect with `NotAuthorized` after the token lifetime | `DisconnectOnExpiry` dropped the client. Reconnect with a new access token | | `USER IS` never matches the person's email | The identity is `sub`. Match a group, or set `UsernameClaim` only if that claim is stable and unique | | A local password is rejected as a bad token | The password contained two dots and three non-empty segments, so it took the token path | Login attempts are published on [`$SYS/Coreflux/Comms/Logins`](/latest/mqtt-broker/sys-topics#syscorefluxcommslogins). For a token login, `Username` is the subject and `DisplayName` is the display-name claim. The password is not included. ## How this fits with the portal [portal.coreflux.org](https://portal.coreflux.org) is where an organization gets a license and registers brokers. Enrollment tells **this** broker which organization it belongs to (`BoundOrg`). The HUB fleet view lists those brokers for people signed in with an account in that organization. **Connect** on a fleet row sends that account's access token to the broker. LoT rules then see the groups on the token. A license plan and an organization binding are different steps. The plan is [activation](/latest/mqtt-broker/licensing). The binding is enrollment. Token login needs the binding when the issuer is shared. *** ## Best practices > **Prefer groups over subject ids** > > Subject ids are opaque. Write `USER IN GROUP` rules for the names your identity provider puts on the token, and keep `UsernameClaim` on `sub`. > **Put tokens on TLS listeners** > > The access token is the CONNECT password. Expose `8883` and `443` on the broker host, and keep `1883` off untrusted networks. > **Keep the client allowlist short** > > Every id in `ClientIdAllowlist` can present a token this broker will try to accept. Add a service client only for the job that needs MQTT. > **Do not confuse this with MCP tokens** > > [MCP Server](/latest/mqtt-broker/mcp-server) bearer tokens are minted by the broker for HTTP `/mcp`. They are not OIDC access tokens, and an OIDC token is not an MCP token. > [!WARNING] > > The broker accepts RS256 access tokens only, and it requires the `token_use` claim to equal `access`. There is no setting to allow another algorithm or to skip that claim. An identity provider that omits `token_use` or `client_id` cannot authenticate MQTT clients until its tokens include them. ## Next Steps > **[Account, fleet, and Cloud mode](/hub/account-and-fleet)** > > Sign in to the HUB with a Coreflux account and connect to brokers in the organization. > **[RBAC with LoT Rules](/latest/lot-language/rules/rbac#federated-accounts)** > > Authorize token sessions with groups and permission flags. # Operational Endpoints Source: https://docs.coreflux.org/latest/mqtt-broker/operational-endpoints Probe liveness, readiness, license status, and Prometheus metrics on the broker's dedicated HTTP port ## Check the broker without opening MQTT Orchestrators and monitoring systems need a plain HTTP answer: is the process up, is it accepting clients, and what is it doing. The Coreflux MQTT Broker serves that on a **dedicated ops port** (default `9100`), separate from MQTT and from the WebSocket data plane. > [!TIP] > > **Like a side door for the building inspector.** Devices still use the front door (MQTT). Kubernetes and Prometheus use the side door, a small HTTP port that only answers health and status questions. Use these endpoints when you need to: * tell a container orchestrator when to restart the process, and when to send it traffic; * scrape broker gauges and counters into Prometheus; * read version, ports, and license status from the broker host without an MQTT login. The listener is **unauthenticated**. Keep port `9100` on a private network. See [Security](#security). *** These paths answer only on the ops port. The same path on the WebSocket / HTTP port (default `5000`) returns **404**. ## Endpoints | Method | Path | Success | What you get | | - | - | - | - | | `GET` | `/health`, `/healthz` | `200` | Liveness. Always healthy while the process is serving. | | `GET` | `/ready`, `/readyz` | `200` or `503` | Readiness. `503` until startup has finished **and** MQTT is accepting connections. | | `GET` | `/info` | `200` | Build, runtime, ports, and short summaries. No secrets. | | `GET` | `/license` | `200` | License tier, limits, usage, fingerprint, activation, and enrollment. | | `GET` | `/metrics` | `200` | Prometheus text. | | `GET` | `/cluster` | `200` | High-availability summary. Clustering off is still `200`. | | `GET` | `/cluster/trace` | `200` | Replication trace of QoS 1 and 2 messages on the broker host. | | `GET` | `/loglevel` | `200` | Current minimum log level. | | `POST` | `/loglevel` | `200` or `400` | Change the minimum log level until the next restart. | All JSON responses use camelCase property names. ## Liveness `GET /health` (alias `/healthz`) means the process is up and answering HTTP. It does **not** mean MQTT is accepting clients. Use `/ready` for that. ```json wrap theme={"theme":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} { "status": "healthy", "version": "2.14.0", "timestamp": "2026-09-28T10:00:00Z" } ``` | Field | Meaning | | - | - | | `status` | Always `healthy` on this path. | | `version` | Broker version string. | | `timestamp` | UTC time of the response (`yyyy-MM-ddTHH:mm:ssZ`). | ## Readiness `GET /ready` (alias `/readyz`) returns `200` only when **both** are true: 1. The broker application has finished starting. 2. The MQTT server is accepting connections. Until then the status is **503**. Ready: ```json wrap theme={"theme":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} { "status": "ready", "timestamp": "2026-09-28T10:00:00Z" } ``` Not ready: ```json wrap theme={"theme":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} { "status": "not_ready", "reason": "MQTT server is not accepting connections yet", "timestamp": "2026-09-28T10:00:00Z" } ``` | `reason` | When | | - | - | | `application is still starting` | The process is up, but startup has not finished. | | `MQTT server is not accepting connections yet` | Startup finished, and the MQTT listener is not up yet. | Point a Kubernetes **liveness** probe at `/health` and a **readiness** probe at `/ready`. A liveness probe on `/ready` restarts the broker during a normal startup. ## Runtime info `GET /info` is a single JSON document: name, version, uptime, operating system, Python sandbox, cluster and MCP summaries, telemetry status, listener ports, and the CPU, memory, and GC view the process actually has. It does not include passwords, tokens, license files, or the cluster shared secret. A standalone broker (clustering and MCP off, census not yet pinged) has this shape. `runtimeResources` below is trimmed to the CPU and GC fields operators compare first; the live document also includes process memory, the thread pool, container signals, and cgroup data. ```json wrap theme={"theme":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} { "name": "Coreflux DataHub", "version": "2.14.0", "uptimeSeconds": 1234.5, "timestamp": "2026-09-28T10:00:00Z", "os": { "type": "Linux", "version": "Debian GNU/Linux 13", "architecture": "X64", "framework": ".NET 10.0" }, "python": { "available": true, "mode": "coreflux-runner", "version": "3.12.0", "backend": "linux-native", "isolationLevel": "OsNamespaces" }, "cluster": { "enabled": false }, "mcp": { "enabled": false }, "telemetry": { "enabled": true, "reason": null, "lastPingAt": null, "lastStatus": null }, "ports": { "mqtt": 1883, "mqttTls": 8883, "websocket": 5000, "websocketTls": 443, "ops": 9100 }, "runtimeResources": { "runtime": { "processorCount": 2, "is64BitProcess": true, "enableWriteXorExecute": true }, "gc": { "isServerGc": true, "configuration": { "ServerGC": true } } } } ``` `name` is the broker configuration name (`Coreflux MQTT Broker` if that name is unset). `ports` reflects the listeners in effect, including `ops`. `uptimeSeconds` is `0` when the process start time cannot be read. ### Python sandbox | `available` | `mode` | `backend` | `isolationLevel` | Meaning | | - | - | - | - | - | | `false` | `null` | `null` | `null` | Python execution is unavailable. | | `true` | `coreflux-runner` | Reported by the runner, or `unknown` | Reported by the runner, or `unknown` | Scripts run in the sandboxed runner. `unknown` means the runner has not answered yet. | | `true` | `in-process` | `python.net` | `none` | Legacy in-process Python, with no sandbox boundary. | ### Cluster and MCP summaries `cluster` is `{ "enabled": false }` when high-availability clustering is off. When it is on, the summary is `enabled`, `clusterId`, `node`, `role` (lowercase), `isLeader`, `leader`, and `members` (the member count). While a configured cluster fails to start, it is `enabled: false` with `configured: true` and a `startFailure` block. Full status is [`GET /cluster`](#cluster-status). `mcp` is `{ "enabled": false }` when the [MCP Server](/latest/mqtt-broker/mcp-server) is off. When it is on, the summary is: | Field | Meaning | | - | - | | `enabled` | `true` | | `endpoint` | Full URL, such as `http://127.0.0.1:6278/mcp` | | `bind` | Bind address | | `port` | MCP listen port | | `toolsCount` | Tools currently advertised | | `tokensActive` | How many tokens exist — never the token values or the usernames they map to | | `auth` | `bearer` | That object is not the MCP listener. MCP `/health` and MCP `/info` stay on the MCP port (default `6278`). ### Telemetry summary `telemetry` is the anonymous install-census status. It does not include an install id, hostname, or license. | Field | Meaning | | - | - | | `enabled` | `true` when the daily census is allowed to run. | | `reason` | Why it is off: `starting` before the check finishes, `COREFLUX_TELEMETRY`, `DO_NOT_TRACK`, or `Telemetry.Enabled`. `null` when it is on. | | `lastPingAt` | UTC time of the last attempt, or `null` before the first one. | | `lastStatus` | HTTP status from the platform, or `timeout` / `error`. `null` before the first attempt. | Set `COREFLUX_TELEMETRY` to `off`, `false`, or `0` to turn the census off. `COREFLUX_TELEMETRY` affects only the census. `DO_NOT_TRACK=1` turns off the census and also turns off [fleet performance metrics](/latest/mqtt-broker/licensing#fleet-performance-metrics). `Telemetry.Enabled: false` in the broker configuration turns the census off too. The environment is checked first. ### Runtime resources `runtimeResources` is the live view of CPU, memory, garbage collection, the thread pool, and (on Linux) cgroup limits. The fields operators compare most often: | Field | Meaning | | - | - | | `gc.configuration.ServerGC` | Server GC as **requested**. A `DOTNET_gcServer` or `COMPlus_gcServer` value wins; otherwise this is the mode baked in at publish. | | `gc.isServerGc` | The mode **actually in effect**. | | `runtime.processorCount` | Processors the runtime can see. | | `runtime.enableWriteXorExecute` | Effective write-xor-execute mode for generated code. `false` on `linux-arm` / ARMv7 publishes, `true` on other targets, unless `DOTNET_EnableWriteXorExecute` or `COMPlus_EnableWriteXorExecute` overrides it. | | `cgroup` | Linux cgroup files the process could read (CPU quota, memory max, cpuset). Empty off Linux. Keys look like `cpu.max (/sys/fs/cgroup/cpu.max)`. | `ServerGC` and `isServerGc` disagree when the runtime silently drops to workstation GC. That happens when only one processor is visible: a one-CPU container, a cgroup quota of one core, or a Kubernetes `limits.cpu` of `1` or less (`500m` is one processor). Give the broker at least two CPUs to keep Server GC, and confirm with `processorCount` and `cgroup`. The same object also includes process memory (`process`), thread-pool counts (`threadPool`), container signals (`container`), and `dotNetEnv` — only `DOTNET_GC*`, `DOTNET_ThreadPool*`, `COMPlus_*`, `DOTNET_EnableWriteXorExecute`, and `DOTNET_SYSTEM_GLOBALIZATION_INVARIANT`. Broker passwords and tokens are not in this document. ## License status `GET /license` returns the license status: tier, limits, usage, fingerprint, activation, and enrollment. A broker with no license still answers, with `tier` `Community`. HTTP property names are **camelCase**. The retained MQTT topic `$SYS/Coreflux/License` carries the same status with PascalCase names (`Tier`, `EffectiveLimits`), except `isTrial`, which is lowercase on both. Field meanings are on [Licensing and Enrollment](/latest/mqtt-broker/licensing#watch-license-status). ```json wrap theme={"theme":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} { "licensedTo": "N/A", "expiresUtc": null, "secondsToExpiry": null, "isActive": false, "tier": "Community", "isTrial": false, "fingerprint": "…", "fingerprintSource": "machine-id", "activation": { "mode": "offline", "bindingOk": true, "tamperDetected": false }, "enrollment": { "pending": false, "reason": null, "enrolled": false, "boundOrg": null }, "effectiveLimits": { "maxArtifacts": 24, "maxRoutes": 3, "maxConnections": 10000 }, "currentUsage": { "artifactCount": 4, "routes": 1 } } ``` `effectiveLimits` also includes `maxModels`, `maxRules`, `maxActions`, `maxVisus`, and `maxCertInTls`. `currentUsage` also includes `actions` (regular actions, route events, Python scripts), `models`, `rules`, and `panels`. Connector counts still use the name `routes`. If the status cannot be read, the response is **500**: ```json wrap theme={"theme":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} { "error": "failed to read license status", "detail": "…" } ``` > [!WARNING] > > `GET /license` is unauthenticated. It exposes the hardware fingerprint, plan, and enrollment state. Do not publish port `9100` on the public internet. Bind it to `127.0.0.1` when only local probes need it. MQTT clients that already authenticate can read the same status on `$SYS/Coreflux/License`. ## Metrics `GET /metrics` returns Prometheus text exposition format, version 0.0.4 (`text/plain; version=0.0.4; charset=utf-8`). A scrape still succeeds if the live client list cannot be read. In that case `coreflux_clients_connected`, `coreflux_sessions_total`, and `coreflux_retained_messages_total` are `0`. | Metric | Type | Meaning | | - | - | - | | `coreflux_up` | gauge | `1` while the process is running. | | `coreflux_ready` | gauge | `1` when the broker is ready to accept MQTT traffic (same rule as `/ready`). | | `coreflux_uptime_seconds` | gauge | Process uptime, in seconds. | | `coreflux_clients_connected` | gauge | MQTT clients connected right now. | | `coreflux_sessions_total` | gauge | Active MQTT sessions. | | `coreflux_retained_messages_total` | gauge | Retained messages held. | | `coreflux_messages_published_total` | counter | Application messages clients published to the broker since process start. | | `coreflux_messages_delivered_total` | counter | Application messages the broker delivered to clients since process start. | | `coreflux_subscriptions_total` | counter | `$SYS` topic subscriptions since the daily stats reset. Data-topic subscriptions are not counted. | | `coreflux_bytes_sent_total` | counter | Bytes sent since process start. | | `coreflux_bytes_received_total` | counter | Bytes received since process start. | | `coreflux_cpu_usage_ratio` | gauge | Process CPU over the last second, as a fraction of one (0–1). When a cgroup CPU quota is set, the fraction is of that quota; otherwise it is of all cores. `NaN` for the first second after start. | | `coreflux_auth_failures_total` | counter | CONNECTs the broker refused, for any reason (bad credentials, a reserved client id, connect rules, or session expiry). | | `coreflux_acl_denied_total` | counter | Publishes and subscriptions refused by the ACL. The `op` label is `publish` or `subscribe`. | | `coreflux_rate_limited_total` | counter | Publishes refused by the per-client rate limit. | | `coreflux_route_dropped_total` | counter | Messages shed by a saturated route channel. | | `coreflux_errors_total` | counter | Failures in routes, LoT actions, LoT models, and Python scripts. The `component` label is `route`, `action`, `model`, or `python`. | | `coreflux_process_memory_bytes` | gauge | Process working set, in bytes. | | `coreflux_gc_heap_bytes` | gauge | Managed heap size, in bytes. | > [!NOTE] > > Counters other than `coreflux_subscriptions_total` count from process start and return to zero only when the broker restarts, so Prometheus `rate()` and `increase()` work across midnight. In earlier releases, `coreflux_messages_published_total`, `coreflux_bytes_sent_total`, and `coreflux_bytes_received_total` followed the daily stats file (`FluxCapacitorStats_*.json`) and dropped to zero at UTC midnight. That daily file still keeps its per-day totals. `coreflux_subscriptions_total` still counts `$SYS` subscriptions since the daily stats reset. > > The same counters feed an enrolled broker's [fleet performance metrics](/latest/mqtt-broker/licensing#fleet-performance-metrics). They keep updating whether or not fleet metrics are turned on. ### When durability or clustering is active Each family below always includes an `*_enabled` gauge (`0` or `1`). The rest of the family is emitted only while that feature is active. | Family | When you see more than the enabled gauge | Where to read the feature | | - | - | - | | `coreflux_durability_enabled`, `coreflux_wal_*`, `coreflux_durable_*` | [MQTT state durability](/latest/mqtt-broker/durability) is active (WAL appends, fsyncs, durable retained messages, sessions, and queued QoS 1/2). | Durability | | `coreflux_cluster_*`, `coreflux_partitions_*`, `coreflux_partition_*`, `coreflux_replication_*` | [High-availability clustering](/latest/mqtt-broker/clustering) is on. Includes leadership, member counts, Raft progress, partition ownership, quorum replication, rejected publishes, delivery to other nodes, and failover. | Clustering | ## Cluster status `GET /cluster` is the [high-availability clustering](/latest/mqtt-broker/clustering) (beta) view of one node. It never includes the cluster shared secret. Ask the leader for the authoritative view of the other nodes: only the leader checks whether members are alive, so a follower reports other nodes as `unknown`. The answer has one of three shapes. **Clustering off** Clustering is disabled, not granted by the license, its configuration is invalid (the log names the problem), or the node is still starting: ```json wrap theme={"theme":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} { "enabled": false, "message": "Clustering is not enabled on this node.", "timestamp": "2026-10-04T10:00:00Z" } ``` **Start failing** Clustering is configured but could not start, for example because `AdvertisedHost` does not resolve, the cluster port is in use, or the data directory is not writable. Until it starts, the node serves its clients but is not part of the cluster. It retries after 5 s, then with doubling delays up to one minute; a rising `attempts` means the cause persists: ```json wrap theme={"theme":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} { "enabled": false, "configured": true, "message": "Clustering is configured but failed to start; this node is not part of the cluster. Retrying.", "startFailure": { "error": "Cluster: could not resolve the advertised host 'broker-a' within 30s; set Cluster.AdvertisedHost to an address peers can reach.", "attempts": 3, "firstFailedAt": "2026-10-04T10:00:00Z", "lastFailedAt": "2026-10-04T10:01:10Z", "nextAttemptAt": "2026-10-04T10:01:50Z" }, "timestamp": "2026-10-04T10:01:20Z" } ``` **Running** A healthy member of a 3-node cluster (trimmed): ```json wrap theme={"theme":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} { "enabled": true, "identity": { "clusterId": "factory-floor", "node": "broker-a", "endpoint": "10.0.0.11:9200" }, "leadership": { "role": "leader", "isLeader": true, "leader": "broker-a" }, "raft": { "term": 7, "commitIndex": 25, "lastAppliedIndex": 25, "hasState": true, "standby": false, "standbyRecoveries": 0, "logReset": null, "quorumSize": 2 }, "members": [ { "name": "broker-a", "endpoint": "10.0.0.11:9200", "isSelf": true, "isLeader": true, "status": "alive" }, { "name": "broker-b", "endpoint": "10.0.0.12:9200", "isSelf": false, "isLeader": false, "status": "alive" }, { "name": "broker-c", "endpoint": "10.0.0.13:9200", "isSelf": false, "isLeader": false, "status": "alive" } ], "membersTotal": 3, "membersAlive": 3, "partitions": { "enabled": true, "mapVersion": 14, "partitionCount": 32, "replicationFactor": 3, "underReplicated": 0 }, "replication": { "enabled": true, "quorumGateEnabled": true, "connectedPeers": 2, "quorumFailures": 0, "lastQuorumMicros": 2446 }, "timestamp": "2026-10-04T10:05:00Z" } ``` When clustering is running, the document has these blocks: | Block | What it shows | What to look for | | - | - | - | | `identity` | `clusterId`, this `node`, and the `endpoint` the other nodes use to reach it | `endpoint` matches how the other nodes list this node in `members` | | `leadership` | `role` (`leader`, `follower`, or `candidate` while no leader is known), `isLeader`, and the `leader`'s name | Every node names the same leader | | `raft` | Election `term`, the `commitIndex` and `lastAppliedIndex` of the cluster's own bookkeeping (never MQTT messages), `quorumSize`, `hasState` (this node belongs to a cluster), `standby` and `standbyRecoveries`, and `logReset` | `term` rising quickly means repeated elections. `logReset` is set when the node found its cluster log unreadable at start, moved it aside, and rejoined from the leader | | `discovery`, `topology` | Discovery mode and the QoS 0 topology | — | | `members`, `membersTotal`, `membersAlive` | Each member's `name`, `endpoint`, `isSelf`, `isLeader`, and `status` | `membersTotal` equals your number of nodes; on the leader, `membersAlive` equals `membersTotal` | | `dataPlane` | Names of the links that carry QoS 0 messages to the other nodes | One per other node with the `mesh` topology | | `partitions` | `mapVersion`, `partitionCount`, `replicationFactor`, `underReplicated`, the number of partitions each node owns, this node's partitions, and the full map (`owner`, `replicas`, and `epoch` per partition). `enabled: false` until the cluster has formed | Owners spread evenly across nodes; the map version rises after each node failure or return | | `replication` | `quorumGateEnabled`, `connectedPeers`, and counters since `startedUtc`: accepted and forwarded publishes, rejected publishes (`quorumFailures`), the last time to reach a majority (`lastQuorumMicros`), and the `wal`, `forwarding`, `sessions`, `failover`, `consistency`, and `transport` blocks. `partitionProgress` lists, for each partition this node holds, the last stored sequence and epoch | `connectedPeers` equals the number of other nodes; `forwarding.backlog` near `0`; `forwarding.dropped` stays `0`. The same `partitionProgress` values on every copy of a partition mean the copies agree | | `lot` | The committed LoT version and the version this node has applied, each node's state and why it is behind, `mixedMode` while a live node can't take part in LoT changes, and who owns each timer, `ON START` action, and exclusive connector | Every node has applied the committed version; `mixedMode` is `false` | | `routeListeners` | Each server connector's listener on this node (Modbus server, OPC UA server, REST API server, video output) and its state, with the reason when a bind failed | Every listener is up on every node | The counters restart at `0` when the node restarts. The short form on [`/info`](#runtime-info) is enough for a dashboard tile. See [Clustering](/latest/mqtt-broker/clustering#monitoring) for the values to watch. ## Replication trace `GET /cluster/trace` reads the trace of QoS 1 and 2 messages on the broker host. Turn it on with `ReplicationTrace` in the cluster configuration (see [Replication](/latest/mqtt-broker/clustering#replication)). While that setting is off, the trace does not record messages. The node writes the trace to `replication/trace`. At about 100 messages per second, expect about 50 MB per hour on each node. `ReplicationTraceMaxMegabytes` (default `512`, from 8 to 8192) caps the files, and the oldest are deleted first. The path is on the ops port (default `9100`) and is unauthenticated, like the other paths on that port. Keep the port on a private network. ```bash wrap theme={"theme":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} curl -s http://127.0.0.1:9100/cluster/trace ``` ## Log level `GET /loglevel` and `POST /loglevel` change the broker's minimum log level without a restart. The new level lasts until the process exits. The next start uses `COREFLUX_LOG_LEVEL` again (default `Information`). Read the current level: ```bash wrap theme={"theme":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} curl -s http://127.0.0.1:9100/loglevel ``` ```json wrap theme={"theme":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} { "level": "Information" } ``` Set it with a query string or a JSON body. If both are sent, the query string wins. Names are case-insensitive; the response uses the canonical name. ```bash wrap theme={"theme":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} curl -s -X POST 'http://127.0.0.1:9100/loglevel?level=Debug' ``` ```bash wrap theme={"theme":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} curl -s -X POST http://127.0.0.1:9100/loglevel \ -H 'Content-Type: application/json' \ -d '{"level":"Debug"}' ``` Success is `{ "level": "Debug" }`. An unknown or missing level is **400**: ```json wrap theme={"theme":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} { "error": "invalid or missing level", "allowed": ["Verbose", "Debug", "Information", "Warning", "Error", "Fatal"] } ``` > [!WARNING] > > `POST /loglevel` is unauthenticated. Anyone who can reach the ops port can raise verbosity for the whole process. Keep the port on a private network. ## Configuration The ops listener is on by default. Environment variables override the broker configuration. Restart the broker after changing them. | Broker configuration | Environment variable | Default | Effect | | - | - | - | - | | `OpsEnabled` | `COREFLUX_OPS_ENABLED` | `true` | Turn the whole ops listener on or off. | | `OpsPort` | `COREFLUX_OPS_PORT` | `9100` | TCP port. Must be from `1` to `65535`. | | `OpsBindIP` | `COREFLUX_OPS_BIND_IP` | `0.0.0.0` | Bind address on the broker host. `127.0.0.1` keeps probes on the broker host only. | When `COREFLUX_OPS_ENABLED` is set, the listener is on only for `1` or `true` (any letter case). `false`, `0`, and any other value turn it off. When the variable is unset, `OpsEnabled` is used. An invalid `COREFLUX_OPS_PORT` (not an integer, or outside `1`–`65535`) is ignored, and `OpsPort` is used instead. A non-empty `COREFLUX_OPS_BIND_IP` replaces `OpsBindIP`. When the listener is disabled, nothing is bound to the ops port and these paths are not served. ```bash wrap theme={"theme":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} export COREFLUX_OPS_ENABLED=true export COREFLUX_OPS_PORT=9100 export COREFLUX_OPS_BIND_IP=127.0.0.1 ``` The same names are listed under [Environment Variables & Secrets](/latest/mqtt-broker/secrets-and-env#operational-http-health-ready-info-license-metrics). ## Security These endpoints are unauthenticated on purpose, so orchestrator probes and in-cluster scrapers do not need a password. * Do not expose port `9100` to the public internet. * On a single broker host, bind `127.0.0.1` (or publish the Docker port on `127.0.0.1` only). * In Kubernetes, leave the port on the pod network and scrape it with a Service. Do not put it on a public LoadBalancer. * Treat `/license` and `POST /loglevel` as sensitive even though `/health` is not. `/info` is intentionally free of secrets: no MQTT passwords, no MCP token values, no license file, no cluster shared secret. ## Kubernetes The published broker image has no shell, so use HTTP probes rather than a command inside the container. ```yaml wrap theme={"theme":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} ports: - name: mqtt containerPort: 1883 - name: ops containerPort: 9100 livenessProbe: httpGet: path: /health port: 9100 initialDelaySeconds: 5 periodSeconds: 15 readinessProbe: httpGet: path: /ready port: 9100 initialDelaySeconds: 5 periodSeconds: 10 ``` A longer startup example is on [Startup Options](/latest/mqtt-broker/startup-options). Prometheus Operator scrape (the Service must expose a port named `ops`): ```yaml wrap theme={"theme":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} apiVersion: monitoring.coreos.com/v1 kind: ServiceMonitor metadata: name: coreflux-broker spec: selector: matchLabels: app: coreflux-broker endpoints: - port: ops path: /metrics interval: 30s ``` ## Try it on the broker host With the default port published on the broker host: ```bash wrap theme={"theme":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} curl -s http://127.0.0.1:9100/health curl -s http://127.0.0.1:9100/ready curl -s http://127.0.0.1:9100/info curl -s http://127.0.0.1:9100/license curl -s http://127.0.0.1:9100/metrics ``` Confirm the ops paths are not served on the WebSocket / HTTP port (expect `404`): ```bash wrap theme={"theme":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} curl -s -o /dev/null -w '%{http_code}\n' http://127.0.0.1:5000/health ``` ## Next Steps > **[System topics](/latest/mqtt-broker/sys-topics)** > > Read the same license status, and the rest of the broker, over MQTT. > **[Licensing and Enrollment](/latest/mqtt-broker/licensing)** > > Interpret tier, limits, fingerprint, activation, and enrollment. # 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. > [!TIP] > > **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":{"light":"css-variables","dark":"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 connector 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 Connectors](/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 > **Efficient Communication** > > 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. > **Security and Access Control** > > 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. > **Intelligent Data Processing** > > 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. > **System Integration** > > IoT systems don't exist in isolation. They connect to databases, APIs, dashboards, and other brokers. > > **Coreflux enhancement:** LoT Connectors 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, licensing, logging, metrics, the MCP Server, and maintenance through MQTT-native management topics and the [ops HTTP listener](/latest/mqtt-broker/operational-endpoints) | *** ## Next Steps > **[MQTT Protocol](/latest/mqtt-broker/protocol)** > > Learn the fundamentals of MQTT messaging. > **[Installation Guide](/latest/quick-start/installation)** > > 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. > [!TIP] > > **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. > [!NOTE] > > 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":{"light":"css-variables","dark":"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 **1. Publisher sends message** A client publishes a message to a specific topic on the broker. **2. Broker receives message** The broker receives the message and checks which clients are subscribed to that topic. **3. Broker delivers message** 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":{"light":"css-variables","dark":"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/ └── ... ``` > [!TIP] > > 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. **Single-Level (+)** The `+` wildcard matches **exactly one** topic level. **Subscription:** `building/floor1/+/temperature` > [!TIP] > > **Matches:** `building/floor1/room101/temperature`, `building/floor1/room102/temperature`, `building/floor1/lobby/temperature` > [!WARNING] > > **Does NOT match:** `building/floor2/room201/temperature` (different floor), `building/floor1/room101/humidity` (different measurement), `building/floor1/west/room101/temperature` (extra level) **Multi-Level (#)** The `#` wildcard matches **any number of levels** and must be at the end. **Subscription:** `building/floor1/#` > [!TIP] > > **Matches:** `building/floor1/room101/temperature`, `building/floor1/room101/humidity`, `building/floor1/room102/status`, `building/floor1/lobby/motion/detected` > [!WARNING] > > **Does NOT match:** `building/floor2/room201/temperature` (different floor), `building/basement/storage/humidity` (different path) > [!WARNING] > > 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":{"light":"css-variables","dark":"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 | > [!NOTE] > > **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":{"light":"css-variables","dark":"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 | > [!NOTE] > > **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":{"light":"css-variables","dark":"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 | > [!NOTE] > > **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. > [!TIP] > > 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":{"light":"css-variables","dark":"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" ``` **1. Client connects with LWT** When connecting, the client specifies its LWT message, topic, QoS, and retain flag. **2. Connection maintained** The broker holds the LWT message as long as the client stays connected. **3. Unexpected disconnect** 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. | > [!NOTE] > > Persistent sessions are useful for mobile devices or intermittent connections where you don't want to miss messages during brief disconnections. *** ## Shared subscriptions A normal subscription delivers every matching message to every subscriber. A shared subscription splits that work: several clients join one group, and each matching message goes to exactly one member of the group. > [!TIP] > > **Like one ticket window with several clerks.** The next customer (message) is served by one clerk. Anyone watching the queue from outside the window (a normal subscription) still sees every customer. Subscribe with an MQTT 5 share filter: ```text wrap theme={"theme":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} $share/{group}/{filter} ``` `$share/workers/jobs/#` puts the client in the group `workers` for `jobs/#`. `$share/other/jobs/#` is a different group. The broker hands each matching message to one connected member, round-robin. A client subscribed to `jobs/#` without `$share` still receives every message. Shared subscriptions are off until you turn them on. Set `SharedSubscriptions` to `true`, or set `COREFLUX_MQTT_SHARED_SUBSCRIPTIONS` to `1` or `true`. See [Broker Configuration](/latest/mqtt-broker/configuration#shared-subscriptions). | Broker setting | What clients see | | - | - | | `SharedSubscriptions` `false` (default) | `$share/...` SUBSCRIBE is refused with `0x9E` (Shared Subscriptions Not Supported). CONNACK reports Shared Subscription Available = `0`. | | `SharedSubscriptions` `true` | CONNACK reports Shared Subscription Available = `1`. A malformed share filter (empty group name, `+` or `#` in the group name, or a missing filter) is refused with `0x8F` (Topic Filter Invalid). | > [!NOTE] > > In this version, a shared group in a cluster includes only the clients connected to that broker host. Clients on another host form their own group. MQTT 3.1.1 clients can use the `$share/` prefix once the feature is enabled. *** ## Keep Alive The keep-alive mechanism ensures the connection remains active and detects dead connections quickly. ```mermaid theme={"theme":{"light":"css-variables","dark":"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 | > [!TIP] > > 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. *** ## Authentication Clients present a username and password at CONNECT, or connect anonymously when the broker allows it. For organization accounts, the password is an access token and the broker checks it with the identity provider. That path is [OIDC and Bearer Authentication](/latest/mqtt-broker/oidc-authentication). It is separate from TLS: TLS protects the connection, and the token says who the client is. *** ## Next Steps > **[Broker Commands](/latest/mqtt-broker/commands)** > > Learn how to control the Coreflux broker via MQTT commands. > **[\$SYS Topics](/latest/mqtt-broker/sys-topics)** > > 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 connector 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, Connectors, and broker CLI commands using a unified syntax -- no external tools or services required. > [!TIP] > > **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. > [!NOTE] > > 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) config/ # Signed license, install identity, enrollment (persist this) bin/ # Internal broker data (managed automatically), including the LoT stores below log/ # Log files stats/ # Statistics ``` The LoT you deploy is saved in `bin/` as plain-text files, one per kind: `actions.lot`, `models.lot`, `routes.lot` and `rules.lot`, plus `Python.Scripts` for Python scripts and `project-entity-ownership.json`, which records which project loaded each entity. Back up `bin/` with the rest of the data directory. Do not edit these files while the broker runs; change LoT with the broker commands or a project. > [!NOTE] > > Earlier versions named these files `Saul.Ricardo` (actions), `Marco.Paulo` (models), `Roberto.Leal` (connectors), `Ze.Cabra` (rules) and `Ze.Cabra.project-rules.json` (project ownership). On the first start after an upgrade the broker copies each one to its new name and leaves the old file in place, so your LoT carries over and you can still go back to the earlier version. See [Update Coreflux](/latest/updating-coreflux). *** ## 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. | | `COREFLUX_CERT_STORAGE_ROOTS` | — | Extra folders (separated by `:` on Linux and macOS) under which the client certificate storage path may be set from the HUB or `-brokerCertificates`. See [Certificates](/hub/system/certificates#trust-store-settings). | | `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`, `/license`, `/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`). Paths, response shapes, Kubernetes probes, and the Prometheus scrape are on [Operational endpoints](/latest/mqtt-broker/operational-endpoints). `GET /license` returns the license status also published on the retained `$SYS/Coreflux/License` topic (tier, limits, usage, fingerprint, activation, enrollment). It is unauthenticated — keep the ops port on a private network. Field meanings: [Licensing and Enrollment](/latest/mqtt-broker/licensing#watch-license-status). ### Licensing and enrollment These names are read from the **process** environment at boot. They are not the LoT `.env` file you manage with `-setEnv`. | Variable | Description | | - | - | | `COREFLUX_ACTIVATION_CODE` | Online activation code (`CFLX-…`). Get a 30-day Growth-type license at [portal.coreflux.org](https://portal.coreflux.org). Also enables organization enrollment. | | `COREFLUX_LICENSE_TOKEN` | Inline signed license (compact JWS). | | `COREFLUX_LICENSE_FILE` | Path to a signed license file. | There is no enrollment-token environment variable — use `--enroll` / `-enroll` or `{data directory}/config/enrollment-token`. Full guide: [Licensing and Enrollment](/latest/mqtt-broker/licensing). ### MQTT | Variable | Default | Description | | - | - | - | | `COREFLUX_MQTT_SHARED_SUBSCRIPTIONS` | unset | Overrides `SharedSubscriptions` (default `false`). `1` or `true` enables MQTT 5 shared subscriptions; `0` or `false` disables them. See [Broker Configuration](/latest/mqtt-broker/configuration#shared-subscriptions). | OIDC bearer authentication is **not** configured with a process environment variable. The `Oidc` object lives in the broker configuration file. Enrollment writes `Oidc.BoundOrg` after the broker joins an organization. See [OIDC and Bearer Authentication](/latest/mqtt-broker/oidc-authentication). ### Connectors, durability, clustering, Python | Variable | Description | | - | - | | `COREFLUX_ROUTE_OVERLOAD_POLICY` | Broker-wide default `lossless`, `balanced`, or `realtime`. See [Store and Forward Configuration](/latest/lot-language/routes/store-and-forward). | | `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_MCP_*` | MCP Server listen address, tool allow-list, and token lifetime. See [MCP Server](/latest/mqtt-broker/mcp-server). | | `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":{"light":"css-variables","dark":"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`, connector 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. > [!NOTE] > > 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":{"light":"css-variables","dark":"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 connector 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":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} openssl rand -base64 32 # Example output: K7x2mN8pQ1rT5uW3yB6dF9hJ0kL4nP7sV2xZ8cG1eA= ``` > [!WARNING] > > 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 connector 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":{"light":"css-variables","dark":"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":{"light":"css-variables","dark":"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":{"light":"css-variables","dark":"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":{"light":"css-variables","dark":"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 Connectors Environment variables and secrets can be referenced directly in connector definitions, keeping connection details out of your LoT code. ### Standard Syntax Use `GET ENV` and `GET SECRET` in connector configuration blocks to inject values at runtime: ```lot wrap theme={"theme":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} DEFINE CONNECTOR 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 connector `ADD CONFIG` sections: ```lot wrap theme={"theme":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} WITH SERVER ENV:DB_HOST WITH PASSWORD SECRET:DB_PASSWORD ``` ### Event Query Placeholders In connector event queries (e.g. `WITH QUERY`), use curly-brace placeholders that are resolved at execution time: ```lot wrap theme={"theme":{"light":"css-variables","dark":"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":{"light":"css-variables","dark":"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 ``` > [!WARNING] > > **Quote values that contain `//`, a space, or `#`.** The command is cleaned before it runs. Outside double quotes, `//`, `/*` and `-- ` start a comment and cut off the rest of the line, and a space ends the value. Wrap the value in double quotes: `-setEnv API_URL="https://api.example.com/v1"`. The quotes are removed before the value is stored. The same rule applies to `-setSecret`. ### 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":{"light":"css-variables","dark":"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 ``` > [!NOTE] > > Subscribe to `$SYS/Coreflux/Command/Output` to receive confirmation responses from commands. *** ## Deployment Set up environment variables and secrets across different deployment targets. **Windows** On Windows, the data directory defaults to the broker's executable folder + `\Coreflux` unless `COREFLUX_DATA_PATH` is set (used as-is). **1. Locate the data directory** 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":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} [Environment]::SetEnvironmentVariable("COREFLUX_DATA_PATH", "C:\CorefluxData", "Machine") ``` Files then live directly under `C:\CorefluxData\`. **2. Create the .env File** Create a `.env` file in the data directory: ```powershell wrap theme={"theme":{"light":"css-variables","dark":"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 ``` **3. Generate an Encryption Key** Generate and save a `secret.key` file for encrypting secrets: ```powershell wrap theme={"theme":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} openssl rand -base64 32 > "C:\CorefluxData\secret.key" ``` **4. Set Secrets After Start** 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 ``` > [!TIP] > > Subscribe to `$SYS/Coreflux/Command/Output` to confirm the secret was stored. **Linux** APT/DNF installs already set `COREFLUX_DATA_PATH=/var/lib/coreflux` on `coreflux-broker.service`. Put `.env` and `secret.key` there. **1. Create the .env file** Write your environment variables and restrict permissions: ```bash wrap theme={"theme":{"light":"css-variables","dark":"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 ``` **2. Generate an encryption key** ```bash wrap theme={"theme":{"light":"css-variables","dark":"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 ``` **3. Restart the service** ```bash wrap theme={"theme":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} sudo systemctl restart coreflux-broker ``` **4. Set Secrets After Start** Using any MQTT client, publish to `$SYS/Coreflux/Command`: ``` -setSecret DB_PASSWORD=my_secure_password ``` > [!TIP] > > Verify with `-listSecrets` -- the secret name should appear in the output. **Docker Compose** ### 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":{"light":"css-variables","dark":"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":{"light":"css-variables","dark":"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":{"light":"css-variables","dark":"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 connectors 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 > [!NOTE] > > Secrets still need a volume for `secrets.json` persistence, even when passing the encryption key via `COREFLUX_SECRET_KEY`. **Kubernetes** 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":{"light":"css-variables","dark":"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":{"light":"css-variables","dark":"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":{"light":"css-variables","dark":"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":{"light":"css-variables","dark":"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":{"light":"css-variables","dark":"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 > **Use explicit encryption keys in production** > > 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. > **Restrict file permissions** > > On Linux, restrict access to sensitive files to the broker's service user only: > > ```bash wrap theme={"theme":{"light":"css-variables","dark":"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 > ``` > **Prefer GET ENV over hardcoded values** > > 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 Connectors portable across dev, staging, and production. > **Use mounted volumes for Docker** > > 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. > **Never commit secrets to version control** > > 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, Connector configs | `.env` file, then process env fallback | | `GET SECRET "NAME"` | Actions, Models, Connector configs | Decrypts from `secrets.json` | | `ENV:NAME` | Connector `ADD CONFIG` sections only | Same as `GET ENV` | | `SECRET:NAME` | Connector `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 > **[Broker Commands](/latest/mqtt-broker/commands)** > > Full reference for all broker CLI commands including env and secret management. > **[Operations Reference](/latest/lot-language/actions/operations)** > > 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 * Stage a signed license or enrollment token before MQTT accepts connections > [!TIP] > > **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. | > [!NOTE] > > 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, Connectors, 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` | Stage a signed license file (compact JWS) as `config/license.jws` before MQTT starts. | | `-showFingerprint` | `--show-fingerprint` | Print the broker host's hardware fingerprint and exit. Send it to Coreflux to be issued an offline license. Does not start MQTT. | | `-enroll` | `--enroll` | Stage a single-use enrollment token (`et_…`) for organization binding. Also accepted on `$SYS/Coreflux/Command` while the broker is running. | 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. To activate a license at start, set `COREFLUX_ACTIVATION_CODE` in the process environment or pass `--load-license`. Register at [portal.coreflux.org](https://portal.coreflux.org) for a 30-day Growth-type license. See [Licensing and Enrollment](/latest/mqtt-broker/licensing). ### 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 **Linux** ```bash wrap theme={"theme":{"light":"css-variables","dark":"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 ``` **Windows** ```powershell wrap theme={"theme":{"light":"css-variables","dark":"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":{"light":"css-variables","dark":"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 ``` > [!WARNING] > > Force flags overwrite existing configurations. Back up your current configuration before using them. ### Loading a project at startup ```bash wrap theme={"theme":{"light":"css-variables","dark":"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 ``` > [!TIP] > > Package Actions, Models, Connectors, 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":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} { "Name": "MyCorefluxBroker", "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, "FullDebugLog": false, "ServerCertificatePath": "/path/to/certificate.pfx", "ServerCertificatePassword": "your-certificate-password", "ResendRetainTopics": false } ``` > [!NOTE] > > 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":{"light":"css-variables","dark":"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`. **Docker** 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":{"light":"css-variables","dark":"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":{"light":"css-variables","dark":"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. **Docker Compose** Use the Compose file from [Installation](/latest/quick-start/installation). To load a project, add `command` on the broker service: ```yaml wrap theme={"theme":{"light":"css-variables","dark":"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. **Kubernetes** Mount a PVC at `/etc/project`. Use HTTP probes on port `9100` (the image has no shell). Paths and a Prometheus scrape are on [Operational endpoints](/latest/mqtt-broker/operational-endpoints#kubernetes). ```yaml wrap theme={"theme":{"light":"css-variables","dark":"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. **Linux packages** 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":{"light":"css-variables","dark":"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":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} sudo systemctl edit coreflux-broker ``` ```ini wrap theme={"theme":{"light":"css-variables","dark":"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`. ### Ports below 1024 The service runs as the unprivileged user `coreflux`. The unit grants `CAP_NET_BIND_SERVICE`, so the broker can bind ports below 1024. A package upgrade replaces the unit file. Drop-in files under `/etc/systemd/system/coreflux-broker.service.d/` are kept. An older package does not grant that capability. Add it in a drop-in: ```bash wrap theme={"theme":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} sudo systemctl edit coreflux-broker ``` ```ini wrap theme={"theme":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} [Service] AmbientCapabilities=CAP_NET_BIND_SERVICE CapabilityBoundingSet=CAP_NET_BIND_SERVICE ``` Then reload and restart: ```bash wrap theme={"theme":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} sudo systemctl daemon-reload sudo systemctl restart coreflux-broker ``` A container, or a broker you start without the APT or DNF package, needs that capability on the process, or a port of 1024 or above. **Windows** [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":{"light":"css-variables","dark":"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. **Portable Linux** For a tarball (not APT/DNF), run as root: ```bash wrap theme={"theme":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} sudo ./CorefluxMQTTBroker --install ``` That writes a systemd unit. For production, still prefer the Linux package from the [Downloads page](https://portal.coreflux.org/downloads) so upgrades and `COREFLUX_DATA_PATH` stay consistent. *** ## Best practices > **Set the data path in the environment** > > 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`. > **Use force flags only when you mean to reset** > > `-cf` and `-uf` overwrite existing configuration. Back up first. > **Ship LoT as a project** > > Load Actions, Models, Connectors, and Rules with `--project`, not with ad-hoc `.lot` file flags at process start. > **Persist the data volume** > > Always mount a volume at `/etc/project` in Docker and Kubernetes. Without it, configuration, secrets, projects, the signed license (`config/license.jws`), and enrollment credentials disappear when the container is recreated. See [Licensing and Enrollment](/latest/mqtt-broker/licensing#persist-the-data-volume). *** ## Troubleshooting > **Configuration not applied** > > 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. > **Port already in use** > > ```bash wrap theme={"theme":{"light":"css-variables","dark":"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. > **Broker exits at start with code 78 (a LoT state file cannot be used)** > > Before it loads anything, the broker checks its LoT files in `{data directory}/bin/` (`actions.lot`, `models.lot`, `routes.lot`, `rules.lot`, `project-entity-ownership.json`, `Python.Scripts`). It exits with code `78` when it can't write to `bin/`, can't open one of these files for reading and writing, or can't copy a file with an earlier name to its new name. It prints each file with the reason. It does not start empty, because a broker without its rules or actions would allow and run different things than you configured. > > 1. Read the message in the console or the log. It names every file the broker cannot use. > 2. Give the user the broker runs as read and write access to those files. The container image runs as user `10001`, so on a mounted volume run `chown -R 10001 `. > 3. If the broker should start without a file, move that file aside and start again. > **Permission denied on Linux** > > 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. > > If the broker cannot bind a port below 1024, the process is missing `CAP_NET_BIND_SERVICE`. Current APT and DNF units grant it. On an older package, add the drop-in under [Ports below 1024](#ports-below-1024). A container or a manual run needs that capability, or a port of 1024 or above. *** ## Next Steps > **[Broker Configuration](/latest/mqtt-broker/configuration)** > > Ports, TLS, sessions, ops, and every JSON field. > **[Licensing and Enrollment](/latest/mqtt-broker/licensing)** > > Activation codes, signed licenses, enrollment, and `GET /license`. # 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, connector health, and project state as MQTT messages — the same way you already consume sensor data. > [!TIP] > > **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` | | See the license tier and usage | `$SYS/Coreflux/License` (same status as ops [`GET /license`](/latest/mqtt-broker/operational-endpoints#license-status)) | | Send management commands | `$SYS/Coreflux/Command` and `Command/Output` | | Inspect or back up LoT source | `$SYS/Coreflux/Actions/#`, `Models/#`, `Routes/#`, `Rules/#`, `Visus/#` | | Watch connector health | `$SYS/Coreflux/Routes/+/status` | | See which project is loaded | `$SYS/Coreflux/Projects/active` | | Confirm the MCP Server is listening | `$SYS/Coreflux/MCP/Info` | > [!WARNING] > > 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 │ ├── Connectors/ # Connector LoT │ ├── Rules/ # Rule LoT │ ├── Visus/ # Panel LoT │ ├── Things/ # Thing source, tree, status │ ├── Projects/ # Project list, active, manifest, download │ ├── Entities/ownership # Project vs ad-hoc origin │ ├── MCP/Info # MCP Server status │ ├── MCP/Tools # Advertised MCP tool names │ ├── Cluster/ # Role, leader, members (when clustering) │ ├── Python/Scripts # Registered Python scripts │ ├── AI/Providers # Discovered LLM providers │ └── Certificates/Summary # Certificate expiry counts (retained) └── / # 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}` | Connector LoT (**retained**) | | `$SYS/Coreflux/Routes/{name}/status` | Connector 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**) | | `$SYS/Coreflux/MCP/Info` | MCP Server status and endpoint (**retained**) | | `$SYS/Coreflux/MCP/Tools` | Currently advertised MCP tool names (**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 connector 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. **1. Subscribe to output** Using any MQTT client (such as MQTT Explorer), subscribe to `$SYS/Coreflux/Command/Output`. **2. Publish the command** Publish the command string to `$SYS/Coreflux/Command`. Add `-requestId ` when several commands may be in flight. **3. Read the JSON envelope** 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":{"light":"css-variables","dark":"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, Connector, 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":{"light":"css-variables","dark":"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/{connector_name}` Connector LoT (**retained**). Extra subtopics appear depending on the connector type: | Topic | Content | | - | - | | `$SYS/Coreflux/Routes/{name}/status` | Health and counters. See [Connector Status Monitoring](/latest/lot-language/routes/overview#connector-status-monitoring) | | `$SYS/Coreflux/Routes/{name}/Certificates` | Certificate-store summary for cert-managed connectors (**retained**) | | `$SYS/Coreflux/Routes/{name}/Tools` | MCP tool summary for MCP connectors | | `$SYS/Coreflux/Routes/{name}/Tools/{tool}` | One MCP tool (schema and capability stats) | | `$SYS/Coreflux/Routes/{name}/Tools/{tool}/Capability` | Call statistics for one MCP tool, updated after every call (**retained**). See [Capability Payload](/latest/lot-language/routes/mcp-routes#capability-payload) | | `$SYS/Coreflux/Routes/{name}/Events/{event}/Trigger` | **Publish** to fire that connector event | ### `$SYS/Coreflux/Agents/{agent}` Agent source (**retained**). Agents are not listed under `$SYS/Coreflux/Routes/`. See [Agent topics](/latest/lot-language/agents/syntax#agent-topics). | Topic | Content | | - | - | | `$SYS/Coreflux/Agents/{agent}/card` | Discovery card, schema version 1 (**retained**) | | `$SYS/Coreflux/Agents/{agent}/status` | `online`, `offline`, or `error` (**retained**) | | `$SYS/Coreflux/Agents/{agent}/task/{id}/request` | **Publish** work. Needs a `PublishSys` rule | | `$SYS/Coreflux/Agents/{agent}/task/{id}/response` | Final answer | | `$SYS/Coreflux/Agents/{agent}/task/{id}/stream` | Event stream, schema version 1 | | `$SYS/Coreflux/Agents/{agent}/task/{id}/status` | Task status | | `$SYS/Coreflux/Agents/{agent}/task/{id}/cancel` | **Publish** to cancel that task | The broker does not publish `$SYS/Coreflux/Agents/{agent}/Capabilities` or `…/Metrics`. ### `$SYS/Coreflux/Things/{name}` Every Thing is published **retained**. An empty payload means the Thing was removed. See [LoT THING](/latest/lot-language/things/overview). | Topic | Payload | | - | - | | `$SYS/Coreflux/Things/{name}` | The source as typed, for roots and templates | | `$SYS/Coreflux/Things/{name}/tree` | JSON: `kind`, `template`, `uses`, `usedBy`, `params`, `project`, and for a root the `instance` tree (each instance's `chain`, `template`, `path`, resolved `params`, `children`, and nested `instances`) | | `$SYS/Coreflux/Things/{name}/status` | Roots only: `state` (`ok`, `degraded`, or `error`), child counts, the `worst` children, and `updated`. Built from connector health, agent status, and action errors | Children keep their own topics (`$SYS/Coreflux/Routes/…`, `$SYS/Coreflux/Agents/…`). `$SYS/Coreflux/Entities/ownership` marks each child with `thing: { root, chain, clause }` and lists each Thing as `thing:`. ### `$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 Connectors | `$SYS/Coreflux/Routes/#` | | All agents | `$SYS/Coreflux/Agents/#` | | All Things | `$SYS/Coreflux/Things/#` | | All Rules | `$SYS/Coreflux/Rules/#` | | All panels | `$SYS/Coreflux/Visus/#` | *** ## Broker Information ### `$SYS/Coreflux/Version` Broker version and build (**retained**). ```json wrap theme={"theme":{"light":"css-variables","dark":"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, current usage, activation mode, fingerprint, and organization enrollment (**retained**). The same status is served on ops [`GET /license`](/latest/mqtt-broker/operational-endpoints#license-status) (default port `9100`; HTTP names are camelCase). Brokers with no license still publish this topic so you can see `Tier` (`Community`), `EffectiveLimits`, and `CurrentUsage`. There is no built-in trial: with no license the payload shows **Community**. A trial is a **30-day Growth-type license** from [portal.coreflux.org](https://portal.coreflux.org) that you activate on the broker. Use the same portal to manage your fleet. See [Licensing and Enrollment](/latest/mqtt-broker/licensing). ```json wrap theme={"theme":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} { "LicensedTo": "N/A", "ExpiresUtc": null, "IsActive": false, "Tier": "Community", "Fingerprint": "…", "FingerprintSource": "machine-id", "Activation": { "Mode": "offline", "BindingOk": true }, "Enrollment": { "Pending": false, "Reason": null }, "EffectiveLimits": { "MaxArtifacts": 24, "MaxRoutes": 3, "MaxConnections": 10000 }, "CurrentUsage": { "ArtifactCount": 3, "Actions": { "Regular": 2, "RouteEvents": 0, "PythonScripts": 0 }, "Models": 0, "Rules": 12, "Routes": 1, "Panels": 0 } } ``` JSON still uses `Routes` for connectors. `Enrollment.Pending: false` means no problem was recorded — not proof that the broker is bound to an organization. ### `$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":{"light":"css-variables","dark":"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" } } ``` `ServerCertificatePassword` is masked as `********`. Publishing the masked value back to `$SYS/Coreflux/Config/New` keeps the stored password. > [!NOTE] > > 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 merges that object into the live configuration and keeps every omitted field, including bind addresses. It then writes `$SYS/Coreflux/Config/New/Result` with success or validation failure text. See [Applying Configuration](/latest/mqtt-broker/configuration#via-mqtt-topic). ### `$SYS/Coreflux/Certificates/Summary` Retained certificate health for the broker's TLS identity and mTLS trust store. The broker publishes it after every `-brokerCertificates` change and at most every 5 minutes otherwise. The HUB status bar reads it. It carries no host paths. ```json wrap theme={"theme":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} { "pathOk": true, "pathError": null, "thresholdDays": 30, "total": 4, "valid": 1, "expiring": 2, "expired": 1, "notYetValid": 0, "unreadable": 0, "server": { "commonName": "broker.example.com", "notAfter": "2027-11-19T00:00:00Z", "status": "valid", "daysUntilExpiry": 399 }, "rootCa": { "commonName": "Example Root CA", "notAfter": "2029-10-19T00:00:00Z", "status": "valid", "daysUntilExpiry": 1099 }, "alerts": { "expiring": 2, "expired": 1, "total": 3 }, "level": "expired", "updatedAt": "2026-10-15T11:48:00Z" } ``` `total` through `unreadable` count client certificates in the storage path (revoked ones excluded). `alerts` adds the server certificate and Root CA. `level` is `expired`, `expiring`, or `ok`. When the storage path is unavailable, `pathOk` is `false` and the counts are `0`. ### `$SYS/Coreflux/Config/Users` Configured users with passwords masked. `Groups` is the list used by `USER IN GROUP` in Rules. ```json wrap theme={"theme":{"light":"css-variables","dark":"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) | *** ## MCP Server When the [MCP Server](/latest/mqtt-broker/mcp-server) is enabled, the broker republishes two retained topics. Neither payload includes token values or the usernames tokens map to. ### `$SYS/Coreflux/MCP/Info` Status of the Streamable HTTP listener (`status` is `running` or `stopped`). Subscribe in MQTT Explorer to read `http_endpoint`, `bind_ip`, `http_port`, `tokens_active`, and the currently advertised `tools` list. ### `$SYS/Coreflux/MCP/Tools` An array of `{ "name": "…" }` objects for tools the **running** server currently advertises. When `EnableAllTools` is false and `EnabledTools` is empty, this list is empty. *** ## Connection Monitoring ### `$SYS/Coreflux/Comms/Sessions` Active MQTT sessions. ```json wrap theme={"theme":{"light":"css-variables","dark":"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":{"light":"css-variables","dark":"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 JSON object per login attempt. The password is not included. ```json wrap theme={"theme":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} { "ClientId": "lotvstudio-a6kjtbkwp", "Endpoint": "172.17.0.1:53454", "Username": "root", "DisplayName": "", "CleanSession": true, "ReasonCode": "Success" } ``` For an [OIDC access token](/latest/mqtt-broker/oidc-authentication), `Username` is the token subject and `DisplayName` is the token's display name (empty when the token has none). A local login leaves `DisplayName` empty and puts the broker username in `Username`. 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**). A Thing's child also carries `thing: { root, chain, clause }`. 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). > [!NOTE] > > 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":{"light":"css-variables","dark":"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":{"light":"css-variables","dark":"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. > [!WARNING] > > 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 > **[Broker Configuration](/latest/mqtt-broker/configuration)** > > Change ports, TLS, and other broker settings > **[Broker Commands](/latest/mqtt-broker/commands)** > > 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. > [!TIP] > > **Like learning chords before writing songs.** Your first Action proves the broker runs your logic; patterns teach you how dozens of Actions, Models, and Connectors 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. > **[Naming conventions](/latest/lot-language/usage-patterns/naming-conventions)** > > PascalCase entities, snake\_case variables, and topic naming basics. > **[Designing your data layer (the foundation)](/latest/lot-language/usage-patterns/data-layer)** > > Topic trees (UNS), scalar vs JSON payloads, Models, and inheritance — how data is shaped and addressed. > **[Working with JSON](/latest/lot-language/usage-patterns/working-with-json)** > > GET JSON in Actions, nested payloads, and publishing structured output through Models. > **[Writing reusable logic (the building blocks)](/latest/lot-language/usage-patterns/reusable-logic)** > > Callable actions, wildcards, counting over time, and Python — techniques for the Action you are writing now; logic that scales and stays maintainable. > **[Alarms and monitoring](/latest/lot-language/usage-patterns/alarms-and-monitoring)** > > Threshold crossings and device or connector offline detection; optional email connectors alongside alarms. > **[Connecting to the outside world (integrations)](/latest/lot-language/usage-patterns/integrations)** > > REST server and client, live KPIs from a database, and ENV / Secrets — Connectors that bridge to non-MQTT systems. > **[Production architecture](/latest/lot-language/usage-patterns/production-architecture)** > > End-to-end device-to-database ingestion and edge–cloud bridges — synthesis patterns that build on everything before. > **[Project organization](/latest/lot-language/usage-patterns/project-organization)** > > Keeping notebooks clean and readable — single-file vs multi-file layouts and shared discipline at every stage. > **[Anti-patterns](/latest/lot-language/usage-patterns/anti-patterns)** > > Common LoT mistakes and what to do instead. *** ## Next Steps > **[Introduction to LoT](/latest/lot-language/introduction)** > > Building blocks and how Actions, Models, Connectors, and Rules fit together. > **[Model Inheritance](/latest/lot-language/models/model-inheritance)** > > Deep dive on `FROM` and base models. New to the language? **[Introduction to LoT](/latest/lot-language/introduction)** links Actions, Models, Connectors, 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. ![The finished Line 3 Shift Lead panel with a temperature gauge, a live temperature trend chart, a pump toggle, and a Start Line button](https://mintcdn.com/coreflux/ry-30sFxFVWzN72U/images/dashboard/tutorial/line3-hero.png?fit=max\&auto=format\&n=ry-30sFxFVWzN72U\&q=85\&s=d5ee7920b953f0b16f80cb363f64b85f) Where you're heading: the finished panel — a temperature gauge, a live trend chart, a pump toggle, and a start button 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. > [!TIP] > > **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. **1. Generate some live data** 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":{"light":"css-variables","dark":"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. > [!TIP] > > **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`. **2. Find your signal** 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. ![Coreflux HUB Data Viewer showing the publish panel on the left and the topic tree with the sensor and machine topics](https://mintcdn.com/coreflux/ry-30sFxFVWzN72U/images/dashboard/manager/data-viewer-publish.png?fit=max\&auto=format\&n=ry-30sFxFVWzN72U\&q=85\&s=8fa37e11d51ba68bf4f031d1d3f6cbd0) The Data Viewer's PUBLISH panel and topic tree — confirm your signal is live before binding to it **3. Create a panel** 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":{"light":"css-variables","dark":"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 ``` **4. Add a gauge** 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":{"light":"css-variables","dark":"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. ![A radial temperature gauge with a 0 to 150 range reading a live value of 82 degrees](https://mintcdn.com/coreflux/ry-30sFxFVWzN72U/images/dashboard/tutorial/line3-gauge.png?fit=max\&auto=format\&n=ry-30sFxFVWzN72U\&q=85\&s=f3e61a06dfe5baf98f8c6899127a0043) The gauge bound to the live temperature signal (warning threshold configured at 100 °C) **5. Ask the AI to add a chart** 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":{"light":"css-variables","dark":"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. **6. Add a pump switch** 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":{"light":"css-variables","dark":"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" ``` **7. Wire an action** 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":{"light":"css-variables","dark":"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. ![The finished Line 3 Shift Lead panel showing a temperature gauge, a live temperature trend chart, a pump toggle, and a Start Line button](https://mintcdn.com/coreflux/ry-30sFxFVWzN72U/images/dashboard/tutorial/line3-hero.png?fit=max\&auto=format\&n=ry-30sFxFVWzN72U\&q=85\&s=d5ee7920b953f0b16f80cb363f64b85f) The final result: gauge, live trend chart, pump toggle, and start button — all live **8. Put it on a second 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":{"light":"css-variables","dark":"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 > **[Component reference](/hub/dashboards/components/display)** > > Every widget, property, and bind target. > **[Common patterns](/hub/dashboards/patterns)** > > 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 two things: a **`TYPE LLM` connector** (the model, the key, the price) and a **`DEFINE AGENT`** that uses it. The agent can do nothing until `ADD TOOL` grants a tool. Full options — grants, delegation, topics — are in [AI Connectors and Agents](/latest/lot-language/routes/ai-routes). > [!TIP] > > **Like a supervisor on the floor and an 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) > [!NOTE] > > The full `DEFINE AGENT` reference lives in the **LoT AGENT** section: [Agents Overview](/latest/lot-language/agents/overview), [Agent Syntax](/latest/lot-language/agents/syntax), and [Agent Examples](/latest/lot-language/agents/examples). Each tab is a full recipe: an LLM connector, an agent, and an Action where the work is scheduled or triggered. 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//response` | | **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/`, `connectors/` | 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 **Growth or Enterprise** license. Community refuses `-addAgent`. See [Licensing](/latest/mqtt-broker/licensing#plans) * A **model provider** — OpenAI, Anthropic, an OpenAI-compatible gateway, or **local Ollama** on the broker host. Every recipe below uses **OpenAI**. Swap `PROVIDER`, `MODEL`, and the secret name for another provider (see [Picking a provider](/latest/lot-language/routes/ai-routes#picking-a-provider)) * A **broker user** named in `DEFAULT_USER`. Each recipe uses `operator`. Create that user first (`-addUser operator …`). Tasks started by an Action run as this user, and the user must be allowed to do what the grants allow Store an [OpenAI API key](https://platform.openai.com/api-keys) once: ```lot wrap theme={"theme":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} KEEP SECRET "OPENAI_API_KEY" WITH "sk-..." ``` Deploy the connector with `-addConnector` and the agent with `-addAgent` (LoT Editor, a LoT Notebook, or `$SYS/Coreflux/Command`). The agent does not show up under Connectors. > [!NOTE] > > The **[HUB AI Assistant](/hub/ai-assistant)** (Coreflux icon in the dock) is a separate helper. It can run inside the HUB, or as a broker agent the wizard installs. This guide is about **your** agents — the ones you write in LoT. **Talk with your data** **Outcome:** Anyone who can publish to the interaction topic can ask a question. The agent reads live MQTT under `telemetry/#` and can list what is loaded. ```lot wrap theme={"theme":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} DEFINE CONNECTOR SiteModel WITH TYPE LLM ADD LLM_CONFIG WITH PROVIDER "openai" WITH MODEL "gpt-6-luna" WITH API_KEY GET SECRET "OPENAI_API_KEY" WITH API_SURFACE "auto" DEFINE AGENT SiteGuide USES LLM CONNECTOR SiteModel WITH DESCRIPTION "Answers questions about this site from live MQTT" WITH DEFAULT_USER "operator" WITH SYSTEM_PROMPT "You are a site operations guide. Use your tools to answer from live MQTT data. Be concise." WITH INTERACTION_TOPIC "agents/site" ADD TOOL "read_mqtt" WITH TOPICS "telemetry/#" ADD TOOL "read_lot" ``` **1. Deploy and confirm** Add **SiteModel** as a connector and **SiteGuide** as an agent. `-listAgents` should show `SiteGuide` as `online`. Read `$SYS/Coreflux/Agents/SiteGuide/status` if it stays in `error` — usually the connector has not started, or `operator` does not exist. **2. Ask a real question** In MQTT Explorer or HUB [Data Viewer](/hub/mqtt/data-viewer), publish to `agents/site/s1/ask` and subscribe to `agents/site/s1/response`. `s1` is the session. A later question on `s1` continues that conversation for the same user. ```text wrap theme={"theme":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} Which sensors have been updated in the last few minutes? ``` ```text wrap theme={"theme":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} What is the latest data on telemetry/zone1/status? ``` Rename `telemetry/` in the grant to `production/`, `solar/`, `buildings/`, or your own tree. A wildcard read shortens payloads over 1,024 characters. Reading the exact topic returns the whole payload. **3. Read the answer on response** `agents/site/s1/response` carries the agent's answer. Tool calls for that session appear on `$SYS/Coreflux/Agents/SiteGuide/task/s1/stream` when your user may subscribe to `$SYS`. > [!TIP] > > You just **talked with your data**. No extra AI server — the agent runs where your MQTT already lives. **Automatic report** **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":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} DEFINE CONNECTOR SiteModel WITH TYPE LLM ADD LLM_CONFIG WITH PROVIDER "openai" WITH MODEL "gpt-6-luna" WITH API_KEY GET SECRET "OPENAI_API_KEY" WITH API_SURFACE "auto" DEFINE AGENT OperationsReporter USES LLM CONNECTOR SiteModel WITH DESCRIPTION "Writes the hourly operations summary" WITH DEFAULT_USER "operator" WITH SYSTEM_PROMPT "You write operations reports from live MQTT. Use your tools on telemetry/ topics. Bullet points: current status, anomalies, items needing follow-up. Plain language for managers." WITH MEMORY "none" ADD TOOL "read_mqtt" WITH TOPICS "telemetry/#" 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.") RETURN AS {report} PUBLISH TOPIC "reports/operations/hourly" WITH {report} ``` **1. Deploy the connector, the agent, and the Action** There is no interaction topic. Only the schedule triggers this agent. A successful `CALL AGENT` returns the answer text in `{report}`. **2. Subscribe to the report** Subscribe to `reports/operations/hourly` in MQTT Explorer, a dashboard, or the HUB Data Viewer. **3. See the first report** Publish sample payloads under your data tree (`telemetry/`, `solar/site1/power`, `buildings/tower/hvac`, and so on), then wait for the next run — or temporarily use `ON EVERY 30 SECONDS` and change it back when done. > [!TIP] > > **Scheduled insight on autopilot** — the report speaks your operation's language, not broker diagnostics. **Alert triage** **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":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} DEFINE CONNECTOR SiteModel WITH TYPE LLM ADD LLM_CONFIG WITH PROVIDER "openai" WITH MODEL "gpt-6-luna" WITH API_KEY GET SECRET "OPENAI_API_KEY" WITH API_SURFACE "auto" DEFINE AGENT AlertAnalyst USES LLM CONNECTOR SiteModel WITH DESCRIPTION "Explains one alert in three sentences" WITH DEFAULT_USER "operator" WITH SYSTEM_PROMPT "You triage operational alerts from MQTT. In 3 short sentences: what happened, likely cause, recommended first check. Be direct." WITH MEMORY "none" ADD TOOL "read_mqtt" WITH TOPICS "alerts/#" DEFINE ACTION TriageAlert ON TOPIC "alerts/#" DO CALL AGENT "AlertAnalyst.execute" WITH (task = "Alert on topic " + TOPIC + ": " + PAYLOAD, session_id = TOPIC) RETURN AS {triage} PUBLISH TOPIC "agents/site/alerts/analysis" WITH {triage} ``` **1. Wire your alerts** Ensure your existing logic still publishes under `alerts/` — for example `alerts/inverter/fault`, `alerts/building/leak`, or `alerts/line2/quality`. **2. Deploy triage** Add **AlertAnalyst** and **TriageAlert**. Subscribe to `agents/site/alerts/analysis`. **3. Trigger once** Publish a test alert to `alerts/test/manual` with any message. Read the analysis on the triage topic. `session_id` is the alert topic, so a repeat of the same topic continues that conversation. Drop `session_id` when every alert must stand alone — `MEMORY "none"` already does that. > [!TIP] > > On-call gets **context with the alarm**, not only a raw payload. **Shift handover** **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":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} DEFINE CONNECTOR SiteModel WITH TYPE LLM ADD LLM_CONFIG WITH PROVIDER "openai" WITH MODEL "gpt-6-luna" WITH API_KEY GET SECRET "OPENAI_API_KEY" WITH API_SURFACE "auto" DEFINE AGENT HandoverBrief USES LLM CONNECTOR SiteModel WITH DESCRIPTION "Writes the shift handover" WITH DEFAULT_USER "operator" WITH SYSTEM_PROMPT "You write handover notes for operations teams. Bullet points: status, anomalies, open items. Max 8 bullets." WITH MEMORY "none" ADD TOOL "read_mqtt" WITH TOPICS "telemetry/#, alerts/#" 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.") RETURN AS {brief} PUBLISH TOPIC "agents/site/shift/handover" WITH {brief} ``` **1. Deploy** Add **HandoverBrief** and **ShiftHandover**. Subscribe to `agents/site/shift/handover`. **2. Test faster** Temporarily use `ON EVERY 2 MINUTES`, read one brief, then restore `ON EVERY 8 HOURS` for production. > [!TIP] > > **Crew change becomes a story**, not a scramble through dashboards. > [!TIP] > > **Starter pack:** **SiteGuide** + **HourlyOperationsReport** + **AlertAnalyst** — ask your deployment, get scheduled insight, enrich every alarm. They can share one `SiteModel` connector. Rename topic grants to `production/`, `solar/`, or `buildings/` when you copy the prompts. *** ## When You're Ready to Grow You already have the building blocks: **chat** (`{INTERACTION_TOPIC}//ask`), **schedules** (`ON EVERY`), and **event-driven** (`ON TOPIC`) automations with `CALL AGENT`. Next is a writing grant (`ADD TOOL "write_mqtt"` with `WITH LEVEL "write"`), peers the agent may call (`WITH AGENTS`), and [MCP Connectors](/latest/lot-language/routes/mcp-routes) called from Actions. For several agents on one broker, see [Multi-agent deployments](/latest/lot-language/agents/syntax#delegation-with-agents). If you still have a `TYPE AGENT` connector from an earlier release, rewrite it before you upgrade. The broker skips those connectors at load. See [Coming from TYPE AGENT](/latest/lot-language/agents/overview#coming-from-type-agent). *** ## Stay Safe While You Experiment > **Try read-only first** > > Grant only `read_mqtt` and `read_lot` until you trust the prompt. For one question, publish JSON `{ "text": "…", "mode": "insight" }` on the ask topic. Insight mode refuses every writing tool, including in agents this one calls. A default of insight mode on the agent itself is not available yet. > **Keep API keys off the clipboard in LoT files** > > Always use `GET SECRET "OPENAI_API_KEY"` on the `TYPE LLM` connector — never paste keys into definitions that get saved or shared. > **Match the agent to the audience** > > An interaction topic is for people. Timed Actions are for machines. Leave `INTERACTION_TOPIC` off agents that should only run in the background. > **Say who the agent is** > > `DEFAULT_USER` is required. Pick a user that can do the job, and grant `ADD TOOL` only the topics that job needs. A writing tool also needs `WITH LEVEL "write"`. *** ## What to Explore Next > **[AI Connectors and Agents](/latest/lot-language/routes/ai-routes)** > > Providers, tool grants, delegation, topics, and the move off TYPE AGENT. > **[Using AI with Coreflux](/latest/quick-start/using-ai)** > > 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. ![Coreflux HUB overview screen with a 'Start here' arrow pointing to the first navigation icon in the bottom toolbar](https://mintcdn.com/coreflux/7tBTxEEkDVX4IRGH/images/overview/hub-overview-start.png?fit=max\&auto=format\&n=7tBTxEEkDVX4IRGH\&q=85\&s=bab525c05994ccb1c98530f1b50f40e1) Coreflux HUB overview — start here ## 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). > [!TIP] > > **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
_Input` with `RESTHeader` and `InputParameters`, and each row is a `
_REC` object. PUT updates rows that match a `Filter` inside an `Update` object. For PL/SQL services the root is `_Input`. Take the element names from the service's WADL and XSD. * **`method`**, **`view`**, **`irepclass`**, **`params`**: the resource segment of the URL where the event leaves it open, for example `SUBMIT_CP_` on `ConcurrentProgramSubmit`, the deployed view name on `WipWorkOrdersGet`, `PLSQL:FND_PROFILE` on `GetRestInterface`. Material transaction (a row the Inventory Transaction Manager picks up when `PROCESS_FLAG` is 1, `LOCK_FLAG` is 2 and `TRANSACTION_MODE` is 3): ```json theme={"theme":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} {"cookie":"=","alias":"","body":"{\"MTL_TRANSACTIONS_INTERFACE_Input\":{\"RESTHeader\":{\"Responsibility\":\"\",\"RespApplication\":\"INV\",\"SecurityGroup\":\"STANDARD\",\"NLSLanguage\":\"AMERICAN\",\"Org_Id\":\"\"},\"InputParameters\":{\"MTL_TRANSACTIONS_INTERFACE_REC\":{\"SOURCE_CODE\":\"COREFLUX\",\"SOURCE_LINE_ID\":\"1\",\"SOURCE_HEADER_ID\":\"1\",\"PROCESS_FLAG\":\"1\",\"LOCK_FLAG\":\"2\",\"TRANSACTION_MODE\":\"3\",\"ORGANIZATION_ID\":\"\",\"ITEM_SEGMENT1\":\"\",\"TRANSACTION_QUANTITY\":\"-1\",\"TRANSACTION_UOM\":\"\",\"TRANSACTION_DATE\":\"2026-10-05\",\"SUBINVENTORY_CODE\":\"\",\"TRANSACTION_TYPE_ID\":\"\"}}}}"} ``` Failed inventory rows: ```json theme={"theme":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} {"cookie":"=","alias":"","query":"filter=PROCESS_FLAG==3&select=TRANSACTION_INTERFACE_ID,SOURCE_CODE,ERROR_CODE,ERROR_EXPLANATION&limit=50"} ``` Job requests use `WIP_JOB_SCHEDULE_INTERFACE` columns such as `GROUP_ID`, `LOAD_TYPE`, `ORGANIZATION_CODE`, `JOB_NAME`, `START_QUANTITY`, `STATUS_TYPE`, `FIRST_UNIT_START_DATE` and `LAST_UNIT_COMPLETION_DATE`; WIP Mass Load reports `PROCESS_PHASE` and `PROCESS_STATUS`. Quality rows use `QA_RESULTS_INTERFACE` columns such as `ORGANIZATION_CODE`, `PLAN_NAME`, `INSERT_TYPE` (1 insert, 2 update with `MATCHING_ELEMENTS`), `PROCESS_STATUS`, `SOURCE_CODE` and the plan's `CHARACTERx` columns. ## Pacing and errors * Faults come back as `ISGServiceFault` with `Code`, `Message`, `Resolution` and `ServiceDetails`. Open interface writes return `OutputParameters.Summary` (`SuccessCount`, `ErrorCount`) and a `Status` and `Message` for each row. * Interface rows are only staged. The Inventory Transaction Manager, WIP Mass Load and Collection Import move them to base tables later, so read the interface back (`PROCESS_FLAG`, `PROCESS_STATUS`, `ERROR_EXPLANATION`) before you treat a transaction as posted. * Open interface GETs page with `offset` and `limit` (the documented sample response reports `Limit` 200). Keep `limit` small and poll on a coarse schedule. * Long ISG calls can hit web tier timeouts. The template uses a 120-second request timeout. ## Example actions These log in every 20 minutes, keep the session cookie on a topic, and read failed inventory interface rows every hour. ```lot theme={"theme":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} # Example actions for the Oracle E-Business Suite routes (docs page). Not part of the route template. # Secret: EBS_BASIC_AUTH = "Basic " for an EBS user with grants on the deployed REST methods. # The login response carries data.accessTokenName and data.accessToken; the cookie is "=". # Inline JSON object literals are not supported when publishing to a topic, so requests use COLLAPSED models sent # with PUBLISH MODEL. "invtxn" is an example alias: use the alias your administrator gave the inventory interface. DEFINE MODEL EbsLoginRequest COLLAPSED ADD STRING "basic" DEFINE MODEL EbsQueryRequest COLLAPSED ADD STRING "cookie" ADD STRING "alias" ADD STRING "query" DEFINE ACTION EbsLoginEvery20Minutes ON EVERY 20 MINUTES DO PUBLISH MODEL EbsLoginRequest TO "erp/oracle-ebs/auth/login" WITH basic = (GET SECRET "EBS_BASIC_AUTH") DEFINE ACTION EbsKeepCookie ON TOPIC "erp/oracle-ebs/auth/login/response" DO SET "name" WITH GET JSON "data.accessTokenName" IN PAYLOAD AS STRING SET "token" WITH GET JSON "data.accessToken" IN PAYLOAD AS STRING KEEP TOPIC "erp/oracle-ebs/state/cookie" WITH {name} + "=" + {token} DEFINE ACTION EbsFailedInventoryTransactionsEveryHour ON EVERY 1 HOUR DO SET "cookie" WITH GET TOPIC "erp/oracle-ebs/state/cookie" AS STRING PUBLISH MODEL EbsQueryRequest TO "erp/oracle-ebs/inventory/transactions/get" WITH cookie = {cookie} alias = "invtxn" query = "filter=PROCESS_FLAG==3&select=TRANSACTION_INTERFACE_ID,SOURCE_CODE,ERROR_CODE,ERROR_EXPLANATION&limit=50" ``` ## Connector template ```lot theme={"theme":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} # Coreflux REST_API template — Oracle E-Business Suite 12.2 (Integrated SOA Gateway REST services) for plants: # post inventory material transactions (+ lots) through the Material Transactions Interface, create / update # discrete jobs through the WIP Job Schedule (Work Order) Interface, read work orders through a deployed Interface # View, import quality results through the Quality Results (Collection Import) Interface, read back interface rows # and their errors, submit the associated concurrent programs, and call any other deployed ISG REST service. # Guide 12.2 (E20927: REST messages, WADL, PL/SQL / Java Bean / Open Interface REST services), Implementation Guide # (E20925: REST security, deploying REST services), User's Guide (E20923: Integration Repository), Mobile Apps # Developer's Guide (E69284: login / initialize / logout services), Oracle eTRM 12.2 table definitions (via etrm.live). # interface from the Integration Repository by your integration administrator and described by its own WADL, so # events are hand-curated from the documented URL patterns. # Docs: https://docs.coreflux.org/latest/lot-language/routes/rest-api-apps/oracle-e-business-suite # # One route, topic prefix erp/oracle-ebs/: # OracleEbsIsg https://ebs.example.com (your EBS web entry point, add :port; all paths start /webservices/rest/) # URLs: /webservices/rest/// . The alias is chosen when the interface is deployed (REST Web # Service tab, Integration Repository); the resource is the method name for PL/SQL / Java services, the table or # view name for open interfaces, and SUBMIT_CP_ for the interface's program. # Pass the alias in every payload ("alias"). The WADL for a deployed service is /webservices/rest//?WADL. # Auth: HTTP Basic or token-based (both must be selected as REST security when the service is deployed). # GET /webservices/rest/login with "Authorization: Basic base64(user:password)" returns accessTokenName + # accessToken (EBS session id, also in Set-Cookie). Send "Cookie: =" on later # calls (payload field "cookie"); /webservices/rest/logout ends the session. Keep the Basic value in broker secret # EBS_BASIC_AUTH and pass it with GET SECRET. EBS 12.2 needs profile "FND: Authn Service Token Scope" = Header and # Body for the token in the login body (Oracle Doc ID 2938671.1). The user needs a grant on each deployed method. # App context: pass Responsibility / RespApplication / SecurityGroup / NLSLanguage / Org_Id in "RESTHeader" in POST / # PUT bodies, as ctx_responsibility / ctx_respapplication / ctx_securitygroup / ctx_nlslanguage / ctx_orgid query # parameters on GET, or once per session with POST /webservices/rest/initialize. # Open interface GET query: select=COL1,COL2&filter=COL==value;COL2=le=100&sort=...&offset=0&limit=200 (URL-encoded). # Errors: JSON / XML ISGServiceFault {Code, Message, Resolution, ServiceDetails}; per-row Status / Message in # OutputParameters.Result.Output for open interface writes. # Writes (# WRITE): rows inserted / updated in interface tables are only staged until the interface program # (Inventory Transaction Manager, WIP Mass Load, Collection Import Manager) moves them to base tables. # Not packaged: DELETE on interface tables, SOAP services, business events, XML Gateway, Oracle Integration EBS adapter. # # Payload fields (JSON on the source topic): cookie on every event except Login; alias on every service event; # query (URL-encoded GET query, or "" for none); body (JSON string) on POST / PUT; method / view / irepclass / # params where the endpoint names them. # Login {"basic":"Basic "} # InventoryTransactionsCreate {"cookie":"demo=...","alias":"invtxn","body":"{\"MTL_TRANSACTIONS_INTERFACE_Input\":{...}}"} # InventoryTransactionsGet {"cookie":"demo=...","alias":"invtxn","query":"filter=PROCESS_FLAG==3&select=TRANSACTION_INTERFACE_ID,ERROR_CODE,ERROR_EXPLANATION&limit=50"} # WipWorkOrdersGet {"cookie":"demo=...","alias":"wipjobs","view":"","query":"filter=ORGANIZATION_ID==207&limit=50"} # ConcurrentProgramSubmit {"cookie":"demo=...","alias":"wipjobs","method":"SUBMIT_CP_","body":"{...}"} DEFINE CONNECTOR OracleEbsIsg WITH TYPE REST_API ADD REST_API_CONFIG WITH BASE_ADDRESS "https://ebs.example.com" WITH ENABLE_CLIENT "true" WITH USE_SSL "true" WITH REQUEST_TIMEOUT '120' WITH DEFAULT_CONTENT_TYPE "application/json" # Replace with your EBS web entry point (https://:). Long ISG calls may need a higher timeout. # AOL security Login service: HTTP Basic login. Returns data.accessTokenName + data.accessToken (EBS session id, also in Set-Cookie). Build cookie = accessTokenName=accessToken. ADD EVENT Login WITH SOURCE_TOPIC "erp/oracle-ebs/auth/login" WITH DESTINATION_TOPIC "erp/oracle-ebs/auth/login/response" WITH METHOD "GET" WITH ENDPOINT "/webservices/rest/login" WITH HEADERS ADD "Accept" "application/json" ADD "User-Agent" "Coreflux-Broker REST_API route (https://www.coreflux.org)" ADD "Authorization" "{value.json.basic}" # AOL security Initialize service: Optional. Sets responsibility / security group / org context for the session. body (XML): ....STANDARD.. ADD EVENT InitializeSession WITH SOURCE_TOPIC "erp/oracle-ebs/auth/initialize" WITH DESTINATION_TOPIC "erp/oracle-ebs/auth/initialize/response" WITH METHOD "POST" WITH ENDPOINT "/webservices/rest/initialize" WITH HEADERS ADD "Accept" "application/json" ADD "User-Agent" "Coreflux-Broker REST_API route (https://www.coreflux.org)" ADD "Cookie" "{value.json.cookie}" ADD "Content-Type" "application/xml" WITH BODY "{value.json.body}" # AOL security Logout service: Invalidates the EBS session behind the cookie (response accessToken -1). ADD EVENT Logout WITH SOURCE_TOPIC "erp/oracle-ebs/auth/logout" WITH DESTINATION_TOPIC "erp/oracle-ebs/auth/logout/response" WITH METHOD "GET" WITH ENDPOINT "/webservices/rest/logout" WITH HEADERS ADD "Accept" "application/json" ADD "User-Agent" "Coreflux-Broker REST_API route (https://www.coreflux.org)" ADD "Cookie" "{value.json.cookie}" # Deployed REST service: WADL of a deployed service (resources, methods, XSD links). Use it to check paths and payload element names. ADD EVENT GetServiceWadl WITH SOURCE_TOPIC "erp/oracle-ebs/service/wadl/get" WITH DESTINATION_TOPIC "erp/oracle-ebs/service/wadl/get/response" WITH METHOD "GET" WITH ENDPOINT "/webservices/rest/{value.json.alias}/?WADL" WITH HEADERS ADD "Accept" "application/json" ADD "User-Agent" "Coreflux-Broker REST_API route (https://www.coreflux.org)" ADD "Cookie" "{value.json.cookie}" # REST Service Locator (Java Bean Service oracle.apps.fnd.rep.ws.service.EbsRestLocator): Details of a deployed REST interface by internal name, for example PLSQL:FND_PROFILE. Needs the locator deployed (doc alias servicelocator). ADD EVENT GetRestInterface WITH SOURCE_TOPIC "erp/oracle-ebs/service/interface/get" WITH DESTINATION_TOPIC "erp/oracle-ebs/service/interface/get/response" WITH METHOD "GET" WITH ENDPOINT "/webservices/rest/{value.json.alias}/getRestInterface/{value.json.irepclass}/" WITH HEADERS ADD "Accept" "application/json" ADD "User-Agent" "Coreflux-Broker REST_API route (https://www.coreflux.org)" ADD "Cookie" "{value.json.cookie}" # Open interface: Material Transactions Interface (MTL_TRANSACTIONS_INTERFACE): WRITE: insert material transactions (issues, receipts, transfers, WIP completions). Rows with PROCESS_FLAG 1, LOCK_FLAG 2, TRANSACTION_MODE 3 are picked up by the Inventory Transaction Manager. ADD EVENT InventoryTransactionsCreate WITH SOURCE_TOPIC "erp/oracle-ebs/inventory/transactions/create" WITH DESTINATION_TOPIC "erp/oracle-ebs/inventory/transactions/create/response" WITH METHOD "POST" WITH ENDPOINT "/webservices/rest/{value.json.alias}/MTL_TRANSACTIONS_INTERFACE/" WITH HEADERS ADD "Accept" "application/json" ADD "User-Agent" "Coreflux-Broker REST_API route (https://www.coreflux.org)" ADD "Cookie" "{value.json.cookie}" ADD "Content-Type" "application/json" WITH BODY "{value.json.body}" # Open interface: Material Transactions Interface (MTL_TRANSACTIONS_INTERFACE): Read interface rows not yet processed or failed (PROCESS_FLAG 3 + ERROR_CODE / ERROR_EXPLANATION). query: select / filter / sort / offset / limit. ADD EVENT InventoryTransactionsGet WITH SOURCE_TOPIC "erp/oracle-ebs/inventory/transactions/get" WITH DESTINATION_TOPIC "erp/oracle-ebs/inventory/transactions/get/response" WITH METHOD "GET" WITH ENDPOINT "/webservices/rest/{value.json.alias}/MTL_TRANSACTIONS_INTERFACE/?{value.json.query}" WITH HEADERS ADD "Accept" "application/json" ADD "User-Agent" "Coreflux-Broker REST_API route (https://www.coreflux.org)" ADD "Cookie" "{value.json.cookie}" # Open interface: Material Transactions Interface (MTL_TRANSACTIONS_INTERFACE): WRITE: update interface rows by Filter, for example reset failed rows to PROCESS_FLAG 1 / LOCK_FLAG 2 after fixing data. ADD EVENT InventoryTransactionsUpdate WITH SOURCE_TOPIC "erp/oracle-ebs/inventory/transactions/update" WITH DESTINATION_TOPIC "erp/oracle-ebs/inventory/transactions/update/response" WITH METHOD "PUT" WITH ENDPOINT "/webservices/rest/{value.json.alias}/MTL_TRANSACTIONS_INTERFACE/" WITH HEADERS ADD "Accept" "application/json" ADD "User-Agent" "Coreflux-Broker REST_API route (https://www.coreflux.org)" ADD "Cookie" "{value.json.cookie}" ADD "Content-Type" "application/json" WITH BODY "{value.json.body}" # Open interface: Material Transactions Interface lots (MTL_TRANSACTION_LOTS_INTERFACE): WRITE: lot rows for lot-controlled items, linked by TRANSACTION_INTERFACE_ID. Only if the table is a method of your deployed inventory interface. ADD EVENT InventoryTransactionLotsCreate WITH SOURCE_TOPIC "erp/oracle-ebs/inventory/transaction-lots/create" WITH DESTINATION_TOPIC "erp/oracle-ebs/inventory/transaction-lots/create/response" WITH METHOD "POST" WITH ENDPOINT "/webservices/rest/{value.json.alias}/MTL_TRANSACTION_LOTS_INTERFACE/" WITH HEADERS ADD "Accept" "application/json" ADD "User-Agent" "Coreflux-Broker REST_API route (https://www.coreflux.org)" ADD "Cookie" "{value.json.cookie}" ADD "Content-Type" "application/json" WITH BODY "{value.json.body}" # Open interface: WIP Job Schedule (Work Order) Interface (WIP_JOB_SCHEDULE_INTERFACE): WRITE: requests to create or update discrete jobs / repetitive schedules (LOAD_TYPE, GROUP_ID, JOB_NAME, START_QUANTITY, dates). WIP Mass Load processes them. ADD EVENT WipJobRequestsCreate WITH SOURCE_TOPIC "erp/oracle-ebs/wip/job-requests/create" WITH DESTINATION_TOPIC "erp/oracle-ebs/wip/job-requests/create/response" WITH METHOD "POST" WITH ENDPOINT "/webservices/rest/{value.json.alias}/WIP_JOB_SCHEDULE_INTERFACE/" WITH HEADERS ADD "Accept" "application/json" ADD "User-Agent" "Coreflux-Broker REST_API route (https://www.coreflux.org)" ADD "Cookie" "{value.json.cookie}" ADD "Content-Type" "application/json" WITH BODY "{value.json.body}" # Open interface: WIP Job Schedule (Work Order) Interface (WIP_JOB_SCHEDULE_INTERFACE): Read load status of job requests (PROCESS_PHASE, PROCESS_STATUS, WIP_ENTITY_ID) by GROUP_ID / JOB_NAME. ADD EVENT WipJobRequestsGet WITH SOURCE_TOPIC "erp/oracle-ebs/wip/job-requests/get" WITH DESTINATION_TOPIC "erp/oracle-ebs/wip/job-requests/get/response" WITH METHOD "GET" WITH ENDPOINT "/webservices/rest/{value.json.alias}/WIP_JOB_SCHEDULE_INTERFACE/?{value.json.query}" WITH HEADERS ADD "Accept" "application/json" ADD "User-Agent" "Coreflux-Broker REST_API route (https://www.coreflux.org)" ADD "Cookie" "{value.json.cookie}" # Open interface view (GET only): Read work orders through the Interface View your administrator deployed (view = the view name shown as the resource in its WADL). query: select / filter / sort / offset / limit. ADD EVENT WipWorkOrdersGet WITH SOURCE_TOPIC "erp/oracle-ebs/wip/work-orders/get" WITH DESTINATION_TOPIC "erp/oracle-ebs/wip/work-orders/get/response" WITH METHOD "GET" WITH ENDPOINT "/webservices/rest/{value.json.alias}/{value.json.view}/?{value.json.query}" WITH HEADERS ADD "Accept" "application/json" ADD "User-Agent" "Coreflux-Broker REST_API route (https://www.coreflux.org)" ADD "Cookie" "{value.json.cookie}" # Open interface: Quality Results Interface (QA_RESULTS_INTERFACE, collection import): WRITE: quality results for a collection plan (ORGANIZATION_CODE, PLAN_NAME, INSERT_TYPE 1 insert / 2 update + MATCHING_ELEMENTS, CHARACTERx values). Collection Import moves them to QA_RESULTS. ADD EVENT QualityResultsCreate WITH SOURCE_TOPIC "erp/oracle-ebs/quality/results/create" WITH DESTINATION_TOPIC "erp/oracle-ebs/quality/results/create/response" WITH METHOD "POST" WITH ENDPOINT "/webservices/rest/{value.json.alias}/QA_RESULTS_INTERFACE/" WITH HEADERS ADD "Accept" "application/json" ADD "User-Agent" "Coreflux-Broker REST_API route (https://www.coreflux.org)" ADD "Cookie" "{value.json.cookie}" ADD "Content-Type" "application/json" WITH BODY "{value.json.body}" # Open interface: Quality Results Interface (QA_RESULTS_INTERFACE, collection import): Read import status of quality rows (PROCESS_STATUS, GROUP_ID) by PLAN_NAME / SOURCE_CODE. ADD EVENT QualityResultsGet WITH SOURCE_TOPIC "erp/oracle-ebs/quality/results/get" WITH DESTINATION_TOPIC "erp/oracle-ebs/quality/results/get/response" WITH METHOD "GET" WITH ENDPOINT "/webservices/rest/{value.json.alias}/QA_RESULTS_INTERFACE/?{value.json.query}" WITH HEADERS ADD "Accept" "application/json" ADD "User-Agent" "Coreflux-Broker REST_API route (https://www.coreflux.org)" ADD "Cookie" "{value.json.cookie}" # Open interface / concurrent program REST service: WRITE: run the interface's concurrent program (method SUBMIT_CP_, POST only), for example the import program of a deployed open interface. ADD EVENT ConcurrentProgramSubmit WITH SOURCE_TOPIC "erp/oracle-ebs/concurrent-program/submit" WITH DESTINATION_TOPIC "erp/oracle-ebs/concurrent-program/submit/response" WITH METHOD "POST" WITH ENDPOINT "/webservices/rest/{value.json.alias}/{value.json.method}/" WITH HEADERS ADD "Accept" "application/json" ADD "User-Agent" "Coreflux-Broker REST_API route (https://www.coreflux.org)" ADD "Cookie" "{value.json.cookie}" ADD "Content-Type" "application/json" WITH BODY "{value.json.body}" # Any deployed PL/SQL, Java Bean, Application Module or Business Service Object REST service: WRITE (may write): POST {_Input:{RESTHeader:{...},InputParameters:{...}}} to a deployed method, for example a PL/SQL public API. ADD EVENT ServicePost WITH SOURCE_TOPIC "erp/oracle-ebs/service/post" WITH DESTINATION_TOPIC "erp/oracle-ebs/service/post/response" WITH METHOD "POST" WITH ENDPOINT "/webservices/rest/{value.json.alias}/{value.json.method}/" WITH HEADERS ADD "Accept" "application/json" ADD "User-Agent" "Coreflux-Broker REST_API route (https://www.coreflux.org)" ADD "Cookie" "{value.json.cookie}" ADD "Content-Type" "application/json" WITH BODY "{value.json.body}" # Any deployed REST service with a GET method: GET a deployed method. params: path variables + query, for example PLSQL:FND_PROFILE/ or ?ctx_orgid=204&offset=0&limit=5. ADD EVENT ServiceGet WITH SOURCE_TOPIC "erp/oracle-ebs/service/get" WITH DESTINATION_TOPIC "erp/oracle-ebs/service/get/response" WITH METHOD "GET" WITH ENDPOINT "/webservices/rest/{value.json.alias}/{value.json.method}/{value.json.params}" WITH HEADERS ADD "Accept" "application/json" ADD "User-Agent" "Coreflux-Broker REST_API route (https://www.coreflux.org)" ADD "Cookie" "{value.json.cookie}" ``` ## Not packaged * DELETE on interface tables, SOAP services, business events, XML Gateway messages and the Oracle Integration (OIC) E-Business Suite adapter. * Fixed paths for PL/SQL public APIs: deploy the API you need and call it with `ServicePost` / `ServiceGet`. # Oracle Fusion Cloud ERP and SCM Source: https://docs.coreflux.org/latest/lot-language/routes/rest-api-apps/oracle-fusion-erp LoT connector for Oracle Fusion Cloud SCM: manufacturing work orders, operation completions, on-hand inventory balances and maintenance work orders Technical Preview ## Overview A client-mode connector for a **curated manufacturing, inventory, and maintenance** slice of [Oracle Fusion Cloud SCM REST APIs](https://docs.oracle.com/en/cloud/saas/supply-chain-and-manufacturing/26b/fasrp/index.html) under `/fscmRestApi/resources/latest` (or a fixed version such as `11.13.18.05`): | Catalog connector event | Entity / path | Notes | | - | - | - | | GET workOrders | `workOrders` | Manufacturing work orders | | POST workOrderOperationTransactions (completion) | `operationTransactions` | Official resource name; dispatch-state / completion | | GET inventoryOnhandBalances | `inventoryOnhandBalances` | On-hand by org / subinventory / locator | | POST maintenanceWorkOrders | `maintenanceWorkOrders` | Create maintenance work orders | `BASE_ADDRESS` is the Fusion SCM resources root (`https://{server}.fa.{region}.oraclecloud.com/fscmRestApi/resources/latest`). Topics use `erp/oracle-fusion-erp//`. > [!NOTE] > > Fusion REST APIs require a real tenant and either Basic credentials or an OAuth / JWT Bearer token. Prefer Hub secrets for passwords and tokens. Append `/describe?metadata-format=openapi` on a resource to pull tenant OpenAPI. ## Basic Syntax ```lot wrap theme={"theme":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} # Coreflux REST_API template — Oracle Fusion Cloud ERP and SCM (curated MFG / inventory / maintenance slice) # Auth: Bearer {value.json.access_token} OR Basic WITH USERNAME / WITH PASSWORD GET SECRET # BASE_ADDRESS: https://YOUR_SERVER.fa.YOUR_REGION.oraclecloud.com/fscmRestApi/resources/latest DEFINE CONNECTOR OracleFusionErpRestApi WITH TYPE REST_API ADD REST_API_CONFIG WITH BASE_ADDRESS "https://YOUR_SERVER.fa.YOUR_REGION.oraclecloud.com/fscmRestApi/resources/latest" WITH ENABLE_CLIENT "true" WITH USE_SSL "true" WITH REQUEST_TIMEOUT '60' WITH DEFAULT_CONTENT_TYPE "application/json" ADD EVENT Getworkorders WITH SOURCE_TOPIC "erp/oracle-fusion-erp/work-orders/get" WITH DESTINATION_TOPIC "erp/oracle-fusion-erp/work-orders/get/response" WITH METHOD "GET" WITH ENDPOINT "/workOrders?limit={value.json.limit}&onlyData=true&fields=WorkOrderId,WorkOrderNumber,OrganizationCode,WorkOrderStatusCode,ItemNumber,PlannedStartQuantity,PlannedStartDate" WITH HEADERS ADD "Accept" "application/json" ADD "Content-Type" "application/vnd.oracle.adf.resourceitem+json" ADD "REST-Framework-Version" "4" ADD "Authorization" "Bearer {value.json.access_token}" ADD EVENT Createworkorderoperationtransaction WITH SOURCE_TOPIC "erp/oracle-fusion-erp/operation-transactions/create" WITH DESTINATION_TOPIC "erp/oracle-fusion-erp/operation-transactions/create/response" WITH METHOD "POST" WITH ENDPOINT "/operationTransactions" WITH HEADERS ADD "Accept" "application/json" ADD "Content-Type" "application/vnd.oracle.adf.resourceitem+json" ADD "REST-Framework-Version" "4" ADD "Authorization" "Bearer {value.json.access_token}" WITH BODY "{payload}" ADD EVENT Getinventoryonhandbalances WITH SOURCE_TOPIC "erp/oracle-fusion-erp/inventory-onhand/get" WITH DESTINATION_TOPIC "erp/oracle-fusion-erp/inventory-onhand/get/response" WITH METHOD "GET" WITH ENDPOINT "/inventoryOnhandBalances?limit={value.json.limit}&onlyData=true&fields=OrganizationCode,ItemNumber,SubinventoryCode,PrimaryQuantity" WITH HEADERS ADD "Accept" "application/json" ADD "Content-Type" "application/vnd.oracle.adf.resourceitem+json" ADD "REST-Framework-Version" "4" ADD "Authorization" "Bearer {value.json.access_token}" ADD EVENT Createmaintenanceworkorder WITH SOURCE_TOPIC "erp/oracle-fusion-erp/maintenance-work-orders/create" WITH DESTINATION_TOPIC "erp/oracle-fusion-erp/maintenance-work-orders/create/response" WITH METHOD "POST" WITH ENDPOINT "/maintenanceWorkOrders" WITH HEADERS ADD "Accept" "application/json" ADD "Content-Type" "application/vnd.oracle.adf.resourceitem+json" ADD "REST-Framework-Version" "4" ADD "Authorization" "Bearer {value.json.access_token}" WITH BODY "{payload}" ``` ## Events | Event | Method | Endpoint | Request topic | Response topic | | - | - | - | - | - | | Getworkorders | GET | `/workOrders` | `erp/oracle-fusion-erp/work-orders/get` | `erp/oracle-fusion-erp/work-orders/get/response` | | Createworkorderoperationtransaction | POST | `/operationTransactions` | `erp/oracle-fusion-erp/operation-transactions/create` | `erp/oracle-fusion-erp/operation-transactions/create/response` | | Getinventoryonhandbalances | GET | `/inventoryOnhandBalances` | `erp/oracle-fusion-erp/inventory-onhand/get` | `erp/oracle-fusion-erp/inventory-onhand/get/response` | | Createmaintenanceworkorder | POST | `/maintenanceWorkOrders` | `erp/oracle-fusion-erp/maintenance-work-orders/create` | `erp/oracle-fusion-erp/maintenance-work-orders/create/response` | ## Auth **Option A — OAuth / JWT Bearer (default in the template):** Pass the access token on the MQTT request payload: ```json theme={"theme":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} {"access_token": "YOUR_FUSION_ACCESS_TOKEN", "limit": 10} ``` The connector sends `Authorization: Bearer {value.json.access_token}`. Obtain the token via OCI IAM JWT user assertion for Fusion Applications, or Fusion's token-relay endpoint after SSO. **Option B — Basic over SSL:** In `REST_API_CONFIG`, uncomment: ```lot theme={"theme":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} WITH USERNAME "YOUR_FUSION_USER" WITH PASSWORD GET SECRET "ORACLE_FUSION_PASSWORD" ``` Prefer Hub secrets / `GET SECRET`. Never commit real passwords or tokens. ## Example requests ### List manufacturing work orders Publish to `erp/oracle-fusion-erp/work-orders/get`: ```json theme={"theme":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} {"access_token": "YOUR_FUSION_ACCESS_TOKEN", "limit": 10} ``` ### Report operation completion Publish to `erp/oracle-fusion-erp/operation-transactions/create` with a body shaped from your tenant `/operationTransactions/describe`. Minimal illustrative shape: ```json theme={"theme":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} { "access_token": "YOUR_FUSION_ACCESS_TOKEN", "payload": { "SourceSystemCode": "EXTERNAL", "OperationTransactionDetail": [ { "SourceSystemCode": "EXTERNAL", "OrganizationCode": "M1", "WorkOrderNumber": "WO-PLACEHOLDER", "WoOperationSequenceNumber": 10, "FromDispatchState": "READY", "ToDispatchState": "COMPLETE", "TransactionDate": "2026-10-03T12:00:00+00:00", "TransactionQuantity": 1 } ] } } ``` ### Inventory on-hand Publish to `erp/oracle-fusion-erp/inventory-onhand/get`: ```json theme={"theme":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} {"access_token": "YOUR_FUSION_ACCESS_TOKEN", "limit": 25} ``` ### Create maintenance work order Publish to `erp/oracle-fusion-erp/maintenance-work-orders/create` with a body from `/maintenanceWorkOrders/describe`. ## Configuration notes | Setting | Value | | - | - | | `BASE_ADDRESS` | `https://{server}.fa.{region}.oraclecloud.com/fscmRestApi/resources/latest` | | SSL | Required (`USE_SSL true`) | | Content-Type | `application/vnd.oracle.adf.resourceitem+json` (ADF REST) | | Version alias | `latest` or fixed `11.13.18.05` | | Status | Technical preview | ## Related * [REST API Connectors](/latest/lot-language/routes/rest-api-routes) * [REST API Apps catalog](/latest/lot-language/routes/rest-api-apps) * [Oracle Fusion Cloud SCM REST API](https://docs.oracle.com/en/cloud/saas/supply-chain-and-manufacturing/26b/fasrp/index.html) * [Applications Common Quick Start (auth)](https://docs.oracle.com/en/cloud/saas/applications-common/26c/farca/Quick_Start.html) # Oracle JD Edwards EnterpriseOne Source: https://docs.coreflux.org/latest/lot-language/routes/rest-api-apps/oracle-jd-edwards Client-mode REST API connector for the JD Edwards EnterpriseOne AIS Server: token login and validation plus Orchestrator calls and data service queries Technical Preview ## Overview Client-mode REST API connector for the JD Edwards EnterpriseOne AIS Server: token login and validation plus Orchestrator calls and data service queries. `BASE_ADDRESS` defaults to `https://YOUR_AIS_SERVER:PORT`. Topics use `erp/jd-edwards//`. | Event | Request | Source topic | | - | - | - | | `JdeLogin` | `POST /jderest/v3/orchestrator/jde-login` | `erp/jd-edwards/auth/login` | | `JdeValidateSession` | `GET /jderest/v3/orchestrator/jde-validate-session` | `erp/jd-edwards/auth/validate` | | `JdeLogout` | `POST /jderest/v3/orchestrator/jde-logout` | `erp/jd-edwards/auth/logout` | | `OrchestrationRun` | `POST /jderest/v3/orchestrator/{value.json.orchestration}` | `erp/jd-edwards/orchestration/run` | | `OrchestrationGet` | `GET /jderest/v3/orchestrator/{value.json.orchestration}?{value.json.query}` | `erp/jd-edwards/orchestration/get` | | `DataServiceQuery` | `POST /jderest/v2/dataservice` | `erp/jd-edwards/dataservice/query` | ## Authentication AIS token. JdeLogin sends HTTP Basic (Authorization: \{value.json.authorization}, i.e. "Basic base64(user:password)") with optional jde-AIS-Auth-Environment / -Role / -Device headers and returns userInfo.token. All other events send that token as jde-AIS-Auth: \{value.json.ais\_token}. Tools 9.2.4+ for header authentication. > [!WARNING] > > Keep credentials in broker secrets and never retain topics that carry tokens or client secrets. ## Notes * The AIS Server is customer-hosted (on-premises or OCI); replace BASE\_ADDRESS with your AIS URL. Orchestrations are built by the customer in Orchestrator Studio, so the orchestration name and its inputs are site-specific. * If the token was requested with jde-AIS-Auth-Device, every later call must send the same device; add the header in a fork if you use it. * Call JdeLogout when done; AIS sessions also hold an HTML Server session until timeout. ## Basic Syntax ```lot wrap theme={"theme":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} DEFINE CONNECTOR OracleJdEdwardsRestApi WITH TYPE REST_API ADD REST_API_CONFIG WITH BASE_ADDRESS "https://YOUR_AIS_SERVER:PORT" WITH ENABLE_CLIENT "true" WITH USE_SSL "true" WITH REQUEST_TIMEOUT '120' WITH DEFAULT_CONTENT_TYPE "application/json" ADD EVENT JdeLogin WITH SOURCE_TOPIC "erp/jd-edwards/auth/login" WITH DESTINATION_TOPIC "erp/jd-edwards/auth/login/response" WITH METHOD "POST" WITH ENDPOINT "/jderest/v3/orchestrator/jde-login" WITH HEADERS ADD "Accept" "application/json" ADD "Content-Type" "application/json" ADD "Authorization" "{value.json.authorization}" ADD "jde-AIS-Auth-Environment" "{value.json.environment}" ADD "jde-AIS-Auth-Role" "{value.json.role}" WITH BODY "" ADD EVENT JdeValidateSession WITH SOURCE_TOPIC "erp/jd-edwards/auth/validate" WITH DESTINATION_TOPIC "erp/jd-edwards/auth/validate/response" WITH METHOD "GET" WITH ENDPOINT "/jderest/v3/orchestrator/jde-validate-session" WITH HEADERS ADD "Accept" "application/json" ADD "jde-AIS-Auth" "{value.json.ais_token}" ADD EVENT JdeLogout WITH SOURCE_TOPIC "erp/jd-edwards/auth/logout" WITH DESTINATION_TOPIC "erp/jd-edwards/auth/logout/response" WITH METHOD "POST" WITH ENDPOINT "/jderest/v3/orchestrator/jde-logout" WITH HEADERS ADD "Accept" "application/json" ADD "Content-Type" "application/json" ADD "jde-AIS-Auth" "{value.json.ais_token}" WITH BODY "" ADD EVENT OrchestrationRun WITH SOURCE_TOPIC "erp/jd-edwards/orchestration/run" WITH DESTINATION_TOPIC "erp/jd-edwards/orchestration/run/response" WITH METHOD "POST" WITH ENDPOINT "/jderest/v3/orchestrator/{value.json.orchestration}" WITH HEADERS ADD "Accept" "application/json" ADD "Content-Type" "application/json" ADD "jde-AIS-Auth" "{value.json.ais_token}" WITH BODY "{value.json.body}" ADD EVENT OrchestrationGet WITH SOURCE_TOPIC "erp/jd-edwards/orchestration/get" WITH DESTINATION_TOPIC "erp/jd-edwards/orchestration/get/response" WITH METHOD "GET" WITH ENDPOINT "/jderest/v3/orchestrator/{value.json.orchestration}?{value.json.query}" WITH HEADERS ADD "Accept" "application/json" ADD "jde-AIS-Auth" "{value.json.ais_token}" ADD EVENT DataServiceQuery WITH SOURCE_TOPIC "erp/jd-edwards/dataservice/query" WITH DESTINATION_TOPIC "erp/jd-edwards/dataservice/query/response" WITH METHOD "POST" WITH ENDPOINT "/jderest/v2/dataservice" WITH HEADERS ADD "Accept" "application/json" ADD "Content-Type" "application/json" ADD "jde-AIS-Auth" "{value.json.ais_token}" WITH BODY "{value.json.body}" ``` ## Sources * [https://docs.oracle.com/en/applications/jd-edwards/cross-product/9.2/rest-api/index.html](https://docs.oracle.com/en/applications/jd-edwards/cross-product/9.2/rest-api/index.html) * [https://docs.oracle.com/en/applications/jd-edwards/cross-product/9.2/rest-api/Authentication.html](https://docs.oracle.com/en/applications/jd-edwards/cross-product/9.2/rest-api/Authentication.html) * [https://docs.oracle.com/en/applications/jd-edwards/cross-product/9.2/rest-api/api-orchestrator-service.html](https://docs.oracle.com/en/applications/jd-edwards/cross-product/9.2/rest-api/api-orchestrator-service.html) * [https://docs.oracle.com/en/applications/jd-edwards/cross-product/9.2/rest-api/op-v3-orchestrator-jde-login-post.html](https://docs.oracle.com/en/applications/jd-edwards/cross-product/9.2/rest-api/op-v3-orchestrator-jde-login-post.html) * [https://docs.oracle.com/en/applications/jd-edwards/cross-product/9.2/rest-api/op-v3-orchestrator-orchestration-post.html](https://docs.oracle.com/en/applications/jd-edwards/cross-product/9.2/rest-api/op-v3-orchestrator-orchestration-post.html) * [https://docs.oracle.com/en/applications/jd-edwards/cross-product/9.2/rest-api/op-v2-dataservice-post.html](https://docs.oracle.com/en/applications/jd-edwards/cross-product/9.2/rest-api/op-v2-dataservice-post.html) # Osm Nominatim Source: https://docs.coreflux.org/latest/lot-language/routes/rest-api-apps/osm-nominatim LoT connector for OpenStreetMap Nominatim with no API key: search and structured geocoding, reverse geocoding, OSM ID lookup and server status Technical Preview ## Overview Client-mode REST API connectors for OpenStreetMap with no API key: Nominatim geocoding, reverse geocoding and OSM lookup, and OSRM routing, nearest road, duration matrix, GPS map matching and trip ordering, on the public servers or self-hosted. This is one of the templates in the OpenStreetMap: Nominatim and OSRM (self-hostable) REST API app (`openstreetmap-nominatim-and-osrm`). Deploy it alongside the other connectors of that app when they depend on each other (for example a token connector plus an API connector). ## Auth * **schemes**: \["none"] * **secrets**: \[] * **note**: Nominatim usage policy: max 1 req/s, identifying User-Agent, no bulk geocoding, ODbL attribution ## Template parameters | Placeholder | Type | Description | Required | | - | - | - | - | | `RouteName` | string | Name of the Osm Nominatim connector | no | | `BaseUrl` | string | Osm Nominatim base URL without trailing slash (e.g. [https://nominatim.openstreetmap.org](https://nominatim.openstreetmap.org)) | yes | `BASE_ADDRESS` default: `https://nominatim.openstreetmap.org`. ## Events | Event | Method | Endpoint | Request topic | Response topic | Payload fields | | - | - | - | - | - | - | | `NominatimSearch` | GET | `/search?format=jsonv2&q={value.json.q}{value.json.params}` | `fleet/osm/nominatim/search/get` | `fleet/osm/nominatim/search/get/response` | `params`, `q` | | `NominatimStructured` | GET | `/search?format=jsonv2&street={value.json.street}&city={value.json.city}&postalcode={value.json.postalcode}&country={value.json.country}{value.json.params}` | `fleet/osm/nominatim/structured/get` | `fleet/osm/nominatim/structured/get/response` | `city`, `country`, `params`, `postalcode`, `street` | | `NominatimReverse` | GET | `/reverse?format=jsonv2&lat={value.json.lat}&lon={value.json.lon}{value.json.params}` | `fleet/osm/nominatim/reverse/get` | `fleet/osm/nominatim/reverse/get/response` | `lat`, `lon`, `params` | | `NominatimLookup` | GET | `/lookup?format=jsonv2&osm_ids={value.json.osm_ids}{value.json.params}` | `fleet/osm/nominatim/lookup/get` | `fleet/osm/nominatim/lookup/get/response` | `osm_ids`, `params` | | `NominatimStatus` | GET | `/status?format=json` | `fleet/osm/nominatim/status/get` | `fleet/osm/nominatim/status/get/response` | none | ## Deploy Get the template card (JSON with `template` and `metadata`) from the broker, then deploy the connector: ```bash theme={"theme":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} mosquitto_pub -t '$SYS/Coreflux/Command' -m "-connectorCode OSM_NOMINATIM" ``` The deployable connector from the REST API app, with `BASE_ADDRESS` already filled in: ```lot theme={"theme":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} DEFINE CONNECTOR OsmNominatim WITH TYPE REST_API ADD REST_API_CONFIG WITH BASE_ADDRESS "https://nominatim.openstreetmap.org" WITH ENABLE_CLIENT "true" WITH USE_SSL "true" WITH REQUEST_TIMEOUT '30' WITH DEFAULT_CONTENT_TYPE "application/json" ADD EVENT NominatimSearch WITH SOURCE_TOPIC "fleet/osm/nominatim/search/get" WITH DESTINATION_TOPIC "fleet/osm/nominatim/search/get/response" WITH METHOD "GET" WITH ENDPOINT "/search?format=jsonv2&q={value.json.q}{value.json.params}" WITH HEADERS ADD "Accept" "application/json" ADD "User-Agent" "Coreflux-Broker REST_API route (https://www.coreflux.org)" ADD EVENT NominatimStructured WITH SOURCE_TOPIC "fleet/osm/nominatim/structured/get" WITH DESTINATION_TOPIC "fleet/osm/nominatim/structured/get/response" WITH METHOD "GET" WITH ENDPOINT "/search?format=jsonv2&street={value.json.street}&city={value.json.city}&postalcode={value.json.postalcode}&country={value.json.country}{value.json.params}" WITH HEADERS ADD "Accept" "application/json" ADD "User-Agent" "Coreflux-Broker REST_API route (https://www.coreflux.org)" ADD EVENT NominatimReverse WITH SOURCE_TOPIC "fleet/osm/nominatim/reverse/get" WITH DESTINATION_TOPIC "fleet/osm/nominatim/reverse/get/response" WITH METHOD "GET" WITH ENDPOINT "/reverse?format=jsonv2&lat={value.json.lat}&lon={value.json.lon}{value.json.params}" WITH HEADERS ADD "Accept" "application/json" ADD "User-Agent" "Coreflux-Broker REST_API route (https://www.coreflux.org)" ADD EVENT NominatimLookup WITH SOURCE_TOPIC "fleet/osm/nominatim/lookup/get" WITH DESTINATION_TOPIC "fleet/osm/nominatim/lookup/get/response" WITH METHOD "GET" WITH ENDPOINT "/lookup?format=jsonv2&osm_ids={value.json.osm_ids}{value.json.params}" WITH HEADERS ADD "Accept" "application/json" ADD "User-Agent" "Coreflux-Broker REST_API route (https://www.coreflux.org)" ADD EVENT NominatimStatus WITH SOURCE_TOPIC "fleet/osm/nominatim/status/get" WITH DESTINATION_TOPIC "fleet/osm/nominatim/status/get/response" WITH METHOD "GET" WITH ENDPOINT "/status?format=json" WITH HEADERS ADD "Accept" "application/json" ADD "User-Agent" "Coreflux-Broker REST_API route (https://www.coreflux.org)" ``` Deploy it with `-addConnector` (do not include `ADD METADATA`). ## Usage example Call `NominatimSearch` by publishing a JSON payload to its request topic, and read the reply on the response topic: ```bash theme={"theme":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} mosquitto_sub -t 'fleet/osm/nominatim/search/get/response' & mosquitto_pub -t 'fleet/osm/nominatim/search/get' -m '{"params": "", "q": ""}' ``` ## Related * [REST API Connectors](/latest/lot-language/routes/rest-api-routes) * [REST API Apps catalog](/latest/lot-language/routes/rest-api-apps) # Osm Osrm Source: https://docs.coreflux.org/latest/lot-language/routes/rest-api-apps/osm-osrm LoT connector for the OSRM routing engine with no API key: routes, nearest road, duration matrices, GPS map matching and trip ordering Technical Preview ## Overview Client-mode REST API connectors for OpenStreetMap with no API key: Nominatim geocoding, reverse geocoding and OSM lookup, and OSRM routing, nearest road, duration matrix, GPS map matching and trip ordering, on the public servers or self-hosted. This is one of the templates in the OpenStreetMap: Nominatim and OSRM (self-hostable) REST API app (`openstreetmap-nominatim-and-osrm`). Deploy it alongside the other connectors of that app when they depend on each other (for example a token connector plus an API connector). ## Auth * **schemes**: \["none"] * **secrets**: \[] * **note**: Nominatim usage policy: max 1 req/s, identifying User-Agent, no bulk geocoding, ODbL attribution ## Template parameters | Placeholder | Type | Description | Required | | - | - | - | - | | `RouteName` | string | Name of the Osm Osrm connector | no | | `BaseUrl` | string | Osm Osrm base URL without trailing slash (e.g. [https://router.project-osrm.org](https://router.project-osrm.org)) | yes | `BASE_ADDRESS` default: `https://router.project-osrm.org`. ## Events | Event | Method | Endpoint | Request topic | Response topic | Payload fields | | - | - | - | - | - | - | | `OsrmRoute` | GET | `/route/v1/{value.json.profile}/{value.json.coordinates}?generate_hints=false{value.json.params}` | `fleet/osm/osrm/route/get` | `fleet/osm/osrm/route/get/response` | `coordinates`, `params`, `profile` | | `OsrmNearest` | GET | `/nearest/v1/{value.json.profile}/{value.json.coordinates}?generate_hints=false{value.json.params}` | `fleet/osm/osrm/nearest/get` | `fleet/osm/osrm/nearest/get/response` | `coordinates`, `params`, `profile` | | `OsrmTable` | GET | `/table/v1/{value.json.profile}/{value.json.coordinates}?generate_hints=false{value.json.params}` | `fleet/osm/osrm/table/get` | `fleet/osm/osrm/table/get/response` | `coordinates`, `params`, `profile` | | `OsrmMatch` | GET | `/match/v1/{value.json.profile}/{value.json.coordinates}?generate_hints=false{value.json.params}` | `fleet/osm/osrm/match/get` | `fleet/osm/osrm/match/get/response` | `coordinates`, `params`, `profile` | | `OsrmTrip` | GET | `/trip/v1/{value.json.profile}/{value.json.coordinates}?generate_hints=false{value.json.params}` | `fleet/osm/osrm/trip/get` | `fleet/osm/osrm/trip/get/response` | `coordinates`, `params`, `profile` | ## Deploy Get the template card (JSON with `template` and `metadata`) from the broker, then deploy the connector: ```bash theme={"theme":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} mosquitto_pub -t '$SYS/Coreflux/Command' -m "-connectorCode OSM_OSRM" ``` The deployable connector from the REST API app, with `BASE_ADDRESS` already filled in: ```lot theme={"theme":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} DEFINE CONNECTOR OsmOsrm WITH TYPE REST_API ADD REST_API_CONFIG WITH BASE_ADDRESS "https://router.project-osrm.org" WITH ENABLE_CLIENT "true" WITH USE_SSL "true" WITH REQUEST_TIMEOUT '30' WITH DEFAULT_CONTENT_TYPE "application/json" ADD EVENT OsrmRoute WITH SOURCE_TOPIC "fleet/osm/osrm/route/get" WITH DESTINATION_TOPIC "fleet/osm/osrm/route/get/response" WITH METHOD "GET" WITH ENDPOINT "/route/v1/{value.json.profile}/{value.json.coordinates}?generate_hints=false{value.json.params}" WITH HEADERS ADD "Accept" "application/json" ADD "User-Agent" "Coreflux-Broker REST_API route (https://www.coreflux.org)" ADD EVENT OsrmNearest WITH SOURCE_TOPIC "fleet/osm/osrm/nearest/get" WITH DESTINATION_TOPIC "fleet/osm/osrm/nearest/get/response" WITH METHOD "GET" WITH ENDPOINT "/nearest/v1/{value.json.profile}/{value.json.coordinates}?generate_hints=false{value.json.params}" WITH HEADERS ADD "Accept" "application/json" ADD "User-Agent" "Coreflux-Broker REST_API route (https://www.coreflux.org)" ADD EVENT OsrmTable WITH SOURCE_TOPIC "fleet/osm/osrm/table/get" WITH DESTINATION_TOPIC "fleet/osm/osrm/table/get/response" WITH METHOD "GET" WITH ENDPOINT "/table/v1/{value.json.profile}/{value.json.coordinates}?generate_hints=false{value.json.params}" WITH HEADERS ADD "Accept" "application/json" ADD "User-Agent" "Coreflux-Broker REST_API route (https://www.coreflux.org)" ADD EVENT OsrmMatch WITH SOURCE_TOPIC "fleet/osm/osrm/match/get" WITH DESTINATION_TOPIC "fleet/osm/osrm/match/get/response" WITH METHOD "GET" WITH ENDPOINT "/match/v1/{value.json.profile}/{value.json.coordinates}?generate_hints=false{value.json.params}" WITH HEADERS ADD "Accept" "application/json" ADD "User-Agent" "Coreflux-Broker REST_API route (https://www.coreflux.org)" ADD EVENT OsrmTrip WITH SOURCE_TOPIC "fleet/osm/osrm/trip/get" WITH DESTINATION_TOPIC "fleet/osm/osrm/trip/get/response" WITH METHOD "GET" WITH ENDPOINT "/trip/v1/{value.json.profile}/{value.json.coordinates}?generate_hints=false{value.json.params}" WITH HEADERS ADD "Accept" "application/json" ADD "User-Agent" "Coreflux-Broker REST_API route (https://www.coreflux.org)" ``` Deploy it with `-addConnector` (do not include `ADD METADATA`). ## Usage example Call `OsrmRoute` by publishing a JSON payload to its request topic, and read the reply on the response topic: ```bash theme={"theme":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} mosquitto_sub -t 'fleet/osm/osrm/route/get/response' & mosquitto_pub -t 'fleet/osm/osrm/route/get' -m '{"coordinates": "", "params": "", "profile": ""}' ``` ## Related * [REST API Connectors](/latest/lot-language/routes/rest-api-routes) * [REST API Apps catalog](/latest/lot-language/routes/rest-api-apps) # PagerDuty Source: https://docs.coreflux.org/latest/lot-language/routes/rest-api-apps/pagerduty Client-mode REST API connector for a curated PagerDuty slice: Events API v2 trigger/resolve from alarms, list incidents by status, and add incident notes Technical Preview ## Overview A client-mode connector for a **curated ITSM / alerting** slice of [PagerDuty Events API v2](https://docs.pagerduty.com/developer/events-api-v2-overview) and the [REST API](https://docs.pagerduty.com/developer/api/reference/rest/incidents): | Catalog connector event | Entity / path | Notes | | - | - | - | | POST v2/enqueue trigger | `POST /v2/enqueue` | **Host:** `https://events.pagerduty.com` — `event_action=trigger` + `routing_key` in body | | POST v2/enqueue resolve | `POST /v2/enqueue` | Same host/path; `event_action=resolve` + `dedup_key` | | GET incidents?statuses\[]= | `GET /incidents?statuses[]={status}` | **Host:** `https://api.pagerduty.com` — Token or OAuth | | POST incidents/\{id}/notes | `POST /incidents/{id}/notes` | Requires `From` header (user email) | Default `BASE_ADDRESS` is `https://api.pagerduty.com`. Topics use `itsm/pagerduty//`. > [!NOTE] > > Events enqueue is **not** on `api.pagerduty.com`. Official path is `https://events.pagerduty.com/v2/enqueue`. When calling `Enqueuetrigger` / `Enqueueresolve`, set connector `BASE_ADDRESS` to `https://events.pagerduty.com` (or deploy a second connector copy). Auth for Events is the `routing_key` field in the JSON body, not an Authorization header. > [!NOTE] > > Prefer Hub secrets for API tokens and routing keys. ## Basic Syntax ```lot wrap theme={"theme":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} # Coreflux REST_API template — PagerDuty (Events API v2 + REST curated slice) # Auth REST: Token token={value.json.token} (or Bearer access_token) # Auth Events: routing_key in JSON body # BASE_ADDRESS REST: https://api.pagerduty.com # Events: set BASE_ADDRESS to https://events.pagerduty.com DEFINE CONNECTOR PagerDutyRestApi WITH TYPE REST_API ADD REST_API_CONFIG WITH BASE_ADDRESS "https://api.pagerduty.com" WITH ENABLE_CLIENT "true" WITH USE_SSL "true" WITH REQUEST_TIMEOUT '60' WITH DEFAULT_CONTENT_TYPE "application/json" ADD EVENT Enqueuetrigger WITH SOURCE_TOPIC "itsm/pagerduty/event/trigger" WITH DESTINATION_TOPIC "itsm/pagerduty/event/trigger/response" WITH METHOD "POST" WITH ENDPOINT "/v2/enqueue" WITH HEADERS ADD "Accept" "application/json" ADD "Content-Type" "application/json" WITH BODY "{payload}" ADD EVENT Enqueueresolve WITH SOURCE_TOPIC "itsm/pagerduty/event/resolve" WITH DESTINATION_TOPIC "itsm/pagerduty/event/resolve/response" WITH METHOD "POST" WITH ENDPOINT "/v2/enqueue" WITH HEADERS ADD "Accept" "application/json" ADD "Content-Type" "application/json" WITH BODY "{payload}" ADD EVENT Listincidents WITH SOURCE_TOPIC "itsm/pagerduty/incident/list" WITH DESTINATION_TOPIC "itsm/pagerduty/incident/list/response" WITH METHOD "GET" WITH ENDPOINT "/incidents?statuses[]={value.json.status}&limit={value.json.limit}" WITH HEADERS ADD "Accept" "application/vnd.pagerduty+json;version=2" ADD "Content-Type" "application/json" ADD "Authorization" "Token token={value.json.token}" ADD EVENT Createincidentnote WITH SOURCE_TOPIC "itsm/pagerduty/incident/note" WITH DESTINATION_TOPIC "itsm/pagerduty/incident/note/response" WITH METHOD "POST" WITH ENDPOINT "/incidents/{value.json.id}/notes" WITH HEADERS ADD "Accept" "application/vnd.pagerduty+json;version=2" ADD "Content-Type" "application/json" ADD "Authorization" "Token token={value.json.token}" ADD "From" "{value.json.from_email}" WITH BODY "{payload}" ``` ## Events | Event | Method | Endpoint | Request topic | Response topic | | - | - | - | - | - | | Enqueuetrigger | POST | `/v2/enqueue` | `itsm/pagerduty/event/trigger` | `itsm/pagerduty/event/trigger/response` | | Enqueueresolve | POST | `/v2/enqueue` | `itsm/pagerduty/event/resolve` | `itsm/pagerduty/event/resolve/response` | | Listincidents | GET | `/incidents?statuses[]={status}&limit={limit}` | `itsm/pagerduty/incident/list` | `itsm/pagerduty/incident/list/response` | | Createincidentnote | POST | `/incidents/{id}/notes` | `itsm/pagerduty/incident/note` | `itsm/pagerduty/incident/note/response` | ## Auth **Events API v2 (trigger / resolve):** 1. Create an Events API v2 integration on a PagerDuty service; copy the Integration Key. 2. Set connector `BASE_ADDRESS` to `https://events.pagerduty.com`. 3. Put `routing_key` in the MQTT JSON body (with `event_action`, and for resolve a `dedup_key`). No Authorization header. 4. Prefer Hub secrets / `GET SECRET "PAGERDUTY_ROUTING_KEY"`. Never commit real keys. 5. Successful enqueue returns **HTTP 202**. **REST API token (list / notes) — default in the template:** 1. Create a PagerDuty [API token](https://docs.pagerduty.com/developer/authentication) (account or user). 2. Set connector `BASE_ADDRESS` to `https://api.pagerduty.com`. 3. Pass `token` on the MQTT request payload so headers resolve: * `Authorization: Token token={value.json.token}` 4. Use `Accept: application/vnd.pagerduty+json;version=2`. 5. For notes, also pass `from_email` → `From` header (valid account user email). 6. Store the token in Hub secrets / `GET SECRET "PAGERDUTY_API_TOKEN"`. Never commit real credentials. **OAuth 2.0 (REST alternative):** 1. Obtain an OAuth access token with `incidents.read` / `incidents.write` as needed. 2. Change the Authorization header to `Bearer {value.json.access_token}`. ## Example payloads **Trigger from alarm** (`BASE_ADDRESS` = `https://events.pagerduty.com`): ```json theme={"theme":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} { "routing_key": "YOUR_EVENTS_INTEGRATION_KEY", "event_action": "trigger", "dedup_key": "line-a/vibration", "payload": { "summary": "Vibration above threshold — Zone A", "severity": "critical", "source": "coreflux-broker", "component": "plc-line-a", "custom_details": { "rms_mm_s": 12.4 } }, "client": "Coreflux" } ``` **Resolve:** ```json theme={"theme":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} { "routing_key": "YOUR_EVENTS_INTEGRATION_KEY", "event_action": "resolve", "dedup_key": "line-a/vibration" } ``` **List incidents:** ```json theme={"theme":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} { "token": "YOUR_API_TOKEN", "status": "triggered", "limit": "25" } ``` **Create note:** ```json theme={"theme":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} { "token": "YOUR_API_TOKEN", "id": "PT4KHLK", "from_email": "oncall@example.com", "note": { "content": "Coreflux: vibration cleared after fan restart." } } ``` ## Deploy notes * Deploy **`PagerDutyRestApi.lot` only** (`-addConnector`). Do **not** deploy `hub-metadata.lot` as a connector — it exists for Hub / `-listTemplates` (`ADD METADATA` + `WITH IMAGE` base64). * Hub category: **REST API Connectors**. * Doc link: [https://docs.coreflux.org/latest/lot-language/routes/rest-api-apps/pagerduty](https://docs.coreflux.org/latest/lot-language/routes/rest-api-apps/pagerduty) ## Related * [REST API Connectors](/latest/lot-language/routes/rest-api-routes) * [REST API Apps catalog](/latest/lot-language/routes/rest-api-apps) # Particle Source: https://docs.coreflux.org/latest/lot-language/routes/rest-api-apps/particle LoT connector for Particle Device Cloud: read variables, call functions, publish events, read device vitals and locations, and manage webhooks and Ledger Technical Preview ## Overview Client-mode connector for the [Particle Device Cloud API](https://docs.particle.io/reference/cloud-apis/api/). Particle devices (Photon 2, Argon, Boron, B-SoM, M-SoM, Tracker) expose **cloud variables** that you read and **cloud functions** that you call, publish and subscribe to **events**, and keep shared JSON state in **Ledger**. Fleets are grouped in **products**; cellular devices come with Particle **SIMs**. * **ParticleRestApi** (`https://api.particle.io`) lets the Coreflux broker read a machine value from a device, call a function to change a setpoint, broadcast an event to devices, check device vitals and SIM data usage, follow trackers on a map, and push setpoints to devices through a Cloud to Device ledger. * **ParticleWebhookReceiver** (`cloud-iot_particle_webhook.lot`, opt-in, server mode) receives Particle webhooks so device events reach MQTT without polling. | Event | Request | Notes | | - | - | - | | GetAccessTokenInfo | `GET /v1/access_tokens/current` | Expiry, client and scopes of the token, a good token check | | ListDevices | `GET /v1/devices` | Devices the token can access | | GetDevice | `GET /v1/devices/{device_id}` | `online`, `last_heard`, the variables and functions the firmware exposes | | GetVariable | `GET /v1/devices/{device_id}/{variable_name}?{query}` | **Catalog.** Read a `Particle.variable`; `format=raw` returns only the value | | CallFunction | `POST /v1/devices/{device_id}/{function_name}` | **Catalog.** Call a `Particle.function` with `{"arg": "..."}` (max 63 characters) | | PingDevice | `PUT /v1/devices/{device_id}/ping` | Is the device online? (PUT without a body) | | UpdateDevice | `PUT /v1/devices/{device_id}` | Rename, notes, `signal` (rainbow LED) or `firmware_updates_forced` | | GetDeviceVitals | `GET /v1/diagnostics/{device_id}/last` | Last vitals: signal, battery, memory, cloud connection | | GetDeviceVitalsHistory | `GET /v1/diagnostics/{device_id}?{query}` | `start_date`, `end_date` (ISO 8601) | | RefreshDeviceVitals | `POST /v1/diagnostics/{device_id}/update` | Ask the device to send fresh vitals (202) | | PublishEvent | `POST /v1/devices/events` | **Catalog.** Cloud event to your devices and integrations: `name`, `data`, `ttl` | | ListProducts | `GET /v1/user/products` | Products you are a team member of | | GetProduct | `GET /v1/products/{product_id}` | Product by id or slug | | ListProductDevices | `GET /v1/products/{product_id}/devices?{query}` | `deviceId`, `groups`, `deviceName`, `serialNumber`, `sortAttr`, `sortDir`, `quarantined`, `page`, `perPage` | | GetProductDevice | `GET /v1/products/{product_id}/devices/{device_id}` | Groups, firmware lock, development flag | | GetProductVariable | `GET /v1/products/{product_id}/devices/{device_id}/{variable_name}?{query}` | Product endpoint form (required for product API users) | | CallProductFunction | `POST /v1/products/{product_id}/devices/{device_id}/{function_name}` | Product endpoint form | | PublishProductEvent | `POST /v1/products/{product_id}/events` | Product event stream; `as_device_id` publishes as a product device | | ListProductLocations | `GET /v1/products/{product_id}/locations?{query}` | Asset tracking: `date_range`, `rect_bl`, `rect_tr`, `device_id`, `device_name`, `groups`, `page`, `per_page` | | GetProductDeviceLocation | `GET /v1/products/{product_id}/locations/{device_id}?{query}` | Location history of one device | | GetProductOnlineMetrics | `GET /v1/products/{product_id}/metrics/online?{query}` | Online device counts per bucket (fleet health) | | ListProductFirmware | `GET /v1/products/{product_id}/firmware` | Product firmware versions | | ListSims | `GET /v1/sims?{query}` | `iccid`, `deviceId`, `deviceName`, `page`, `perPage` | | GetSim | `GET /v1/sims/{iccid}` | SIM status and limits | | GetSimDataUsage | `GET /v1/sims/{iccid}/data_usage` | Daily MB used this billing period | | GetSimStatus | `GET /v1/sims/{iccid}/status` | Cellular network status (may answer 202 first) | | ListIntegrations | `GET /v1/integrations` | Webhooks and other integrations | | GetIntegration | `GET /v1/integrations/{integration_id}` | One integration with recent logs | | CreateWebhook | `POST /v1/integrations` | Webhook to any URL, for example the broker receiver | | TestIntegration | `POST /v1/integrations/{integration_id}/test` | Sends a test event to the target | | ListProductIntegrations | `GET /v1/products/{product_id}/integrations` | Integrations of a product | | CreateProductWebhook | `POST /v1/products/{product_id}/integrations` | Webhook for every device in the product | | ListLedgers | `GET /v1/ledgers?{query}` | Ledger definitions (`page`, `perPage`, `archived`) | | GetLedger | `GET /v1/ledgers/{ledger_name}` | One ledger definition | | ListLedgerInstances | `GET /v1/ledgers/{ledger_name}/instances?{query}` | Instances of a ledger | | GetLedgerInstance | `GET /v1/ledgers/{ledger_name}/instances/{scope_value}` | Read an instance (JSON) | | SetLedgerInstance | `PUT /v1/ledgers/{ledger_name}/instances/{scope_value}?{query}` | Write an instance; `set_mode=Replace` or `Merge` | | ListLedgerInstanceVersions | `GET /v1/ledgers/{ledger_name}/instances/{scope_value}/versions?{query}` | `replaced_before`, `replaced_after` | | GetProductLedgerInstance | `GET /v1/products/{product_id}/ledgers/{ledger_name}/instances/{scope_value}` | Product endpoint form | | SetProductLedgerInstance | `PUT /v1/products/{product_id}/ledgers/{ledger_name}/instances/{scope_value}?{query}` | Product endpoint form | | GetOrgLedgerInstance | `GET /v1/orgs/{org_id}/ledgers/{ledger_name}/instances/{scope_value}` | Organization ledger | | SetOrgLedgerInstance | `PUT /v1/orgs/{org_id}/ledgers/{ledger_name}/instances/{scope_value}?{query}` | Organization ledger | Topics are `cloud-iot/particle//` and every reply goes to `/response`. There are no DELETE events. `product_id` takes the product id or slug, `org_id` the organization id or slug. See the base guide: [REST API Connectors](/latest/lot-language/routes/rest-api-routes). ## Authentication Every event sends `Authorization: Bearer {access_token}`. * A **user token** is the quickest start: `particle token create` (valid 90 days unless you pass `--expires-in`). It can do anything your account can. * For unattended plant use, create a **product or organization API user** with only the scopes its events need, for example `devices:list`, `devices.variable:get`, `devices.function:call`, `events:send`, `integrations:create`, `ledger.instances:get` and `ledger.instances:set`. An API user has one token at a time and must use the product (`/v1/products/...`) or organization (`/v1/orgs/...`) events. Keep the token in a broker secret (suggested `PARTICLE_ACCESS_TOKEN`, plus `PARTICLE_DEVICE_ID` / `PARTICLE_PRODUCT_ID`) and map it in with `GET SECRET`. Particle still accepts a token in the query string or a form body, but both are deprecated; this template uses the header only. A missing token returns `400 {"error": "invalid_request", "error_description": "The access token was not found"}`, a wrong or expired one `401 {"error": "invalid_token", ...}`. Particle checks the token before it looks at the path, so a 400 / 401 does not tell you whether the path exists. ## Basic Syntax ```lot wrap theme={"theme":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} DEFINE CONNECTOR ParticleRestApi WITH TYPE REST_API ADD REST_API_CONFIG WITH BASE_ADDRESS "https://api.particle.io" WITH ENABLE_CLIENT "true" WITH USE_SSL "true" WITH REQUEST_TIMEOUT '30' WITH DEFAULT_CONTENT_TYPE "application/json" ADD EVENT GetVariable WITH SOURCE_TOPIC "cloud-iot/particle/device-variable/get" WITH DESTINATION_TOPIC "cloud-iot/particle/device-variable/get/response" WITH METHOD "GET" WITH ENDPOINT "/v1/devices/{value.json.device_id}/{value.json.variable_name}?{value.json.query}" WITH HEADERS ADD "Accept" "application/json" ADD "Authorization" "Bearer {value.json.access_token}" ADD EVENT CallFunction WITH SOURCE_TOPIC "cloud-iot/particle/device-function/call" WITH DESTINATION_TOPIC "cloud-iot/particle/device-function/call/response" WITH METHOD "POST" WITH ENDPOINT "/v1/devices/{value.json.device_id}/{value.json.function_name}" WITH HEADERS ADD "Accept" "application/json" ADD "Authorization" "Bearer {value.json.access_token}" ADD "Content-Type" "application/json" WITH BODY "{value.json.body}" ADD EVENT PublishEvent WITH SOURCE_TOPIC "cloud-iot/particle/event/publish" WITH DESTINATION_TOPIC "cloud-iot/particle/event/publish/response" WITH METHOD "POST" WITH ENDPOINT "/v1/devices/events" WITH HEADERS ADD "Accept" "application/json" ADD "Authorization" "Bearer {value.json.access_token}" ADD "Content-Type" "application/json" WITH BODY "{value.json.body}" ``` The full template, with all 42 events, is `ParticleRestApi.lot` in the REST API app. ## MQTT examples The connector sends only `{value.json.body}` as the request body. Keep `access_token`, `device_id`, `product_id`, `org_id`, `variable_name`, `function_name`, `ledger_name`, `scope_value`, `iccid`, `integration_id` and `query` at the top level of the MQTT payload. Device ids are the 24-character hex ids shown in the Particle console. Send `"query": ""` when you do not need query parameters. **Read a cloud variable**: publish to `cloud-iot/particle/device-variable/get`: ```json theme={"theme":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} { "access_token": "", "device_id": "0123456789abcdef01234567", "variable_name": "temperature", "query": "" } ``` Particle answers `{"name": "temperature", "result": 21.4, "coreInfo": {...}}`; with `"query": "format=raw"` only `21.4`. The device must be online: an offline device answers `404`, a variable the firmware does not expose `400`. **Call a cloud function**: publish to `cloud-iot/particle/device-function/call`: ```json theme={"theme":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} { "access_token": "", "device_id": "0123456789abcdef01234567", "function_name": "setSpeed", "body": { "arg": "1200" } } ``` The reply carries the integer the firmware function returns: `{"id": "...", "connected": true, "return_value": 1200}`. The argument is a string of at most 63 characters. **Publish an event to devices**: publish to `cloud-iot/particle/event/publish`: ```json theme={"theme":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} { "access_token": "", "body": { "name": "coreflux/line-porto-1/shift", "data": "{\"shift\":\"B\",\"target\":1200}", "ttl": 60 } } ``` Devices receive it with `Particle.subscribe("coreflux/line-porto-1/", handler)`. For a product fleet use `cloud-iot/particle/product-event/publish` with `"product_id"`. **Push setpoints through Ledger**: publish to `cloud-iot/particle/ledger-instance/set`: ```json theme={"theme":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} { "access_token": "", "ledger_name": "line-setpoints", "scope_value": "0123456789abcdef01234567", "query": "set_mode=Merge", "body": { "instance": { "data": { "speed_rpm": 1200, "recipe": "R-42" } } } } ``` A Cloud to Device ledger syncs to the device as soon as it is online, so the setpoint arrives even if the device is asleep now. `set_mode=Replace` (the default) overwrites the instance, `Merge` updates only the keys you send. Use the `product-ledger-instance` or `org-ledger-instance` events for product or organization ledgers. **Fleet checks**: `cloud-iot/particle/product-device/list` with `"query": "groups=porto-1&perPage=100"`, `cloud-iot/particle/device-vitals/get` for signal and battery, `cloud-iot/particle/sim-data-usage/get` with `"iccid"`, and `cloud-iot/particle/product-location/list` with `"query": "per_page=100"` for tracker positions. ## Receive Particle events (webhook receiver) Polling variables costs a data operation per call. To get device events pushed to MQTT, deploy the companion file `cloud-iot_particle_webhook.lot`: ```lot wrap theme={"theme":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} DEFINE CONNECTOR ParticleWebhookReceiver WITH TYPE REST_API ADD REST_API_CONFIG WITH ENABLE_SERVER "true" WITH SERVER_PORT '9001' WITH DEFAULT_CONTENT_TYPE "application/json" ADD EVENT WebhookTelemetry WITH ENDPOINT "/particle/webhook/telemetry" WITH METHOD "POST" WITH DESTINATION_TOPIC "cloud-iot/particle/webhook-telemetry/received" ADD EVENT WebhookEvents WITH ENDPOINT "/particle/webhook/events" WITH METHOD "POST" WITH DESTINATION_TOPIC "cloud-iot/particle/webhook-event/received" ``` Then create a Particle webhook that points at it, for example by publishing to `cloud-iot/particle/webhook/create`: ```json theme={"theme":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} { "access_token": "", "body": { "integration_type": "Webhook", "event": "telemetry", "url": "https://broker.example.com/particle/webhook/telemetry", "requestType": "POST", "noDefaults": true, "json": { "event": "{{{PARTICLE_EVENT_NAME}}}", "data": "{{{PARTICLE_EVENT_VALUE}}}", "coreid": "{{{PARTICLE_DEVICE_ID}}}", "published_at": "{{{PARTICLE_PUBLISHED_AT}}}" }, "headers": { "Authorization": "Bearer " } } } ``` Every `Particle.publish("telemetry", ...)` from your devices now arrives on `cloud-iot/particle/webhook-telemetry/received`. `cloud-iot/particle/integration/test` sends a test delivery. 1. Deploying the receiver opens HTTP port `9001` on the broker. 2. Particle delivers from its cloud to a public URL. Put a reverse proxy or tunnel with a public TLS certificate in front of port `9001` and forward `/particle/webhook/...`. 3. Accept only Particle. The connector does not check secrets itself: let the proxy check the `Authorization` header you set on the webhook (or Basic `auth`, or a Particle cloud secret inserted with `{{{SECRET_NAME}}}`). 4. Use the **JSON** request format. The default Web Form format arrives URL-encoded. 5. Answer fast: Particle waits 20 seconds and throttles a webhook whose target fails or times out too often. 6. `data` is the string the device published (often JSON text): parse it in a LoT action. Retries can repeat a delivery, so deduplicate on `coreid` + `published_at`. ## Helper actions Call a function from a plant setpoint topic, poll a variable every minute, and connector webhook telemetry per device, with the token and device id kept in broker secrets: ```lot theme={"theme":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} DEFINE ACTION ParticleSetLineSpeed ON TOPIC "porto/line-1/speed/set" DO SET "tok" WITH GET SECRET "PARTICLE_ACCESS_TOKEN" SET "dev" WITH GET SECRET "PARTICLE_DEVICE_ID" SET "rpm" WITH GET JSON "rpm" IN PAYLOAD AS STRING PUBLISH TOPIC "cloud-iot/particle/device-function/call" WITH { "access_token": {tok}, "device_id": {dev}, "function_name": "setSpeed", "body": { "arg": {rpm} } } DEFINE ACTION ParticlePollTemperature ON EVERY 1 MINUTE DO SET "tok" WITH GET SECRET "PARTICLE_ACCESS_TOKEN" SET "dev" WITH GET SECRET "PARTICLE_DEVICE_ID" PUBLISH TOPIC "cloud-iot/particle/device-variable/get" WITH { "access_token": {tok}, "device_id": {dev}, "variable_name": "temperature", "query": "" } DEFINE ACTION ParticleWebhookToPlant ON TOPIC "cloud-iot/particle/webhook-telemetry/received" DO SET "dev" WITH (GET JSON "coreid" IN PAYLOAD AS STRING) SET "data" WITH (GET JSON "data" IN PAYLOAD AS STRING) PUBLISH TOPIC "plant/particle/" + {dev} + "/telemetry" WITH {data} ``` Restrict who can publish to `cloud-iot/particle/#` with a rule: its payloads carry the token, and the function, event and ledger events change how devices in the field behave. ## Notes * **Rate limits:** 10,000 API requests every 5 minutes per public IP address; a `429` means slow down (retrying at once does not help). The serial-number lookup and the event streams have stricter limits. Polling many variables quickly hits the limit and costs data operations on cellular devices; prefer events plus the webhook receiver. * **Errors** come back on the response topic: `400` (bad request, or the variable / function is not exposed), `401` (token invalid or expired), `403` (token not allowed for this device or product), `404` (device offline or unknown), `408` (the device did not answer in time), `429`, `5xx`. * **Ledger:** names are lowercase letters, digits and dashes, up to 32 characters. Device-synced ledgers hold up to 16 KB of JSON, cloud-only ledgers up to 1 MB. Device to Cloud and Cloud to Device ledgers need Device OS 6.1.0 or later. The API reference lists the set body as `{"data": ...}`; the official JavaScript SDK sends `{"instance": {"data": ...}}` with `set_mode`. This template passes the body through unchanged, so either form can be sent. * **Not packaged:** Server-Sent Event streams (`GET /v1/devices/events/...`, product and organization streams) because a REST\_API connector cannot hold a stream open (use the webhook receiver instead), every DELETE (unclaim, remove from product, delete ledger instance, delete integration), firmware flashing and compiling, claiming and importing devices, SIM activation, customers, groups, Logic functions, environment variables, cloud secrets and OAuth clients. * Only the global host `https://api.particle.io` is documented (TLS 1.2 or later). ## Related * [REST API Connectors](/latest/lot-language/routes/rest-api-routes) * [REST API app templates](/latest/lot-language/routes/rest-api-apps/index) * [Losant](/latest/lot-language/routes/rest-api-apps/losant) * [balena](/latest/lot-language/routes/rest-api-apps/balena) # Pipedrive Source: https://docs.coreflux.org/latest/lot-language/routes/rest-api-apps/pipedrive LoT connector for Pipedrive CRM: open and move deals, log activities and notes from plant alarms, update organizations, search persons and create leads Technical Preview ## Overview A client-mode connector for the [Pipedrive API](https://developers.pipedrive.com/docs/api/v1) that turns plant events into sales and service follow-ups. A machine alarm can open a deal or an activity, add a note, and close the activity when the alarm clears. Counters from the installed base can be written to organization custom fields. The connector uses API v2 (`/api/v2/...`) for deals, activities, organizations, persons, pipelines, stages and deal fields, and v1 (`/v1/...`) only for notes, leads, webhooks and the current user, which have no v2 version yet. Topics use `crm/pipedrive//`; each response is published on `/response`. ## Authentication Create a personal API token in Pipedrive under **Settings > Personal preferences > API**. The connector sends it in the `x-api-token` header, taken from the `api_token` field of each MQTT payload. > [!WARNING] > > Do not publish the token in retained messages. Store it as a broker secret (for example `PIPEDRIVE_API_TOKEN`) and have a LoT action add it to the payload with `GET SECRET` before publishing to the connector topic. For a Marketplace (OAuth) app, change the header to `Authorization: Bearer ` and set `BASE_ADDRESS` to the `api_domain` returned by `https://oauth.pipedrive.com/oauth/token` (for example `https://yourcompany.pipedrive.com`). ## Payload fields | Field | Used by | Notes | | - | - | - | | `api_token` | every event | Pipedrive API token | | `id` | `...Get`, `...Update` | Numeric id of the deal, activity or organization | | `query` | list and search events | URL-encoded query string; send `""` for none. Search needs `term=` (at least 2 characters) | | `body` | create and update events | JSON object sent as the request body | ## Events | Event | Method | Endpoint | Source topic | Write | | - | - | - | - | - | | `CurrentUser` | `GET` | `/v1/users/me` | `crm/pipedrive/users/me/get` | | | `DealsList` | `GET` | `/api/v2/deals?{query}` | `crm/pipedrive/deals/list` | | | `DealGet` | `GET` | `/api/v2/deals/{id}` | `crm/pipedrive/deals/get` | | | `DealsSearch` | `GET` | `/api/v2/deals/search?{query}` | `crm/pipedrive/deals/search` | | | `DealCreate` | `POST` | `/api/v2/deals` | `crm/pipedrive/deals/create` | yes | | `DealUpdate` | `PATCH` | `/api/v2/deals/{id}` | `crm/pipedrive/deals/update` | yes | | `ActivitiesList` | `GET` | `/api/v2/activities?{query}` | `crm/pipedrive/activities/list` | | | `ActivityCreate` | `POST` | `/api/v2/activities` | `crm/pipedrive/activities/create` | yes | | `ActivityUpdate` | `PATCH` | `/api/v2/activities/{id}` | `crm/pipedrive/activities/update` | yes | | `OrganizationsList` | `GET` | `/api/v2/organizations?{query}` | `crm/pipedrive/organizations/list` | | | `OrganizationGet` | `GET` | `/api/v2/organizations/{id}` | `crm/pipedrive/organizations/get` | | | `OrganizationCreate` | `POST` | `/api/v2/organizations` | `crm/pipedrive/organizations/create` | yes | | `OrganizationUpdate` | `PATCH` | `/api/v2/organizations/{id}` | `crm/pipedrive/organizations/update` | yes | | `PersonsSearch` | `GET` | `/api/v2/persons/search?{query}` | `crm/pipedrive/persons/search` | | | `PersonCreate` | `POST` | `/api/v2/persons` | `crm/pipedrive/persons/create` | yes | | `PipelinesList` | `GET` | `/api/v2/pipelines?{query}` | `crm/pipedrive/pipelines/list` | | | `StagesList` | `GET` | `/api/v2/stages?{query}` | `crm/pipedrive/stages/list` | | | `DealFieldsList` | `GET` | `/api/v2/dealFields?{query}` | `crm/pipedrive/dealfields/list` | | | `NoteCreate` | `POST` | `/v1/notes` | `crm/pipedrive/notes/create` | yes | | `LeadCreate` | `POST` | `/v1/leads` | `crm/pipedrive/leads/create` | yes | | `WebhooksList` | `GET` | `/v1/webhooks` | `crm/pipedrive/webhooks/list` | | | `WebhookCreate` | `POST` | `/v1/webhooks` | `crm/pipedrive/webhooks/create` | yes | `OrganizationUpdate` covers the catalog's `PUT organizations/{id}`: in API v2 updates use `PATCH`, and v1 organization endpoints are no longer in Pipedrive's v1 spec. No `DELETE` events are included on purpose. ## Basic Syntax ```lot wrap theme={"theme":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} DEFINE CONNECTOR PipedriveRestApi WITH TYPE REST_API ADD REST_API_CONFIG WITH BASE_ADDRESS "https://api.pipedrive.com" WITH ENABLE_CLIENT "true" WITH USE_SSL "true" WITH REQUEST_TIMEOUT '60' WITH DEFAULT_CONTENT_TYPE "application/json" ADD EVENT CurrentUser WITH SOURCE_TOPIC "crm/pipedrive/users/me/get" WITH DESTINATION_TOPIC "crm/pipedrive/users/me/get/response" WITH METHOD "GET" WITH ENDPOINT "/v1/users/me" WITH HEADERS ADD "Accept" "application/json" ADD "User-Agent" "Coreflux-Broker REST_API route (https://www.coreflux.org)" ADD "x-api-token" "{value.json.api_token}" ADD EVENT DealsList WITH SOURCE_TOPIC "crm/pipedrive/deals/list" WITH DESTINATION_TOPIC "crm/pipedrive/deals/list/response" WITH METHOD "GET" WITH ENDPOINT "/api/v2/deals?{value.json.query}" WITH HEADERS ADD "Accept" "application/json" ADD "User-Agent" "Coreflux-Broker REST_API route (https://www.coreflux.org)" ADD "x-api-token" "{value.json.api_token}" ADD EVENT DealGet WITH SOURCE_TOPIC "crm/pipedrive/deals/get" WITH DESTINATION_TOPIC "crm/pipedrive/deals/get/response" WITH METHOD "GET" WITH ENDPOINT "/api/v2/deals/{value.json.id}" WITH HEADERS ADD "Accept" "application/json" ADD "User-Agent" "Coreflux-Broker REST_API route (https://www.coreflux.org)" ADD "x-api-token" "{value.json.api_token}" ADD EVENT DealsSearch WITH SOURCE_TOPIC "crm/pipedrive/deals/search" WITH DESTINATION_TOPIC "crm/pipedrive/deals/search/response" WITH METHOD "GET" WITH ENDPOINT "/api/v2/deals/search?{value.json.query}" WITH HEADERS ADD "Accept" "application/json" ADD "User-Agent" "Coreflux-Broker REST_API route (https://www.coreflux.org)" ADD "x-api-token" "{value.json.api_token}" ADD EVENT DealCreate WITH SOURCE_TOPIC "crm/pipedrive/deals/create" WITH DESTINATION_TOPIC "crm/pipedrive/deals/create/response" WITH METHOD "POST" WITH ENDPOINT "/api/v2/deals" WITH HEADERS ADD "Accept" "application/json" ADD "User-Agent" "Coreflux-Broker REST_API route (https://www.coreflux.org)" ADD "x-api-token" "{value.json.api_token}" ADD "Content-Type" "application/json" WITH BODY "{value.json.body}" ADD EVENT DealUpdate WITH SOURCE_TOPIC "crm/pipedrive/deals/update" WITH DESTINATION_TOPIC "crm/pipedrive/deals/update/response" WITH METHOD "PATCH" WITH ENDPOINT "/api/v2/deals/{value.json.id}" WITH HEADERS ADD "Accept" "application/json" ADD "User-Agent" "Coreflux-Broker REST_API route (https://www.coreflux.org)" ADD "x-api-token" "{value.json.api_token}" ADD "Content-Type" "application/json" WITH BODY "{value.json.body}" ADD EVENT ActivitiesList WITH SOURCE_TOPIC "crm/pipedrive/activities/list" WITH DESTINATION_TOPIC "crm/pipedrive/activities/list/response" WITH METHOD "GET" WITH ENDPOINT "/api/v2/activities?{value.json.query}" WITH HEADERS ADD "Accept" "application/json" ADD "User-Agent" "Coreflux-Broker REST_API route (https://www.coreflux.org)" ADD "x-api-token" "{value.json.api_token}" ADD EVENT ActivityCreate WITH SOURCE_TOPIC "crm/pipedrive/activities/create" WITH DESTINATION_TOPIC "crm/pipedrive/activities/create/response" WITH METHOD "POST" WITH ENDPOINT "/api/v2/activities" WITH HEADERS ADD "Accept" "application/json" ADD "User-Agent" "Coreflux-Broker REST_API route (https://www.coreflux.org)" ADD "x-api-token" "{value.json.api_token}" ADD "Content-Type" "application/json" WITH BODY "{value.json.body}" ADD EVENT ActivityUpdate WITH SOURCE_TOPIC "crm/pipedrive/activities/update" WITH DESTINATION_TOPIC "crm/pipedrive/activities/update/response" WITH METHOD "PATCH" WITH ENDPOINT "/api/v2/activities/{value.json.id}" WITH HEADERS ADD "Accept" "application/json" ADD "User-Agent" "Coreflux-Broker REST_API route (https://www.coreflux.org)" ADD "x-api-token" "{value.json.api_token}" ADD "Content-Type" "application/json" WITH BODY "{value.json.body}" ADD EVENT OrganizationsList WITH SOURCE_TOPIC "crm/pipedrive/organizations/list" WITH DESTINATION_TOPIC "crm/pipedrive/organizations/list/response" WITH METHOD "GET" WITH ENDPOINT "/api/v2/organizations?{value.json.query}" WITH HEADERS ADD "Accept" "application/json" ADD "User-Agent" "Coreflux-Broker REST_API route (https://www.coreflux.org)" ADD "x-api-token" "{value.json.api_token}" ADD EVENT OrganizationGet WITH SOURCE_TOPIC "crm/pipedrive/organizations/get" WITH DESTINATION_TOPIC "crm/pipedrive/organizations/get/response" WITH METHOD "GET" WITH ENDPOINT "/api/v2/organizations/{value.json.id}" WITH HEADERS ADD "Accept" "application/json" ADD "User-Agent" "Coreflux-Broker REST_API route (https://www.coreflux.org)" ADD "x-api-token" "{value.json.api_token}" ADD EVENT OrganizationCreate WITH SOURCE_TOPIC "crm/pipedrive/organizations/create" WITH DESTINATION_TOPIC "crm/pipedrive/organizations/create/response" WITH METHOD "POST" WITH ENDPOINT "/api/v2/organizations" WITH HEADERS ADD "Accept" "application/json" ADD "User-Agent" "Coreflux-Broker REST_API route (https://www.coreflux.org)" ADD "x-api-token" "{value.json.api_token}" ADD "Content-Type" "application/json" WITH BODY "{value.json.body}" ADD EVENT OrganizationUpdate WITH SOURCE_TOPIC "crm/pipedrive/organizations/update" WITH DESTINATION_TOPIC "crm/pipedrive/organizations/update/response" WITH METHOD "PATCH" WITH ENDPOINT "/api/v2/organizations/{value.json.id}" WITH HEADERS ADD "Accept" "application/json" ADD "User-Agent" "Coreflux-Broker REST_API route (https://www.coreflux.org)" ADD "x-api-token" "{value.json.api_token}" ADD "Content-Type" "application/json" WITH BODY "{value.json.body}" ADD EVENT PersonsSearch WITH SOURCE_TOPIC "crm/pipedrive/persons/search" WITH DESTINATION_TOPIC "crm/pipedrive/persons/search/response" WITH METHOD "GET" WITH ENDPOINT "/api/v2/persons/search?{value.json.query}" WITH HEADERS ADD "Accept" "application/json" ADD "User-Agent" "Coreflux-Broker REST_API route (https://www.coreflux.org)" ADD "x-api-token" "{value.json.api_token}" ADD EVENT PersonCreate WITH SOURCE_TOPIC "crm/pipedrive/persons/create" WITH DESTINATION_TOPIC "crm/pipedrive/persons/create/response" WITH METHOD "POST" WITH ENDPOINT "/api/v2/persons" WITH HEADERS ADD "Accept" "application/json" ADD "User-Agent" "Coreflux-Broker REST_API route (https://www.coreflux.org)" ADD "x-api-token" "{value.json.api_token}" ADD "Content-Type" "application/json" WITH BODY "{value.json.body}" ADD EVENT PipelinesList WITH SOURCE_TOPIC "crm/pipedrive/pipelines/list" WITH DESTINATION_TOPIC "crm/pipedrive/pipelines/list/response" WITH METHOD "GET" WITH ENDPOINT "/api/v2/pipelines?{value.json.query}" WITH HEADERS ADD "Accept" "application/json" ADD "User-Agent" "Coreflux-Broker REST_API route (https://www.coreflux.org)" ADD "x-api-token" "{value.json.api_token}" ADD EVENT StagesList WITH SOURCE_TOPIC "crm/pipedrive/stages/list" WITH DESTINATION_TOPIC "crm/pipedrive/stages/list/response" WITH METHOD "GET" WITH ENDPOINT "/api/v2/stages?{value.json.query}" WITH HEADERS ADD "Accept" "application/json" ADD "User-Agent" "Coreflux-Broker REST_API route (https://www.coreflux.org)" ADD "x-api-token" "{value.json.api_token}" ADD EVENT DealFieldsList WITH SOURCE_TOPIC "crm/pipedrive/dealfields/list" WITH DESTINATION_TOPIC "crm/pipedrive/dealfields/list/response" WITH METHOD "GET" WITH ENDPOINT "/api/v2/dealFields?{value.json.query}" WITH HEADERS ADD "Accept" "application/json" ADD "User-Agent" "Coreflux-Broker REST_API route (https://www.coreflux.org)" ADD "x-api-token" "{value.json.api_token}" ADD EVENT NoteCreate WITH SOURCE_TOPIC "crm/pipedrive/notes/create" WITH DESTINATION_TOPIC "crm/pipedrive/notes/create/response" WITH METHOD "POST" WITH ENDPOINT "/v1/notes" WITH HEADERS ADD "Accept" "application/json" ADD "User-Agent" "Coreflux-Broker REST_API route (https://www.coreflux.org)" ADD "x-api-token" "{value.json.api_token}" ADD "Content-Type" "application/json" WITH BODY "{value.json.body}" ADD EVENT LeadCreate WITH SOURCE_TOPIC "crm/pipedrive/leads/create" WITH DESTINATION_TOPIC "crm/pipedrive/leads/create/response" WITH METHOD "POST" WITH ENDPOINT "/v1/leads" WITH HEADERS ADD "Accept" "application/json" ADD "User-Agent" "Coreflux-Broker REST_API route (https://www.coreflux.org)" ADD "x-api-token" "{value.json.api_token}" ADD "Content-Type" "application/json" WITH BODY "{value.json.body}" ADD EVENT WebhooksList WITH SOURCE_TOPIC "crm/pipedrive/webhooks/list" WITH DESTINATION_TOPIC "crm/pipedrive/webhooks/list/response" WITH METHOD "GET" WITH ENDPOINT "/v1/webhooks" WITH HEADERS ADD "Accept" "application/json" ADD "User-Agent" "Coreflux-Broker REST_API route (https://www.coreflux.org)" ADD "x-api-token" "{value.json.api_token}" ADD EVENT WebhookCreate WITH SOURCE_TOPIC "crm/pipedrive/webhooks/create" WITH DESTINATION_TOPIC "crm/pipedrive/webhooks/create/response" WITH METHOD "POST" WITH ENDPOINT "/v1/webhooks" WITH HEADERS ADD "Accept" "application/json" ADD "User-Agent" "Coreflux-Broker REST_API route (https://www.coreflux.org)" ADD "x-api-token" "{value.json.api_token}" ADD "Content-Type" "application/json" WITH BODY "{value.json.body}" ``` ## Examples **Activity from an alarm** Publish to `crm/pipedrive/activities/create`: ```json theme={"theme":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} {"api_token": "", "body": {"subject": "Check press 7 overtemperature", "type": "task", "due_date": "2026-10-06", "deal_id": 42}} ``` When the alarm clears, publish `{"api_token": "...", "id": , "body": {"done": true}}` to `crm/pipedrive/activities/update`. **Open deals in a pipeline** Publish to `crm/pipedrive/deals/list`: ```json theme={"theme":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} {"api_token": "", "query": "status=open&pipeline_id=1&limit=50"} ``` v2 lists page with a cursor: pass `cursor=` in `query` for the next page. **Organization custom field** Get the field key from Pipedrive's organization fields (`GET /api/v2/organizationFields`, not packaged as an event), then publish to `crm/pipedrive/organizations/update`: ```json theme={"theme":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} {"api_token": "", "id": 7, "body": {"custom_fields": {"": 1280}}} ``` ## Webhooks into the broker `WebhookCreate` registers a Pipedrive webhook (`version` `"2.0"`, `event_action` such as `create` / `change` / `delete` / `*`, `event_object` such as `deal` / `activity` / `organization`). Pipedrive then POSTs each change to `subscription_url`. To receive it, run a separate server-mode connector and protect it with the webhook's `http_auth_user` / `http_auth_password`: ```lot wrap theme={"theme":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} DEFINE CONNECTOR PipedriveWebhookReceiver WITH TYPE REST_API ADD REST_API_CONFIG WITH ENABLE_SERVER "true" WITH SERVER_PORT '8443' WITH DEFAULT_CONTENT_TYPE "application/json" ADD EVENT PipedriveChange WITH ENDPOINT "/pipedrive/webhook" WITH METHOD "POST" WITH DESTINATION_TOPIC "crm/pipedrive/webhook/events" ``` The receiver must be reachable from the internet over HTTPS. It is not part of the client template. ## Rate limits Pipedrive gives each company a daily token budget, and every call spends tokens (the OpenAPI `x-token-cost`: 1 for a GET by id, 5 to 10 for lists, 20 for search, 5 for v2 writes, 10 for v1 writes). Short burst limits also apply, and excess calls get `429`. Responses carry `X-RateLimit-*` and `X-Daily-RateLimit-Token-*` headers. Avoid polling lists from fast timers; react to events or use webhooks instead. ## Related * [REST API Connectors](/latest/lot-language/routes/rest-api-routes) * [REST API Apps](/latest/lot-language/routes/rest-api-apps/index) * [Pipedrive API reference](https://developers.pipedrive.com/docs/api/v1) # Postman Echo REST API Source: https://docs.coreflux.org/latest/lot-language/routes/rest-api-apps/postman-echo Client-mode REST API connector that calls the public Postman Echo API from MQTT Technical Preview ## Overview [Postman Echo](https://postman-echo.com/) is a public HTTP echo service. Use this client-mode `REST_API` connector to prove MQTT-to-HTTP wiring (methods, path placeholders, headers, cookies, status codes, and response topics) without an API key. > [!TIP] > > Public `GET /get` does not need a token. The template still sends `Authorization: Bearer {value.json.api_token}` so the header pattern matches other apps. Leave `api_token` empty for demos, or drop that header. ## Basic Syntax ```lot wrap theme={"theme":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} # Coreflux REST_API template — Postman Echo # Generated from OpenAPI; credentials are placeholders only. # Docs: https://docs.coreflux.org/latest/lot-language/routes/rest-api-routes DEFINE CONNECTOR PostmanEchoRestApi WITH TYPE REST_API ADD REST_API_CONFIG WITH BASE_ADDRESS "https://postman-echo.com" WITH ENABLE_CLIENT "true" WITH USE_SSL "true" WITH REQUEST_TIMEOUT '30' WITH DEFAULT_CONTENT_TYPE "application/json" # Optional: WITH USERNAME "YOUR_USER" / WITH PASSWORD GET SECRET "APP_PASSWORD" ADD EVENT Get WITH SOURCE_TOPIC "integrations/postman-echo/request/Get" WITH DESTINATION_TOPIC "integrations/postman-echo/response/Get" WITH METHOD "GET" WITH ENDPOINT "/get" WITH HEADERS ADD "Accept" "application/json" ADD "Authorization" "Bearer {value.json.api_token}" ADD EVENT Post WITH SOURCE_TOPIC "integrations/postman-echo/request/Post" WITH DESTINATION_TOPIC "integrations/postman-echo/response/Post" WITH METHOD "POST" WITH ENDPOINT "/post" WITH HEADERS ADD "Content-Type" "application/json" ADD "Authorization" "Bearer {value.json.api_token}" WITH BODY "{payload}" ADD EVENT Put WITH SOURCE_TOPIC "integrations/postman-echo/request/Put" WITH DESTINATION_TOPIC "integrations/postman-echo/response/Put" WITH METHOD "PUT" WITH ENDPOINT "/put" WITH HEADERS ADD "Content-Type" "application/json" ADD "Authorization" "Bearer {value.json.api_token}" WITH BODY "{payload}" ADD EVENT Delete WITH SOURCE_TOPIC "integrations/postman-echo/request/Delete" WITH DESTINATION_TOPIC "integrations/postman-echo/response/Delete" WITH METHOD "DELETE" WITH ENDPOINT "/delete" WITH HEADERS ADD "Accept" "application/json" ADD "Authorization" "Bearer {value.json.api_token}" ADD EVENT Headers WITH SOURCE_TOPIC "integrations/postman-echo/request/Headers" WITH DESTINATION_TOPIC "integrations/postman-echo/response/Headers" WITH METHOD "GET" WITH ENDPOINT "/headers" WITH HEADERS ADD "Accept" "application/json" ADD "Authorization" "Bearer {value.json.api_token}" ADD EVENT Statuscode WITH SOURCE_TOPIC "integrations/postman-echo/request/Statuscode" WITH DESTINATION_TOPIC "integrations/postman-echo/response/Statuscode" WITH METHOD "GET" WITH ENDPOINT "/status/{value.json.code}" WITH HEADERS ADD "Accept" "application/json" ADD "Authorization" "Bearer {value.json.api_token}" ADD EVENT Timenow WITH SOURCE_TOPIC "integrations/postman-echo/request/Timenow" WITH DESTINATION_TOPIC "integrations/postman-echo/response/Timenow" WITH METHOD "GET" WITH ENDPOINT "/time/now" WITH HEADERS ADD "Accept" "application/json" ADD "Authorization" "Bearer {value.json.api_token}" ADD EVENT Cookies WITH SOURCE_TOPIC "integrations/postman-echo/request/Cookies" WITH DESTINATION_TOPIC "integrations/postman-echo/response/Cookies" WITH METHOD "GET" WITH ENDPOINT "/cookies" WITH HEADERS ADD "Accept" "application/json" ADD "Authorization" "Bearer {value.json.api_token}" ``` ## Events | Event | Method | Endpoint | Request topic | Response topic | | - | - | - | - | - | | Get | GET | `/get` | `integrations/postman-echo/request/Get` | `integrations/postman-echo/response/Get` | | Post | POST | `/post` | `integrations/postman-echo/request/Post` | `integrations/postman-echo/response/Post` | | Put | PUT | `/put` | `integrations/postman-echo/request/Put` | `integrations/postman-echo/response/Put` | | Delete | DELETE | `/delete` | `integrations/postman-echo/request/Delete` | `integrations/postman-echo/response/Delete` | | Headers | GET | `/headers` | `integrations/postman-echo/request/Headers` | `integrations/postman-echo/response/Headers` | | Statuscode | GET | `/status/{value.json.code}` | `integrations/postman-echo/request/Statuscode` | `integrations/postman-echo/response/Statuscode` | | Timenow | GET | `/time/now` | `integrations/postman-echo/request/Timenow` | `integrations/postman-echo/response/Timenow` | | Cookies | GET | `/cookies` | `integrations/postman-echo/request/Cookies` | `integrations/postman-echo/response/Cookies` | ## Auth None required. Base address is `https://postman-echo.com`. Optional Bearer token is a placeholder only. Prefer `GET SECRET` if you later point the connector at a private echo. Never commit real tokens. ## Try it 1. Add the connector on your broker (`-addConnector` with the LoT above). 2. Publish `{}` to `integrations/postman-echo/request/Get`. 3. Subscribe to `integrations/postman-echo/response/Get`. Postman Echo echoes the request as JSON. 4. Status check: publish `{"code":"200"}` to `integrations/postman-echo/request/Statuscode`. 5. POST/PUT: publish a JSON body to `integrations/postman-echo/request/Post` or `.../Put`. The connector sends it as `{payload}`. ## Related * [REST API Connectors](/latest/lot-language/routes/rest-api-routes) * [REST API Apps](/latest/lot-language/routes/rest-api-apps) # Project Haystack Source: https://docs.coreflux.org/latest/lot-language/routes/rest-api-apps/project-haystack LoT connector for the Project Haystack HTTP API: read points, hisRead and hisWrite history, pointWrite, watches, nav, defs and invokeAction over JSON grids Technical Preview ## Overview **Project Haystack** is the industry standard for semantic tagging of equipment, points and histories in buildings and industrial sites. Compliant servers (SkyFoundry **SkySpark**, J2 Innovations **FIN Framework**, open-source **Haxall**, Tridium Niagara with **nhaystack**, **Haystack Connect**, and others) expose the same HTTP API: ops such as `read`, `hisRead`, `hisWrite`, `pointWrite` and `watchSub` exchange **grids** encoded as Zinc, JSON or Trio. The **ProjectHaystackApi** connector maps 15 MQTT topics onto that standard ops set (4 meta GETs, 11 POSTs including 3 writes). A Coreflux broker can discover tagged points (`point and temp`), pull or push histories, command writable setpoints through the priority array, and poll watches for live `curVal` changes. Each event publishes the server reply on `/response`. | Event | Topic | Request | Use | | | | | | - | - | - | - | - | - | - | - | | `About` | `energy/haystack/about/get` | `GET /about` | Server / project about grid (haystackVersion, tz, serverName, productName, ...); empty request | | | | | | `ListOps` | `energy/haystack/ops/list` | `GET /ops` | Op defs available on this server (standard + vendor extensions) | | | | | | `ListFiletypes` | `energy/haystack/filetypes/list` | `GET /filetypes` | Supported MIME encodings (text/zinc, application/json, text/trio, text/csv, ...) | | | | | | `ListLibs` | `energy/haystack/libs/list` | `GET /libs` | Lib defs in the current namespace | | | | | | `Close` | `energy/haystack/auth/close` | `POST /close` | Invalidate the bearer authToken and close the SCRAM session; body empty grid | | | | | | `Read` | `energy/haystack/read` | `POST /read` | Entities by filter (rows: filter, optional limit) or by id (rows of id refs); e.g. filter=point and temp | | | | | | `Nav` | `energy/haystack/nav` | `POST /nav` | Navigation tree children; body row navId (null/omit for root) | | | | | | `Defs` | `energy/haystack/defs` | `POST /defs` | Def dicts; empty grid for all, or filter/limit row | | | | | | `HisRead` | `energy/haystack/his/read` | `POST /hisRead` | Historized samples: single (row id+range) or batch (meta.range + id rows); range today | yesterday | \{date} | \{date},\{date} | \{dateTime},\{dateTime} | | `HisWrite` | `energy/haystack/his/write` | `POST /hisWrite` | WRITE: WRITE: post history samples; single (meta.id + ts/val rows) or batch (v0/v1 cols with id in col meta) | | | | | | `PointWrite` | `energy/haystack/point/write` | `POST /pointWrite` | WRITE: WRITE: read priority array (row id only) or write a level (id, level 1-17, val or null to auto, optional who/duration) | | | | | | `WatchSub` | `energy/haystack/watch/sub` | `POST /watchSub` | Create watch (meta watchDis, optional lease) or add ids to existing (meta watchId); response meta.watchId | | | | | | `WatchPoll` | `energy/haystack/watch/poll` | `POST /watchPoll` | Poll changes for watchId (meta); add refresh Marker for full refresh | | | | | | `WatchUnsub` | `energy/haystack/watch/unsub` | `POST /watchUnsub` | Remove ids from a watch, or close it (meta watchId + close Marker) | | | | | | `InvokeAction` | `energy/haystack/action/invoke` | `POST /invokeAction` | WRITE: WRITE: invoke a user action; meta id + action name, row = args | | | | | Endpoints are relative to `BASE_ADDRESS`, which **already includes** the project or service path (see below). ## Base URL (hosts differ) Set `WITH BASE_ADDRESS` to your server root **including** the project / service segment. Ops are then `/about`, `/read`, `/hisRead`, …: | Product | Typical base | | - | - | | SkySpark / FIN / Haxall | `https://{host}/api/{project}` e.g. `https://skyspark.example.com/api/demo` | | nhaystack (Niagara) | `https://{host}/{service}` e.g. `https://niagara.example.com/haystack` | | Generic / Haystack Connect | `https://{host}/haystack` | The placeholder in the template is `https://haystack.example.com/api/demo`. Trailing slash is optional. ## Authentication Haystack uses a pluggable HTTP auth protocol. Compliant servers must support **SCRAM with SHA-256**: 1. Client sends a hello (username) to a protected URI. 2. Server answers `401` with `WWW-Authenticate: SCRAM ...`. 3. After the SCRAM exchange the server returns `authToken` in `Authentication-Info`. 4. Subsequent calls send `Authorization: BEARER authToken=`. On **nhaystack**, operators often enable `HTTPBasicScheme` and send `Authorization: Basic ` over HTTPS instead. The multi-step SCRAM hello is **not** a connector event (the REST\_API client does one HTTP call per MQTT publish). Obtain a token with a Haystack client library ([phable](https://phable.dev/), haystack-java, pyhaystack, …) or a small helper script, store the current header value in Hub secret `HAYSTACK_AUTHORIZATION`, and map it with `GET SECRET`. Call `Close` to invalidate the token when done. Every event sends `Authorization: {value.json.authorization}`. ## Formats (JSON grids) The connector uses `Accept` / `Content-Type: application/json` (Haystack JSON grids with `"_kind":"grid"`). Zinc (`text/zinc`) is the HTTP API default; Trio and CSV are also standardized. Change the headers if your tooling prefers Zinc. Request errors that the server can parse still return **HTTP 200** with an **error grid** (`meta.err` Marker + `dis`). Always check for `err` in the reply before treating rows as data. ## Request payload | Field | Used by | Example | | - | - | - | | `authorization` | all events | `(GET SECRET "HAYSTACK_AUTHORIZATION")` → `BEARER authToken=…` or `Basic …` | | `body` | all POST events | Haystack JSON grid (string or object the broker serializes) | ### Example grids Empty grid (Close, Nav root, Defs all): ```json theme={"theme":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} {"_kind":"grid","meta":{"ver":"3.0"},"cols":[{"name":"empty"}],"rows":[]} ``` Read by filter: ```json theme={"theme":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} {"_kind":"grid","meta":{"ver":"3.0"},"cols":[{"name":"filter"},{"name":"limit"}],"rows":[{"filter":"point and temp","limit":100}]} ``` HisRead today: ```json theme={"theme":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} {"_kind":"grid","meta":{"ver":"3.0"},"cols":[{"name":"id"},{"name":"range"}],"rows":[{"id":{"_kind":"ref","val":"zoneTemp"},"range":"today"}]} ``` PointWrite level 16: ```json theme={"theme":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} {"_kind":"grid","meta":{"ver":"3.0"},"cols":[{"name":"id"},{"name":"level"},{"name":"val"},{"name":"who"}],"rows":[{"id":{"_kind":"ref","val":"sp"},"level":16,"val":{"_kind":"number","val":21,"unit":"°C"},"who":"Coreflux"}]} ``` ## Example actions ```lot theme={"theme":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} # Example actions for the ProjectHaystackApi route (docs page). Not part of the route template. # Secret: HAYSTACK_AUTHORIZATION = "BEARER authToken=" after SCRAM hello (or "Basic " on nhaystack). # Inline JSON object literals are not supported when publishing to a topic, so the request payload is a COLLAPSED # model sent with PUBLISH MODEL. For POST ops the body field is the Haystack JSON grid as a string. DEFINE MODEL HaystackGetRequest COLLAPSED ADD STRING "authorization" DEFINE MODEL HaystackGridRequest COLLAPSED ADD STRING "authorization" ADD STRING "body" DEFINE ACTION HaystackAboutHourly ON EVERY 1 HOUR DO PUBLISH MODEL HaystackGetRequest TO "energy/haystack/about/get" WITH authorization = (GET SECRET "HAYSTACK_AUTHORIZATION") DEFINE ACTION HaystackReadPointsEvery5Minutes ON EVERY 5 MINUTES DO PUBLISH MODEL HaystackGridRequest TO "energy/haystack/read" WITH authorization = (GET SECRET "HAYSTACK_AUTHORIZATION") body = "{\"_kind\":\"grid\",\"meta\":{\"ver\":\"3.0\"},\"cols\":[{\"name\":\"filter\"},{\"name\":\"limit\"}],\"rows\":[{\"filter\":\"point and temp\",\"limit\":100}]}" DEFINE ACTION HaystackHisReadDaily ON EVERY 1 DAY DO PUBLISH MODEL HaystackGridRequest TO "energy/haystack/his/read" WITH authorization = (GET SECRET "HAYSTACK_AUTHORIZATION") body = "{\"_kind\":\"grid\",\"meta\":{\"ver\":\"3.0\"},\"cols\":[{\"name\":\"id\"},{\"name\":\"range\"}],\"rows\":[{\"id\":{\"_kind\":\"ref\",\"val\":\"zoneTemp\"},\"range\":\"today\"}]}" ``` ## Writes `HisWrite`, `PointWrite` (when the body includes `level`) and `InvokeAction` change server state. Restrict which MQTT clients may publish to `energy/haystack/his/write`, `energy/haystack/point/write` and `energy/haystack/action/invoke`. `PointWrite` with an id-only body only **reads** the priority array. ## Watches 1. `WatchSub` with `watchDis` (+ optional `lease`) and id rows → response `meta.watchId`. 2. Poll with `WatchPoll` (`meta.watchId`; add `refresh` Marker for a full refresh). 3. `WatchUnsub` with `close` Marker (or remove individual ids). Lease expiry on the server closes the watch if you stop polling. ## Not packaged SkySpark / Haxall extensions such as `eval` and `commit`, other vendor ops beyond the [standard Ops chapter](https://project-haystack.org/doc/docHaystack/Ops), and the SCRAM hello handshake as a connector event. ## Sources * [Ops](https://project-haystack.org/doc/docHaystack/Ops) · [HTTP API](https://project-haystack.org/doc/docHaystack/HttpApi) · [Authentication](https://project-haystack.org/doc/docHaystack/Auth) · [Ops appendix](https://project-haystack.org/doc/appendix/ops) * Project Haystack does not publish an official OpenAPI file. # Prometheus, Alertmanager and Pushgateway Source: https://docs.coreflux.org/latest/lot-language/routes/rest-api-apps/prometheus-alertmanager-and-pushgateway LoT connector for Prometheus, Alertmanager and Pushgateway: run PromQL instant and range queries, list scrape targets, push metrics and list alerts Technical Preview ## Overview A client-mode connector for a **curated observability / ITSM** slice of the [Prometheus HTTP API](https://prometheus.io/docs/prometheus/latest/querying/api/), [Pushgateway](https://prometheus.io/docs/instrumenting/pushing/), and [Alertmanager API](https://prometheus.io/docs/alerting/latest/management_api/): | Catalog connector event | Entity / path | Notes | | - | - | - | | GET query | `GET /api/v1/query?query=` | PromQL instant query; typically **no auth** | | GET query\_range | `GET /api/v1/query_range` | PromQL range (`start` / `end` / `step`) | | GET targets | `GET /api/v1/targets` | Scrape target discovery | | POST Pushgateway metrics | `POST /metrics/job/{job}` | text/plain OpenMetrics / Prometheus exposition; **BASE = Pushgateway :9091** | | GET Alertmanager alerts | `GET /api/v2/alerts` | Alertmanager API v2; **BASE = Alertmanager :9093** | Default `BASE_ADDRESS` is `http://prometheus.example.com:9090` (replace with your Prometheus host). Topics use `itsm/prometheus//`. > [!NOTE] > > Prometheus, Alertmanager, and Pushgateway are usually separate hosts/ports (`:9090` / `:9093` / `:9091`). Point `BASE_ADDRESS` at the matching host for each event, or deploy separate connector copies. For HTTPS reverse proxies set `WITH USE_SSL "true"`. Do not put a trailing slash on `BASE_ADDRESS`. > [!NOTE] > > Prefer Hub secrets when a reverse proxy requires Basic or Bearer. ## Basic Syntax ```lot wrap theme={"theme":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} # Coreflux REST_API template — Prometheus / Alertmanager / Pushgateway curated slice # Auth: none by default; optional Bearer/Basic via reverse proxy # BASE_ADDRESS: http://prometheus.example.com:9090 # PushMetrics → Pushgateway :9091; ListAlerts → Alertmanager :9093 DEFINE CONNECTOR PrometheusRestApi WITH TYPE REST_API ADD REST_API_CONFIG WITH BASE_ADDRESS "http://prometheus.example.com:9090" WITH ENABLE_CLIENT "true" WITH USE_SSL "false" WITH REQUEST_TIMEOUT '60' WITH DEFAULT_CONTENT_TYPE "application/json" ADD EVENT Queryinstant WITH SOURCE_TOPIC "itsm/prometheus/query/instant" WITH DESTINATION_TOPIC "itsm/prometheus/query/instant/response" WITH METHOD "GET" WITH ENDPOINT "/api/v1/query?query={value.json.query}&time={value.json.time}" WITH HEADERS ADD "Accept" "application/json" ADD EVENT Queryrange WITH SOURCE_TOPIC "itsm/prometheus/query/range" WITH DESTINATION_TOPIC "itsm/prometheus/query/range/response" WITH METHOD "GET" WITH ENDPOINT "/api/v1/query_range?query={value.json.query}&start={value.json.start}&end={value.json.end}&step={value.json.step}" WITH HEADERS ADD "Accept" "application/json" ADD EVENT Listtargets WITH SOURCE_TOPIC "itsm/prometheus/targets/list" WITH DESTINATION_TOPIC "itsm/prometheus/targets/list/response" WITH METHOD "GET" WITH ENDPOINT "/api/v1/targets" WITH HEADERS ADD "Accept" "application/json" ADD EVENT Pushmetrics WITH SOURCE_TOPIC "itsm/prometheus/pushgateway/push" WITH DESTINATION_TOPIC "itsm/prometheus/pushgateway/push/response" WITH METHOD "POST" WITH ENDPOINT "/metrics/job/{value.json.job}" WITH HEADERS ADD "Accept" "application/json" ADD "Content-Type" "text/plain" WITH BODY "{payload}" ADD EVENT Listalerts WITH SOURCE_TOPIC "itsm/prometheus/alertmanager/alerts/list" WITH DESTINATION_TOPIC "itsm/prometheus/alertmanager/alerts/list/response" WITH METHOD "GET" WITH ENDPOINT "/api/v2/alerts" WITH HEADERS ADD "Accept" "application/json" ``` ## Events | Event | Method | Endpoint | Request topic | Response topic | | - | - | - | - | - | | Queryinstant | GET | `/api/v1/query?query=&time=` | `itsm/prometheus/query/instant` | `itsm/prometheus/query/instant/response` | | Queryrange | GET | `/api/v1/query_range?query=&start=&end=&step=` | `itsm/prometheus/query/range` | `itsm/prometheus/query/range/response` | | Listtargets | GET | `/api/v1/targets` | `itsm/prometheus/targets/list` | `itsm/prometheus/targets/list/response` | | Pushmetrics | POST | `/metrics/job/{job}` | `itsm/prometheus/pushgateway/push` | `itsm/prometheus/pushgateway/push/response` | | Listalerts | GET | `/api/v2/alerts` | `itsm/prometheus/alertmanager/alerts/list` | `itsm/prometheus/alertmanager/alerts/list/response` | ## Auth **None (default):** 1. Most Prometheus installs expose `/api/v1/*` without auth on private networks. 2. Leave Authorization headers off for Queryinstant / Queryrange / Listtargets. **Bearer via reverse proxy (optional):** 1. Terminate TLS + auth in nginx / Traefik / similar in front of Prometheus / Alertmanager / Pushgateway. 2. Pass `token` on the MQTT request payload: * `Authorization: Bearer {value.json.token}` 3. Store the token in Hub secrets / `GET SECRET "PROMETHEUS_TOKEN"`. Never commit real credentials. **Basic via reverse proxy (optional):** 1. Configure `WITH USERNAME` / `WITH PASSWORD GET SECRET "PROMETHEUS_PASSWORD"` on `REST_API_CONFIG`. 2. Remove any Bearer headers from events. ## Install 1. Copy `PrometheusRestApi.lot` into your broker examples / connectors folder (no `ADD METADATA` in the deployable file). 2. Set `BASE_ADDRESS` to your Prometheus URL (use `https://…` and `WITH USE_SSL "true"` when TLS terminates on the API host or proxy). 3. `-addConnector` the file (or paste into Hub). 4. For Pushgateway and Alertmanager, either change BASE and re-deploy, or keep separate connector copies with the matching BASE. 5. Subscribe to the `/response` topics; publish JSON to the request topics. Hub template listing uses `hub-metadata.lot` (`WITH STATUS "beta"`, icon, doclink) — keep that file separate from the deployable connector. ## Example payloads **Instant query** ```json theme={"theme":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} { "query": "up", "time": "" } ``` **Range query** ```json theme={"theme":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} { "query": "rate(http_requests_total[5m])", "start": "2026-10-04T00:00:00Z", "end": "2026-10-04T01:00:00Z", "step": "1m" } ``` **List targets** ```json theme={"theme":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} {} ``` **Pushgateway push** — MQTT JSON must supply `job`; body `{payload}` is raw text/plain exposition (example metrics body): ```text theme={"theme":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} # TYPE coreflux_temperature gauge coreflux_temperature{instance="factory"} 1 ``` **List Alertmanager alerts** (BASE = Alertmanager) ```json theme={"theme":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} {} ``` ## Complementary surfaces (not client events here) | Surface | Role | | - | - | | Alertmanager webhook receiver | Inbound: Alertmanager `webhook_config` POSTs alert payloads to a Coreflux REST\_API **server** connector or HTTP ingress | | Remote write | Prometheus remote\_write (protobuf/snappy) — different protocol; not this JSON/text HTTP slice | | OpenMetrics | Exposition format used by Pushgateway bodies and scrapes | ## References * [Prometheus HTTP API](https://prometheus.io/docs/prometheus/latest/querying/api/) * [Pushing metrics (Pushgateway)](https://prometheus.io/docs/instrumenting/pushing/) * [Alertmanager HTTP API](https://prometheus.io/docs/alerting/latest/management_api/) * [Alertmanager configuration (webhooks)](https://prometheus.io/docs/alerting/latest/configuration/) * [Remote write spec](https://prometheus.io/docs/specs/remote_write_spec/) * [OpenMetrics](https://github.com/OpenObservability/OpenMetrics) * Public demos: [demo.promlabs.com](https://demo.promlabs.com), [prometheus.demo.prometheus.io](https://prometheus.demo.prometheus.io) # PTC Windchill (Windchill REST Services) Source: https://docs.coreflux.org/latest/lot-language/routes/rest-api-apps/ptc-windchill LoT connector for PTC Windchill REST Services: read parts, BOMs, CAD drawings, documents and change notices, and raise problem reports from quality events Technical Preview ## Overview [PTC Windchill](https://www.ptc.com/en/products/windchill) is the PLM system that owns parts, BOMs, CAD documents, drawings and change processes for many manufacturers. This connector lets shop-floor logic on MQTT look up a part and its released structure, build a kitting list from the multi-level components report, fetch part attachments and drawings for a work instruction screen, watch change notices, and raise a problem report or variance from a quality event, without custom code. **PtcWindchillApi** (`PtcWindchillRestApi.lot`) is one client connector on `https://YOUR_WINDCHILL_SERVER` with 17 events: 1 CSRF token call, 14 reads and 2 writes. No event updates, deletes, revises or checks out anything. > [!NOTE] > > Windchill REST Services (WRS) is an OData v4 API under `/Windchill/servlet/odata//`. This template uses the Product Management (`ProdMgmt`), Document Management (`DocMgmt`), Change Management (`ChangeMgmt`) and common (`PTC`) domains. Every server publishes its own `$metadata` per domain (for example `/Windchill/servlet/odata/ProdMgmt/$metadata`), which also lists your custom types and soft attributes. The events follow PTC's public [Windchill REST Services 2.7 guide](https://support.ptc.com/help/windchill_rest_services/r2.7/en/windchill_rest_services/examples_lp_prodmgmt.html). To add another entity set or action, copy an event and change the path. ## Server host Windchill is installed per customer, or runs as Windchill+ (SaaS). Replace `YOUR_WINDCHILL_SERVER` with your Windchill web server host, with no trailing slash, and add the port when it does not use 443. To pin a domain version, insert it after `odata/` (for example `/Windchill/servlet/odata/v5/ProdMgmt/`); without a version the server uses its default. ## Authentication > [!WARNING] > > WRS calls act as the Windchill user behind the credentials. Keep them in Hub secrets, map them in with `GET SECRET`, and never publish the authorization value or the CSRF nonce on a topic other clients can read. * **HTTP Basic (default).** Windchill relies on web server authentication. Store the full header value, `Basic `, as the secret `WINDCHILL_AUTHORIZATION` and send it as `"authorization"` in every payload. * **OAuth 2.0.** When Windchill is set up as an OAuth resource server (client credentials for machine clients), send `"authorization": "Bearer "` and change every endpoint prefix from `/Windchill/servlet/odata/` to `/Windchill/oauth/servlet/odata/` ([WRS OAuth authorization](https://support.ptc.com/help/windchill_rest_services/r2.7/en/windchill_rest_services/oauth_for_wrs.html)). Getting the token from your authorization server is outside this connector. * **CSRF nonce.** Every `POST` needs the header `CSRF_NONCE`. Publish on `mes/ptc-windchill/csrf-token/get`, read `NonceValue` from the reply and send it as `"nonce"`. The nonce belongs to the server session, so refresh it when POST calls start failing. ## Payload conventions * `id` is a Windchill object ID, for example `OR:wt.part.WTPart:191559` (find it with `QueryParts` by number). * `filter` is an OData `$filter` expression, **URL-encoded** because the broker puts it straight into the URL: `Number%20eq%20'0000000007'` or `startswith(Number,'00')`. Set the request header `PTC-WildcardSearch: true` (add it to the event) to use `*` wildcards. * `request` is the JSON body sent unchanged on `POST` events. ```json theme={"theme":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} { "authorization": "Basic ", "filter": "Number%20eq%20'0000000007'", "top": "10" } ``` ## Events | Event | Method | Path (after `/Windchill/servlet/odata/`) | Topic | What it does | | - | - | - | - | - | | `GetCsrfToken` | GET | `PTC/GetCSRFToken()` | `mes/ptc-windchill/csrf-token/get` | CSRF nonce (NonceKey CSRF\_NONCE, NonceValue). Send NonceValue as nonce on every POST event | | `QueryParts` | GET | `ProdMgmt/Parts?$filter={value.json.filter}&$top={value.json.top}` | `mes/ptc-windchill/parts/query` | Parts by OData $filter (URL-encoded, for example Number eq '...' or startswith(Number,'00')) and $top | | `GetPart` | GET | `ProdMgmt/Parts('{value.json.id}')?$expand=Uses` | `mes/ptc-windchill/part/get` | One part by OID with its part usage links (Uses) | | `GetPartStructure` | POST | `ProdMgmt/Parts('{value.json.id}')/PTC.ProdMgmt.GetPartStructure?$expand=Components($levels=max)` | `mes/ptc-windchill/part-structure/get` | Multi-level BOM (part structure) of a part with inline NavigationCriteria (config spec, view, plant filters); send \{} for the default filter | | `GetComponentsReport` | POST | `ProdMgmt/Parts('{value.json.id}')/PTC.ProdMgmt.GetMultiLevelComponentsReport?$select=Quantity,Unit,Name,Number&$expand=Part($select=Identity,Name)` | `mes/ptc-windchill/components-report/get` | Consolidated multi-level components list (quantity, unit) for kitting and material staging; replaces deprecated GetPartsList | | `GetPartAttachments` | GET | `ProdMgmt/Parts('{value.json.id}')/Attachments` | `mes/ptc-windchill/part-attachments/get` | Attachments (secondary content: file name, format, download URL) of a part, for work instruction display | | `GetPartCadDocuments` | GET | `ProdMgmt/Parts('{value.json.id}')/PartDocAssociations?$expand=RelatedCADDoc($select=Identity)` | `mes/ptc-windchill/part-cad-documents/get` | CAD documents and drawings associated with a part | | `QueryDocuments` | GET | `DocMgmt/Documents?$filter={value.json.filter}&$top={value.json.top}` | `mes/ptc-windchill/documents/query` | Documents by OData \$filter (URL-encoded), for example work instructions by number prefix | | `GetDocumentStructure` | GET | `DocMgmt/Documents('{value.json.id}')/DocUsageLinks?$expand=DocUsedBy,DocUses` | `mes/ptc-windchill/document-structure/get` | Document structure (usage links to parent and child documents) | | `ListProblemReports` | GET | `ChangeMgmt/ProblemReports` | `mes/ptc-windchill/problem-reports/list` | All problem reports visible to the user | | `GetProblemReport` | GET | `ChangeMgmt/ProblemReports('{value.json.id}')` | `mes/ptc-windchill/problem-report/get` | One problem report by OID (state, priority, category) | | `GetProblemReportAffectedObjects` | GET | `ChangeMgmt/ProblemReports('{value.json.id}')/CIAffectLinks?$expand=AffectedObjects` | `mes/ptc-windchill/problem-report-affected/get` | Affected parts / documents of a problem report | | `CreateProblemReport` | POST | `ChangeMgmt/ProblemReports` | `mes/ptc-windchill/problem-report/create` | Write. problem report from a quality event, optionally with affected parts ([Changeables@odata.bind](mailto:Changeables@odata.bind)) in the product context | | `CreateVariance` | POST | `ChangeMgmt/Variances` | `mes/ptc-windchill/variance/create` | Write. variance (deviation / waiver) request, for example to run a lot with an approved deviation | | `ListChangeRequests` | GET | `ChangeMgmt/ChangeRequests?$expand=AffectedObjects` | `mes/ptc-windchill/change-requests/list` | Change requests with their affected objects | | `ListChangeNotices` | GET | `ChangeMgmt/ChangeNotices` | `mes/ptc-windchill/change-notices/list` | Change notices (released engineering changes) to flag BOM / routing updates on the shop floor | | `GetChangeNotice` | GET | `ChangeMgmt/ChangeNotices('{value.json.id}')` | `mes/ptc-windchill/change-notice/get` | One change notice by OID | Replies arrive on `/response`. ### Part structure and kitting list `GetPartStructure` returns the multi-level BOM; `GetComponentsReport` returns one consolidated line per component with total quantity and unit, which suits kitting and line-side staging. Both take a `NavigationCriteria` body (config spec, view, plant filters). Send `{}` to use the default filter, or the ID of a saved filter: `{"NavigationCriteria": {"ID": "OR:wt.filter.NavigationCriteria:..."}}`. ```json theme={"theme":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} { "authorization": "Basic ", "id": "OR:wt.part.WTPart:191559", "nonce": "", "request": { "NavigationCriteria": { "ApplicableType": "PTC.ProdMgmt.Part", "ApplyToTopLevelObject": true, "Centricity": false, "ConfigSpecs": [ { "@odata.type": "#PTC.NavCriteria.WTPartStandardConfigSpec", "WorkingIncluded": false, "View": "Manufacturing" } ], "Filters": [], "HideUnresolvedDependents": false, "UseDefaultForUnresolved": false } } } ``` ### Problem report from a quality event `CreateProblemReport` creates a standalone problem report in a product or library context. Add affected parts with `Changeables@odata.bind`. Category, priority and life cycle values depend on your Change Management setup. ```json theme={"theme":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} { "authorization": "Basic ", "nonce": "", "request": { "Name": "Press 01 dimensional NOK", "Description": "Bore diameter out of tolerance on 3 of 20 parts (station 03)", "Category": { "Value": "DESIGN_ISSUE" }, "IssuePriority": { "Value": "HIGH" }, "Context@odata.bind": "Containers('OR:wt.pdmlink.PDMLinkProduct:122008')", "Changeables@odata.bind": [ "Changeables('OR:wt.part.WTPart:122556')" ] } } ``` ## Connector ```lot theme={"theme":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} # Coreflux REST_API template — PTC Windchill (beta): read parts, part structures (multi-level BOM), consolidated # component lists, attachments and CAD / drawing associations, query documents, read problem reports, change requests # and change notices, and from a quality event create a problem report (with affected parts) or a variance, over # Windchill REST Services (WRS, OData v4) at /Windchill/servlet/odata/. # Docs: https://docs.coreflux.org/latest/lot-language/routes/rest-api-apps/ptc-windchill # # Host: Windchill is installed per customer (or Windchill+ SaaS). Replace YOUR_WINDCHILL_SERVER with the Windchill web # server host (add :port when it does not share 443). # Auth: HTTP Basic by default: publish "authorization": "Basic ". OAuth 2.0 sites (Windchill as # resource server, client credentials) send "Bearer " and must call /Windchill/oauth/servlet/odata/ instead: # replace the endpoint prefix. Keep credentials in Hub secrets. # CSRF: POST events need "nonce": NonceValue from GetCsrfToken (header CSRF_NONCE). The nonce belongs to the session. # Payloads: OIDs go in "id" (for example "OR:wt.part.WTPart:191559"); $filter values must be URL-encoded; "request" is # the JSON body sent unchanged. # QueryParts {"authorization": "Basic ...", "filter": "Number%20eq%20'0000000007'", "top": "10"} # DEFINE CONNECTOR PtcWindchillApi WITH TYPE REST_API ADD REST_API_CONFIG WITH BASE_ADDRESS "https://YOUR_WINDCHILL_SERVER" WITH ENABLE_CLIENT "true" WITH USE_SSL "true" WITH REQUEST_TIMEOUT '60' WITH DEFAULT_CONTENT_TYPE "application/json" # CSRF nonce (NonceKey CSRF_NONCE, NonceValue). Send NonceValue as nonce on every POST event ADD EVENT GetCsrfToken WITH SOURCE_TOPIC "mes/ptc-windchill/csrf-token/get" WITH DESTINATION_TOPIC "mes/ptc-windchill/csrf-token/get/response" WITH METHOD "GET" WITH ENDPOINT "/Windchill/servlet/odata/PTC/GetCSRFToken()" WITH HEADERS ADD "Accept" "application/json" ADD "Authorization" "{value.json.authorization}" # Parts by OData $filter (URL-encoded, for example Number eq '...' or startswith(Number,'00')) and $top ADD EVENT QueryParts WITH SOURCE_TOPIC "mes/ptc-windchill/parts/query" WITH DESTINATION_TOPIC "mes/ptc-windchill/parts/query/response" WITH METHOD "GET" WITH ENDPOINT "/Windchill/servlet/odata/ProdMgmt/Parts?$filter={value.json.filter}&$top={value.json.top}" WITH HEADERS ADD "Accept" "application/json" ADD "Authorization" "{value.json.authorization}" # One part by OID with its part usage links (Uses) ADD EVENT GetPart WITH SOURCE_TOPIC "mes/ptc-windchill/part/get" WITH DESTINATION_TOPIC "mes/ptc-windchill/part/get/response" WITH METHOD "GET" WITH ENDPOINT "/Windchill/servlet/odata/ProdMgmt/Parts('{value.json.id}')?$expand=Uses" WITH HEADERS ADD "Accept" "application/json" ADD "Authorization" "{value.json.authorization}" # Multi-level BOM (part structure) of a part with inline NavigationCriteria (config spec, view, plant filters); send {} for the default filter ADD EVENT GetPartStructure WITH SOURCE_TOPIC "mes/ptc-windchill/part-structure/get" WITH DESTINATION_TOPIC "mes/ptc-windchill/part-structure/get/response" WITH METHOD "POST" WITH ENDPOINT "/Windchill/servlet/odata/ProdMgmt/Parts('{value.json.id}')/PTC.ProdMgmt.GetPartStructure?$expand=Components($levels=max)" WITH HEADERS ADD "Accept" "application/json" ADD "Authorization" "{value.json.authorization}" ADD "Content-Type" "application/json" ADD "CSRF_NONCE" "{value.json.nonce}" WITH BODY "{value.json.request}" # Consolidated multi-level components list (quantity, unit) for kitting and material staging; replaces deprecated GetPartsList ADD EVENT GetComponentsReport WITH SOURCE_TOPIC "mes/ptc-windchill/components-report/get" WITH DESTINATION_TOPIC "mes/ptc-windchill/components-report/get/response" WITH METHOD "POST" WITH ENDPOINT "/Windchill/servlet/odata/ProdMgmt/Parts('{value.json.id}')/PTC.ProdMgmt.GetMultiLevelComponentsReport?$select=Quantity,Unit,Name,Number&$expand=Part($select=Identity,Name)" WITH HEADERS ADD "Accept" "application/json" ADD "Authorization" "{value.json.authorization}" ADD "Content-Type" "application/json" ADD "CSRF_NONCE" "{value.json.nonce}" WITH BODY "{value.json.request}" # Attachments (secondary content: file name, format, download URL) of a part, for work instruction display ADD EVENT GetPartAttachments WITH SOURCE_TOPIC "mes/ptc-windchill/part-attachments/get" WITH DESTINATION_TOPIC "mes/ptc-windchill/part-attachments/get/response" WITH METHOD "GET" WITH ENDPOINT "/Windchill/servlet/odata/ProdMgmt/Parts('{value.json.id}')/Attachments" WITH HEADERS ADD "Accept" "application/json" ADD "Authorization" "{value.json.authorization}" # CAD documents and drawings associated with a part ADD EVENT GetPartCadDocuments WITH SOURCE_TOPIC "mes/ptc-windchill/part-cad-documents/get" WITH DESTINATION_TOPIC "mes/ptc-windchill/part-cad-documents/get/response" WITH METHOD "GET" WITH ENDPOINT "/Windchill/servlet/odata/ProdMgmt/Parts('{value.json.id}')/PartDocAssociations?$expand=RelatedCADDoc($select=Identity)" WITH HEADERS ADD "Accept" "application/json" ADD "Authorization" "{value.json.authorization}" # Documents by OData $filter (URL-encoded), for example work instructions by number prefix ADD EVENT QueryDocuments WITH SOURCE_TOPIC "mes/ptc-windchill/documents/query" WITH DESTINATION_TOPIC "mes/ptc-windchill/documents/query/response" WITH METHOD "GET" WITH ENDPOINT "/Windchill/servlet/odata/DocMgmt/Documents?$filter={value.json.filter}&$top={value.json.top}" WITH HEADERS ADD "Accept" "application/json" ADD "Authorization" "{value.json.authorization}" # Document structure (usage links to parent and child documents) ADD EVENT GetDocumentStructure WITH SOURCE_TOPIC "mes/ptc-windchill/document-structure/get" WITH DESTINATION_TOPIC "mes/ptc-windchill/document-structure/get/response" WITH METHOD "GET" WITH ENDPOINT "/Windchill/servlet/odata/DocMgmt/Documents('{value.json.id}')/DocUsageLinks?$expand=DocUsedBy,DocUses" WITH HEADERS ADD "Accept" "application/json" ADD "Authorization" "{value.json.authorization}" # All problem reports visible to the user ADD EVENT ListProblemReports WITH SOURCE_TOPIC "mes/ptc-windchill/problem-reports/list" WITH DESTINATION_TOPIC "mes/ptc-windchill/problem-reports/list/response" WITH METHOD "GET" WITH ENDPOINT "/Windchill/servlet/odata/ChangeMgmt/ProblemReports" WITH HEADERS ADD "Accept" "application/json" ADD "Authorization" "{value.json.authorization}" # One problem report by OID (state, priority, category) ADD EVENT GetProblemReport WITH SOURCE_TOPIC "mes/ptc-windchill/problem-report/get" WITH DESTINATION_TOPIC "mes/ptc-windchill/problem-report/get/response" WITH METHOD "GET" WITH ENDPOINT "/Windchill/servlet/odata/ChangeMgmt/ProblemReports('{value.json.id}')" WITH HEADERS ADD "Accept" "application/json" ADD "Authorization" "{value.json.authorization}" # Affected parts / documents of a problem report ADD EVENT GetProblemReportAffectedObjects WITH SOURCE_TOPIC "mes/ptc-windchill/problem-report-affected/get" WITH DESTINATION_TOPIC "mes/ptc-windchill/problem-report-affected/get/response" WITH METHOD "GET" WITH ENDPOINT "/Windchill/servlet/odata/ChangeMgmt/ProblemReports('{value.json.id}')/CIAffectLinks?$expand=AffectedObjects" WITH HEADERS ADD "Accept" "application/json" ADD "Authorization" "{value.json.authorization}" # WRITE: problem report from a quality event, optionally with affected parts (Changeables@odata.bind) in the product context ADD EVENT CreateProblemReport WITH SOURCE_TOPIC "mes/ptc-windchill/problem-report/create" WITH DESTINATION_TOPIC "mes/ptc-windchill/problem-report/create/response" WITH METHOD "POST" WITH ENDPOINT "/Windchill/servlet/odata/ChangeMgmt/ProblemReports" WITH HEADERS ADD "Accept" "application/json" ADD "Authorization" "{value.json.authorization}" ADD "Content-Type" "application/json" ADD "CSRF_NONCE" "{value.json.nonce}" WITH BODY "{value.json.request}" # WRITE: variance (deviation / waiver) request, for example to run a lot with an approved deviation ADD EVENT CreateVariance WITH SOURCE_TOPIC "mes/ptc-windchill/variance/create" WITH DESTINATION_TOPIC "mes/ptc-windchill/variance/create/response" WITH METHOD "POST" WITH ENDPOINT "/Windchill/servlet/odata/ChangeMgmt/Variances" WITH HEADERS ADD "Accept" "application/json" ADD "Authorization" "{value.json.authorization}" ADD "Content-Type" "application/json" ADD "CSRF_NONCE" "{value.json.nonce}" WITH BODY "{value.json.request}" # Change requests with their affected objects ADD EVENT ListChangeRequests WITH SOURCE_TOPIC "mes/ptc-windchill/change-requests/list" WITH DESTINATION_TOPIC "mes/ptc-windchill/change-requests/list/response" WITH METHOD "GET" WITH ENDPOINT "/Windchill/servlet/odata/ChangeMgmt/ChangeRequests?$expand=AffectedObjects" WITH HEADERS ADD "Accept" "application/json" ADD "Authorization" "{value.json.authorization}" # Change notices (released engineering changes) to flag BOM / routing updates on the shop floor ADD EVENT ListChangeNotices WITH SOURCE_TOPIC "mes/ptc-windchill/change-notices/list" WITH DESTINATION_TOPIC "mes/ptc-windchill/change-notices/list/response" WITH METHOD "GET" WITH ENDPOINT "/Windchill/servlet/odata/ChangeMgmt/ChangeNotices" WITH HEADERS ADD "Accept" "application/json" ADD "Authorization" "{value.json.authorization}" # One change notice by OID ADD EVENT GetChangeNotice WITH SOURCE_TOPIC "mes/ptc-windchill/change-notice/get" WITH DESTINATION_TOPIC "mes/ptc-windchill/change-notice/get/response" WITH METHOD "GET" WITH ENDPOINT "/Windchill/servlet/odata/ChangeMgmt/ChangeNotices('{value.json.id}')" WITH HEADERS ADD "Accept" "application/json" ADD "Authorization" "{value.json.authorization}" ``` ## Example actions These actions refresh the CSRF nonce, look up a part by number from a station, request a kitting list when an order is released, and turn a press quality event into a problem report. Set `internal/ptc-windchill/product-context` to the product container OID (for example `OR:wt.pdmlink.PDMLinkProduct:122008`) first. ```lot theme={"theme":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} DEFINE ACTION WcCsrfRefresh ON EVERY 20 MINUTES DO SET "auth" WITH GET SECRET "WINDCHILL_AUTHORIZATION" PUBLISH TOPIC "mes/ptc-windchill/csrf-token/get" WITH { "authorization": {auth} } DEFINE ACTION WcKeepNonce ON TOPIC "mes/ptc-windchill/csrf-token/get/response" DO SET "nonce" WITH (GET JSON "NonceValue" IN PAYLOAD AS STRING) KEEP TOPIC "internal/ptc-windchill/nonce" WITH {nonce} DEFINE ACTION WcPartLookup ON TOPIC "porto/line1/station03/work-instruction/part-number" DO SET "auth" WITH GET SECRET "WINDCHILL_AUTHORIZATION" SET "pn" WITH PAYLOAD AS STRING PUBLISH TOPIC "mes/ptc-windchill/parts/query" WITH { "authorization": {auth}, "filter": "Number%20eq%20'" + {pn} + "'", "top": "1" } DEFINE ACTION WcKittingList ON TOPIC "porto/line1/order/released/part-oid" DO SET "auth" WITH GET SECRET "WINDCHILL_AUTHORIZATION" SET "nonce" WITH GET TOPIC "internal/ptc-windchill/nonce" AS STRING SET "oid" WITH PAYLOAD AS STRING PUBLISH TOPIC "mes/ptc-windchill/components-report/get" WITH { "authorization": {auth}, "nonce": {nonce}, "id": {oid}, "request": {} } DEFINE ACTION WcQualityEventToProblemReport ON TOPIC "porto/line1/press01/quality-event" DO SET "auth" WITH GET SECRET "WINDCHILL_AUTHORIZATION" SET "nonce" WITH GET TOPIC "internal/ptc-windchill/nonce" AS STRING SET "ctx" WITH GET TOPIC "internal/ptc-windchill/product-context" AS STRING SET "title" WITH (GET JSON "title" IN PAYLOAD AS STRING) SET "detail" WITH (GET JSON "detail" IN PAYLOAD AS STRING) PUBLISH TOPIC "mes/ptc-windchill/problem-report/create" WITH { "authorization": {auth}, "nonce": {nonce}, "request": { "Name": {title}, "Description": {detail}, "Category": { "Value": "OTHER" }, "IssuePriority": { "Value": "HIGH" }, "Context@odata.bind": "Containers('" + {ctx} + "')" } } ``` ## Not included * Updates, check out / check in, revise and delete operations (by policy for a shop-floor template). * Content upload (three-stage upload to the file vault) and `DownloadContentAsZip` (returns a ZIP stream, not JSON). Use the download URL from `GetPartAttachments` instead. * Deprecated `GetBOM` and `GetPartsList` actions (replaced by `GetPartStructure` and `GetMultiLevelComponentsReport`). * Webhooks: Windchill has no generic outbound webhook. Poll `ListChangeNotices`, or push events with Windchill's own event and workflow tooling. ## Status See also the [REST API connectors](/latest/lot-language/routes/rest-api-routes) base page and the [REST API apps catalog](/latest/lot-language/routes/rest-api-apps/index). # PurpleAir Source: https://docs.coreflux.org/latest/lot-language/routes/rest-api-apps/purpleair Client-mode REST API connector for a curated PurpleAir v1 slice: list sensors and read one sensor Technical Preview # PurpleAir Client-mode REST API connector for a curated PurpleAir v1 slice: list sensors and read one sensor. * Hub category: **Public data**. * Base: `https://api.purpleair.com` ## Template reference Generated from the shipped broker template `purpleair` (`-connectorCode PURPLEAIR`). It lists every event the template defines. ### Auth Auth headers used by the template: `X-API-Key: Bearer {value.json.api_token}`. ### Template parameters | Placeholder | Type | Description | Required | | - | - | - | - | | `RouteName` | string | Name of the PurpleAir connector | no | | `BaseUrl` | string | PurpleAir base URL without trailing slash (e.g. [https://api.purpleair.com](https://api.purpleair.com)) | yes | `BASE_ADDRESS` default: `https://api.purpleair.com`. ### Events | Event | Method | Endpoint | Request topic | Response topic | Payload fields | | - | - | - | - | - | - | | `Listsensors` | GET | `/v1/sensors?fields=name,humidity,temperature,pm2.5` | `public/purpleair/sensors/list` | `public/purpleair/sensors/list/response` | `api_token` | | `Getsensor` | GET | `/v1/sensors/{value.json.sensor_index}` | `public/purpleair/sensors/get` | `public/purpleair/sensors/get/response` | `api_token`, `sensor_index` | ### Deploy Get the template card (JSON with `template` and `metadata`) from the broker, then deploy the connector: ```bash theme={"theme":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} mosquitto_pub -t '$SYS/Coreflux/Command' -m "-connectorCode PURPLEAIR" ``` The deployable connector from the REST API app, with `BASE_ADDRESS` already filled in: ```lot theme={"theme":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} DEFINE CONNECTOR PurpleAirRestApi WITH TYPE REST_API ADD REST_API_CONFIG WITH BASE_ADDRESS "https://api.purpleair.com" WITH ENABLE_CLIENT "true" WITH USE_SSL "true" WITH REQUEST_TIMEOUT '60' WITH DEFAULT_CONTENT_TYPE "application/json" ADD EVENT Listsensors WITH SOURCE_TOPIC "public/purpleair/sensors/list" WITH DESTINATION_TOPIC "public/purpleair/sensors/list/response" WITH METHOD "GET" WITH ENDPOINT "/v1/sensors?fields=name,humidity,temperature,pm2.5" WITH HEADERS ADD "Accept" "application/json" ADD "X-API-Key" "Bearer {value.json.api_token}" ADD EVENT Getsensor WITH SOURCE_TOPIC "public/purpleair/sensors/get" WITH DESTINATION_TOPIC "public/purpleair/sensors/get/response" WITH METHOD "GET" WITH ENDPOINT "/v1/sensors/{value.json.sensor_index}" WITH HEADERS ADD "Accept" "application/json" ADD "X-API-Key" "Bearer {value.json.api_token}" ``` Deploy it with `-addConnector` (do not include `ADD METADATA`). ### Usage example Call `Listsensors` by publishing a JSON payload to its request topic, and read the reply on the response topic: ```bash theme={"theme":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} mosquitto_sub -t 'public/purpleair/sensors/list/response' & mosquitto_pub -t 'public/purpleair/sensors/list' -m '{"api_token": ""}' ``` # Pushover Source: https://docs.coreflux.org/latest/lot-language/routes/rest-api-apps/pushover LoT connector for the Pushover Message API: send notifications including emergency priority, validate user keys, poll receipts and update glances Technical Preview ## Overview A client-mode connector for a **curated messaging** slice of the [Pushover Message API](https://pushover.net/api): | Catalog connector event | Entity / path | Notes | | - | - | - | | POST messages.json | `POST /messages.json` | Push alert; `priority=2` + `retry`/`expire` for critical alarms | | GET receipts/\{id}.json | `GET /receipts/{receipt_id}.json` | Poll emergency acknowledgement | | POST glances.json | `POST /glances.json` | Live KPI text/count on glances widgets | | POST users/validate.json | `POST /users/validate.json` | Credential check → `status: 1` + devices | | GET sounds.json | `GET /sounds.json` | Built-in (+ custom) sound list | Default `BASE_ADDRESS` is `https://api.pushover.net/1`. Topics use `messaging/pushover//`. > [!NOTE] > > Auth is **not** OAuth: include application `token` (APP\_TOKEN) and recipient `user` (USER\_KEY) in the JSON body for POSTs, or `token` as a query parameter for GETs. Prefer Hub secrets `PUSHOVER_API_TOKEN` (alias `PUSHOVER_APP_TOKEN`) and `PUSHOVER_USER_KEY`. ## Basic Syntax ```lot wrap theme={"theme":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} # Coreflux REST_API template — Pushover Message API v1 # Auth: token + user in JSON body / query (no Authorization header) # BASE_ADDRESS: https://api.pushover.net/1 DEFINE CONNECTOR PushoverRestApi WITH TYPE REST_API ADD REST_API_CONFIG WITH BASE_ADDRESS "https://api.pushover.net/1" WITH ENABLE_CLIENT "true" WITH USE_SSL "true" WITH REQUEST_TIMEOUT '60' WITH DEFAULT_CONTENT_TYPE "application/json" ADD EVENT Sendmessage WITH SOURCE_TOPIC "messaging/pushover/message/send" WITH DESTINATION_TOPIC "messaging/pushover/message/send/response" WITH METHOD "POST" WITH ENDPOINT "/messages.json" WITH HEADERS ADD "Accept" "application/json" ADD "Content-Type" "application/json" WITH BODY "{payload}" ADD EVENT Validateuser WITH SOURCE_TOPIC "messaging/pushover/user/validate" WITH DESTINATION_TOPIC "messaging/pushover/user/validate/response" WITH METHOD "POST" WITH ENDPOINT "/users/validate.json" WITH HEADERS ADD "Accept" "application/json" ADD "Content-Type" "application/json" WITH BODY "{payload}" ADD EVENT Getreceipt WITH SOURCE_TOPIC "messaging/pushover/receipt/get" WITH DESTINATION_TOPIC "messaging/pushover/receipt/get/response" WITH METHOD "GET" WITH ENDPOINT "/receipts/{value.json.receipt_id}.json?token={value.json.token}" WITH HEADERS ADD "Accept" "application/json" ADD EVENT Listsounds WITH SOURCE_TOPIC "messaging/pushover/sound/list" WITH DESTINATION_TOPIC "messaging/pushover/sound/list/response" WITH METHOD "GET" WITH ENDPOINT "/sounds.json?token={value.json.token}" WITH HEADERS ADD "Accept" "application/json" ADD EVENT Updateglance WITH SOURCE_TOPIC "messaging/pushover/glance/update" WITH DESTINATION_TOPIC "messaging/pushover/glance/update/response" WITH METHOD "POST" WITH ENDPOINT "/glances.json" WITH HEADERS ADD "Accept" "application/json" ADD "Content-Type" "application/json" WITH BODY "{payload}" ``` ## Events | Event | Method | Endpoint | Request topic | Response topic | | - | - | - | - | - | | Sendmessage | POST | `/messages.json` | `messaging/pushover/message/send` | `messaging/pushover/message/send/response` | | Validateuser | POST | `/users/validate.json` | `messaging/pushover/user/validate` | `messaging/pushover/user/validate/response` | | Getreceipt | GET | `/receipts/{receipt_id}.json` | `messaging/pushover/receipt/get` | `messaging/pushover/receipt/get/response` | | Listsounds | GET | `/sounds.json` | `messaging/pushover/sound/list` | `messaging/pushover/sound/list/response` | | Updateglance | POST | `/glances.json` | `messaging/pushover/glance/update` | `messaging/pushover/glance/update/response` | ## Auth 1. [Register an application](https://pushover.net/apps/build) to get an API token (`APP_TOKEN`). Copy your user key from the [Pushover dashboard](https://pushover.net/). 2. Set connector `BASE_ADDRESS` to `https://api.pushover.net/1`. 3. Pass `token` and `user` on MQTT JSON payloads for POSTs. For GETs, pass `token` (and `receipt_id` for Getreceipt) so query/path placeholders resolve. 4. Store secrets in Hub / `GET SECRET "PUSHOVER_API_TOKEN"` and `GET SECRET "PUSHOVER_USER_KEY"`. Never commit real credentials. 5. Successful validate/send returns **HTTP 200** with `status: 1`. Emergency (`priority: 2`) responses include a `receipt` for polling. ## Example payloads **validateUser:** ```json theme={"theme":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} { "token": "YOUR_PUSHOVER_API_TOKEN", "user": "YOUR_PUSHOVER_USER_KEY" } ``` **sendMessage (high priority):** ```json theme={"theme":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} { "token": "YOUR_PUSHOVER_API_TOKEN", "user": "YOUR_PUSHOVER_USER_KEY", "title": "Coreflux alarm — Zone A", "message": "RMS exceeded limit on Zone A", "priority": 1, "sound": "siren" } ``` **sendMessage (emergency / priority 2):** ```json theme={"theme":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} { "token": "YOUR_PUSHOVER_API_TOKEN", "user": "YOUR_PUSHOVER_USER_KEY", "title": "CRITICAL — Zone A trip", "message": "Emergency stop engaged. Acknowledge required.", "priority": 2, "retry": 60, "expire": 1800, "sound": "persistent" } ``` **listSounds:** ```json theme={"theme":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} { "token": "YOUR_PUSHOVER_API_TOKEN" } ``` **getReceipt:** ```json theme={"theme":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} { "token": "YOUR_PUSHOVER_API_TOKEN", "receipt_id": "YOUR_30_CHAR_RECEIPT" } ``` **updateGlance:** ```json theme={"theme":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} { "token": "YOUR_PUSHOVER_API_TOKEN", "user": "YOUR_PUSHOVER_USER_KEY", "title": "Zone A", "text": "vibration_rms", "subtext": "12.4", "count": 3, "percent": 82 } ``` ## Install 1. The template ships with the broker. List it with `-listTemplates` or fetch it from the Hub catalog. 2. Deploy with `-addConnector` from `PushoverRestApi.lot` (not `hub-metadata.lot`). ## See also * [REST API Connectors](/latest/lot-language/routes/rest-api-routes) * [REST API Apps](/latest/lot-language/routes/rest-api-apps) # QAD Adaptive ERP Source: https://docs.coreflux.org/latest/lot-language/routes/rest-api-apps/qad-adaptive-erp LoT connector for QAD Adaptive ERP: read items, inventory, work orders, BOMs, routings and orders, and call Business Document and Service REST APIs Technical Preview ## Overview [QAD Adaptive ERP](https://www.qad.com/) (QAD Adaptive Applications) runs items, inventory, production, purchasing, sales and quality for many manufacturers. It is built on the **QAD Enterprise Platform (QEP)**, which serves REST APIs under the web application context, usually `/clouderp`. Connecting QAD to the broker lets the shop floor read work orders, on-hand inventory, BOMs and routings when a line needs them, check supplier and item certifications, and report production through the business services your environment exposes. QAD has no single public API catalog. The platform has three kinds of REST API: * **Business components** (`api/qracore/be/`): read a record by its domain and key fields. QAD names each component with a URI such as `urn:be:com.qad.base.item.IItem`. * **Business documents**: complete documents with Query REST APIs (filtering, sorting, paging). Each environment documents its own under **Business Document API Documentation** (Swagger). * **Business services**: non-CRUD methods exposed as REST with GET (query parameters) or POST (body). Each environment lists them under **Business Service API Documentation**. The template has one client-mode connector, `QadAdaptiveErp`, with 36 events under `erp/qad/`. Each event publishes the reply on `/response`. 4 events write (business document submits and business service POSTs). The business component events only read, and there is no DELETE event. | Event | Topic | Method | Use | | - | - | - | - | | `GetToken` | `erp/qad/auth/token` | POST | Get an access token (OAuth 2 password grant with your Client ID) (fields: `form`) | | `RefreshToken` | `erp/qad/auth/token/refresh` | POST | Get a new access token with the refresh token (fields: `form`) | | `GetEsignatureToken` | `erp/qad/auth/esignature-token` | POST | Get an e-signature token for the `*Esig` writes (fields: `authorization`, `body`) | | `GetApps` | `erp/qad/platform/apps/get` | GET | Apps installed in the environment, for example query appURI=urn:app:com.qad.base (fields: `authorization`, `query`) | | `ListWorkspaces` | `erp/qad/platform/workspaces/list` | GET | Domains / workspaces the API user can open (reply key qad-qracore: key, name, description, default) (path from a third-party client, not in QAD's guides) (fields: `authorization`) | | `GetBusinessComponent` | `erp/qad/be/get` | GET | Read any business component by URI (`urn:be:`) (fields: `authorization`, `beUri`, `query`) | | `GetSite` | `erp/qad/sites/get` | GET | Read a site (`urn:be:com.qad.base.item.ISite`) (fields: `authorization`, `query`) | | `GetItem` | `erp/qad/items/get` | GET | Read an item (item master) (`urn:be:com.qad.base.item.IItem`) (fields: `authorization`, `query`) | | `GetItemSite` | `erp/qad/item-sites/get` | GET | Read item-site planning / inventory data (`urn:be:com.qad.base.item.IItemSite`) (fields: `authorization`, `query`) | | `GetInventoryDetail` | `erp/qad/inventory/detail/get` | GET | Read inventory detail (on hand by site / location / lot / reference) (`urn:be:com.qad.inventory.inv.IInventoryDetail`) (fields: `authorization`, `query`) | | `GetLocation` | `erp/qad/inventory/locations/get` | GET | Read a location master record (`urn:be:com.qad.inventory.inv.ILocationMaster`) (fields: `authorization`, `query`) | | `GetLot` | `erp/qad/inventory/lots/get` | GET | Read a lot master record (`urn:be:com.qad.inventory.inv.ILotMaster`) (fields: `authorization`, `query`) | | `GetInventoryTransactionHistory` | `erp/qad/inventory/transactions/history/get` | GET | Read inventory transaction history (`urn:be:com.qad.transhist.transaction.IInventoryTransHist`) (fields: `authorization`, `query`) | | `GetWorkOrder` | `erp/qad/production/work-orders/get` | GET | Read a work order (header, status, quantities) (`urn:be:com.qad.pushproduction.workorder.IWorkOrderMaster`) (fields: `authorization`, `query`) | | `GetWorkOrderRouting` | `erp/qad/production/work-order-routings/get` | GET | Read work order routing operations (`urn:be:com.qad.pushproduction.workorder.IWorkOrderRouting`) (fields: `authorization`, `query`) | | `GetWorkOrderDetail` | `erp/qad/production/work-order-details/get` | GET | Read work order component requirements (`urn:be:com.qad.pushproduction.workorder.IWorkOrderDetail`) (fields: `authorization`, `query`) | | `GetOperationHistory` | `erp/qad/production/operation-history/get` | GET | Read operation transaction history (labor, run, scrap reporting) (`urn:be:com.qad.transhist.transaction.IOperationTransHist`) (fields: `authorization`, `query`) | | `GetBom` | `erp/qad/engineering/boms/get` | GET | Read a product structure (BOM) (`urn:be:com.qad.engineering.productstructure.IBom`) (fields: `authorization`, `query`) | | `GetRoutingOperation` | `erp/qad/engineering/routing-operations/get` | GET | Read routing operations (`urn:be:com.qad.engineering.routing.IRoutingOperation`) (fields: `authorization`, `query`) | | `GetProductionLine` | `erp/qad/engineering/production-lines/get` | GET | Read a production line (`urn:be:com.qad.engineering.setup.IProductionLine`) (fields: `authorization`, `query`) | | `GetPurchaseOrder` | `erp/qad/purchasing/purchase-orders/get` | GET | Read a purchase order (`urn:be:com.qad.purchasing.purchaseorders.IPurchaseOrder`) (fields: `authorization`, `query`) | | `GetPurchaseReceiptHistory` | `erp/qad/purchasing/receipts/history/get` | GET | Read purchase receipt history (`urn:be:com.qad.purchasing.receipts.IPurchaseReceiptHistory`) (fields: `authorization`, `query`) | | `GetSalesOrder` | `erp/qad/sales/sales-orders/get` | GET | Read a sales order (`urn:be:com.qad.sales.salesorder.ISalesOrder`) (fields: `authorization`, `query`) | | `GetShipper` | `erp/qad/logistics/shippers/get` | GET | Read a shipper header (`urn:be:com.qad.logistics.shipping.IShipperHeader`) (fields: `authorization`, `query`) | | `GetSupplier` | `erp/qad/suppliers/get` | GET | Read a supplier (`urn:be:com.qad.base.supplier.ISupplier`) (fields: `authorization`, `query`) | | `GetSupplierCertification` | `erp/qad/quality/supplier-certifications/get` | GET | Read a supplier certification (`urn:be:com.qad.base.supplier.ISupplierCertification`) (fields: `authorization`, `query`) | | `GetItemCertification` | `erp/qad/quality/item-certifications/get` | GET | Read an item certification (`urn:be:com.qad.base.supplier.IItemCertification`) (fields: `authorization`, `query`) | | `GetReasonCode` | `erp/qad/codes/reason-codes/get` | GET | Read a reason code (downtime, scrap, rework, ...) (`urn:be:com.qad.base.codes.IReasonCode`) (fields: `authorization`, `query`) | | `GetInstalledBaseItem` | `erp/qad/service/installed-base/get` | GET | Read an installed base item (equipment at a customer) (`urn:be:com.qad.service.installedbase.IInstalledBaseItem`) (fields: `authorization`, `query`) | | `BrowseQuery` | `erp/qad/browse/query` | GET | Multi-record read through a browse (path from a third-party client, not in QAD's guides) (fields: `authorization`, `browseId`, `query`) | | `BusinessDocumentQuery` | `erp/qad/business-document/query` | GET | Query REST API of a business document (filtering, sorting, paging, partial data) (fields: `authorization`, `path`, `query`) | | `BusinessDocumentSubmit` | `erp/qad/business-document/submit` | POST | **Write.** Send a complete business document (fields: `authorization`, `body`, `path`) | | `BusinessDocumentSubmitEsig` | `erp/qad/business-document/submit-esig` | POST | **Write.** Same as BusinessDocumentSubmit with X-QAD-Esig-Token from GetEsignatureToken, for components with an active e-signature configuration (fields: `authorization`, `body`, `esig_token`, `path`) | | `BusinessServiceGet` | `erp/qad/business-service/get` | GET | Call a business service (non-CRUD method exposed as REST) (fields: `authorization`, `path`, `query`) | | `BusinessServicePost` | `erp/qad/business-service/post` | POST | **Write (may write).** Call a business service whose HTTP Method Type is POST, for example a production or inventory transaction service (fields: `authorization`, `body`, `path`) | | `BusinessServicePostEsig` | `erp/qad/business-service/post-esig` | POST | **Write (may write).** Same as BusinessServicePost with X-QAD-Esig-Token (fields: `authorization`, `body`, `esig_token`, `path`) | ## Prerequisites * A QAD Adaptive ERP environment (QAD cloud or on-premises) reachable over HTTPS from the broker. Note its host and web application context: `/clouderp` on QAD cloud; some on-premises installs use `/qad-central`. * A **Client ID** for the broker, generated in **Client IDs** (Adaptive UX) or **Client ID Maintenance** (36.3.12). Use one Client ID per integration. * An API user who can sign in with a password (not single sign-on only), with the **webui\_user** role, **Read** on each business component or browse you call, and **Execute** on each business service (Role Permissions > Not In Menu > Integration > External Business Services). * The domain code you work in, and the key field names of each component (Business Components > Fields, or the component's Swagger). * For business documents and business services: the REST path and body schema from **Business Document API Documentation** or **Business Service API Documentation** in your environment. ## Connection and security QAD's API authentication is OAuth 2 (password grant) or HTTP Basic. QAD recommends OAuth 2. Store the credentials as broker secrets and build the token form with `GET SECRET`: | Secret | Value | | - | - | | `QAD_CLIENT_ID` | The Client ID generated for Coreflux | | `QAD_USERNAME` | The API user | | `QAD_PASSWORD` | The API user's password, URL-encoded (it goes into a form body) | `GetToken` posts `client_id=&grant_type=password&username=&password=` as `application/x-www-form-urlencoded` to `/clouderp/oauth/token`. No client secret is sent. The reply carries `access_token` and `refresh_token`. Send `"authorization": "Bearer "` on every other event. A Basic value (`Basic `) also works in the same field, but QAD discourages it. When a component has an active **e-signature** configuration, a write without a token fails with `APIModeRequiredException` or `ESignatureRequiredException`. Call `GetEsignatureToken` with `{"password", "reasonCode", "remarks"}` and use the `*Esig` events, which send the token in the `X-QAD-Esig-Token` header. If your environment uses `/qad-central` (or another context), replace `/clouderp/` in every endpoint before you deploy, and change `BASE_ADDRESS` to your host. ## Payload Publish JSON on the event's source topic. The broker fills the `{value.json.}` placeholders: * `authorization`: on every event except `GetToken` and `RefreshToken`. * `query`: URL-encoded query string, for example `domainCode=&itemCode=`. Use `""` for none. * `form`: the URL-encoded token form on `GetToken` and `RefreshToken`. * `body`: JSON on POST events. * `path`: the REST path from the API documentation screen (starts with `/clouderp/api/...`) on the business document and business service events. * `beUri`: the business component URI on `GetBusinessComponent`. * `browseId`: the browse URI (`urn:browse::`) on `BrowseQuery`. * `esig_token`: the e-signature token on the `*Esig` events. ```json theme={"theme":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} {"authorization": "Bearer ", "query": "domainCode=&itemCode="} ``` Values are not URL-encoded by the connector, so encode `query` and `form` before you publish. ## Pacing and errors QAD publishes no rate limit or token lifetime. Read `expires_in` from the token reply and refresh before it runs out. Business document queries support paging; ask for small pages from the shop floor. Errors come back with the HTTP status and QAD's envelope: ```json theme={"theme":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} {"submitResult": {"success": false, "errors": [{"severity": 1, "code": "401", "message": "Unauthorized. "}]}, "data": null} ``` A bad or expired bearer returns 401 with `{"error": "invalid_token"}`. A token request with an unknown Client ID returns 401 `The client is not authenticated.` ## Example actions ```lot theme={"theme":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} # Example actions for the QadAdaptiveErp route (docs page). Not part of the route template. # Secrets: QAD_CLIENT_ID (a Client ID generated in Client IDs / Client ID Maintenance 36.3.12 for this integration), # QAD_USERNAME and QAD_PASSWORD (an API user with the webui_user role and Read on the components used; store the # password URL-encoded because it goes into a form body). QAD does not publish the token lifetime, so the token # action runs every 10 minutes; lengthen it to just under the expires_in your environment returns. # The bearer value is kept on internal/qad/authorization. Replace the <...> placeholders (domain, key fields, path). DEFINE ACTION QadGetToken ON EVERY 10 MINUTES DO SET "cid" WITH GET SECRET "QAD_CLIENT_ID" SET "user" WITH GET SECRET "QAD_USERNAME" SET "pwd" WITH GET SECRET "QAD_PASSWORD" SET "form" WITH "client_id=" + {cid} + "&grant_type=password&username=" + {user} + "&password=" + {pwd} PUBLISH TOPIC "erp/qad/auth/token" WITH { "form": {form} } DEFINE ACTION QadKeepToken ON TOPIC "erp/qad/auth/token/response" DO SET "tok" WITH (GET JSON "access_token" IN PAYLOAD AS STRING) KEEP TOPIC "internal/qad/authorization" WITH "Bearer " + {tok} DEFINE ACTION QadItemOnRequest ON TOPIC "plant/line1/item/request" DO SET "auth" WITH GET TOPIC "internal/qad/authorization" AS STRING SET "item" WITH PAYLOAD AS STRING SET "q" WITH "domainCode=&itemCode=" + {item} PUBLISH TOPIC "erp/qad/items/get" WITH { "authorization": {auth}, "query": {q} } DEFINE ACTION QadWorkOrderEvery5Minutes ON EVERY 5 MINUTES DO SET "auth" WITH GET TOPIC "internal/qad/authorization" AS STRING PUBLISH TOPIC "erp/qad/production/work-orders/get" WITH { "authorization": {auth}, "query": "domainCode=&=" } DEFINE ACTION QadReportProductionFromLine ON TOPIC "plant/line1/counter/good" DO SET "auth" WITH GET TOPIC "internal/qad/authorization" AS STRING SET "qty" WITH PAYLOAD AS STRING PUBLISH TOPIC "erp/qad/business-service/post" WITH { "authorization": {auth}, "path": "", "body": { "domainCode": "", "quantity": {qty} } } ``` ## Connector template ```lot theme={"theme":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} # Coreflux REST_API template — QAD Adaptive ERP (QAD Adaptive Applications on the QAD Enterprise Platform, cloud or # on-premises) for plants: OAuth 2 token (password grant) from the platform, read business components (sites, items, # item-sites, inventory detail, locations, lots, work orders with routing and component lines, BOMs, routings, # production lines, purchase orders, receipts, sales orders, shippers, inventory and operation transaction history, # supplier / item certifications, reason codes, installed base), run browse queries, and call the Business Document # and Business Service REST APIs your environment publishes (with an e-signature token when a component needs it). # (connect.qad.com/web/documentlibrary, no login): QAD Enterprise Platform Developer Guide 2020 (QEP_DG_v2020: URI # formats, api/qracore/be/?domainCode=..&=.. reads, api/qracore/apps, API auth = OAuth2 # and HTTP Basic), QAD Security Administration Guides 2024 QAE + QAD Adaptive 2026 (API Authentication: POST # /clouderp/oauth/token password grant with client_id, Client IDs), 2020 QAE (business component URIs, from its # field security appendix) and the AI_Docs Security Admin Guide (e-signature API mode: POST /api/qracore/esignature/token -> X-QAD-Esig-Token header, submitResult error envelope), # QAD Online Help Foundation (Business Document / Business Service API Documentation screens: per-environment # Swagger / OpenAPI, Business Service HTTP method GET or POST with QUERY or BODY parameters), QAD Adaptive release # documentlibrary.qad.com (online help + Swagger guides) sits behind a Cloudflare Turnstile challenge and the real # per-document Swagger lives inside each QAD environment (Development > Business Document API Documentation), so # there is no public OpenAPI. Business document / business service paths are payload fields copied from that screen. # The browse query path api/qracore/browses?browseId=..&filters=..&fields=..&pageSize=.. and api/webshell/workspaces # are NOT in QAD's public guides: they come from a public QAD client (github.com/frantm05/QMA) and are marked as such. # # Docs: https://docs.coreflux.org/latest/lot-language/routes/rest-api-apps/qad-adaptive-erp # # One route, topic prefix erp/qad/: # QadAdaptiveErp https://qad.example.com (your QAD Adaptive host). Every path starts with the web app context # /clouderp (QAD's documented example, used by QAD cloud). Some on-premises installs serve the same app as # /qad-central (QAD Implementation Guide examples): replace "/clouderp/" with "/qad-central/" in every endpoint then. # Auth (QAD Security Administration Guide, "API Authentication"): OAuth 2 or HTTP Basic. # GetToken: POST /clouderp/oauth/token, application/x-www-form-urlencoded # client_id=&grant_type=password&username=&password= (no client secret in the request) # -> access_token (+ refresh_token). Send "authorization":"Bearer " on every other event. HTTP Basic # ("Basic ") also works in the same field but QAD discourages it (re-authenticates every call). # Client IDs are generated in Client IDs (Adaptive UX) / Client ID Maintenance 36.3.12 — use one per integration. # The API user needs the webui_user role plus Read on each business component / browse and Execute on each business # service (Role Permissions > Not In Menu > Integration > External Business Services). # E-signature: if a component has an active e-signature configuration, writes fail with APIModeRequiredException / # ESignatureRequiredException. Get a token with GetEsignatureToken {"password","reasonCode","remarks"} and use the # *Esig write events, which add header X-QAD-Esig-Token. # Scope: most components are domain-scoped; pass domainCode (and site / entity where the component is site- or # entity-scoped) plus the key fields in "query", for example domainCode=&itemCode= (the DG shows domainCode + key-field # query parameters; check each component's key field names in Business Components > Fields or its Swagger). # Errors: HTTP status + {"submitResult":{"success":false,"errors":[{"severity","code","message","messageType",...}]}, # "data":null}. 401 without or with a bad token (Spring OAuth "invalid_token" for a bad bearer). # Writes (# WRITE): business documents and business services only. QAD's public guides do not document create / update # / delete verbs on api/qracore/be, so this template does not write through it, and has no DELETE event. # Not packaged: QXtend (SOAP), Business Events (published to an event streaming platform, not HTTP callbacks), QAD # Integration Platform (Boomi) flows, analytics / composer admin APIs, Java extension framework. # # Payload fields (JSON on the source topic): authorization on every event except GetToken / RefreshToken; # query = URL-encoded query string ("" for none); body = JSON string on POST; path = the REST path from the Business # Document / Business Service API Documentation screen (starts /clouderp/api/...); beUri on GetBusinessComponent; # browseId on BrowseQuery; esig_token on the *Esig events; form on the token events. # GetToken {"form":"client_id=&grant_type=password&username=&password="} # GetItem {"authorization":"Bearer ","query":"domainCode=&itemCode="} # GetWorkOrder {"authorization":"Bearer ","query":"domainCode=&="} (key field names: see the component in Business Components > Fields or its Swagger) # BrowseQuery {"authorization":"Bearer ","browseId":"urn:browse:mfg:","query":"filters=..&fields=..&pageSize=100"} # BusinessServicePost {"authorization":"Bearer ","path":"/clouderp/api/","body":"{...}"} DEFINE CONNECTOR QadAdaptiveErp WITH TYPE REST_API ADD REST_API_CONFIG WITH BASE_ADDRESS "https://qad.example.com" WITH ENABLE_CLIENT "true" WITH USE_SSL "true" WITH REQUEST_TIMEOUT '120' WITH DEFAULT_CONTENT_TYPE "application/json" # Replace with your QAD Adaptive host (https://[:port]). Paths start /clouderp (or /qad-central on some on-premises installs). # OAuth 2 token endpoint (Security Administration Guide, API Authentication): Password grant: form client_id=&grant_type=password&username=..&password=.. (client secret not sent). Returns access_token, refresh_token, expires_in. ADD EVENT GetToken WITH SOURCE_TOPIC "erp/qad/auth/token" WITH DESTINATION_TOPIC "erp/qad/auth/token/response" WITH METHOD "POST" WITH ENDPOINT "/clouderp/oauth/token" WITH HEADERS ADD "Accept" "application/json" ADD "User-Agent" "Coreflux-Broker REST_API route (https://www.coreflux.org)" ADD "Content-Type" "application/x-www-form-urlencoded" WITH BODY "{value.json.form}" # OAuth 2 token endpoint, refresh_token grant (RFC 6749 section 6; the guide says the password flow issues access and refresh tokens): form client_id=&grant_type=refresh_token&refresh_token=. ADD EVENT RefreshToken WITH SOURCE_TOPIC "erp/qad/auth/token/refresh" WITH DESTINATION_TOPIC "erp/qad/auth/token/refresh/response" WITH METHOD "POST" WITH ENDPOINT "/clouderp/oauth/token" WITH HEADERS ADD "Accept" "application/json" ADD "User-Agent" "Coreflux-Broker REST_API route (https://www.coreflux.org)" ADD "Content-Type" "application/x-www-form-urlencoded" WITH BODY "{value.json.form}" # E-signature API mode (Security Administration Guide, Electronic Signatures): body {"password":..,"reasonCode":..,"remarks":..} for the current user. Returns a base64 token for header X-QAD-Esig-Token on the *Esig writes. ADD EVENT GetEsignatureToken WITH SOURCE_TOPIC "erp/qad/auth/esignature-token" WITH DESTINATION_TOPIC "erp/qad/auth/esignature-token/response" WITH METHOD "POST" WITH ENDPOINT "/clouderp/api/qracore/esignature/token" WITH HEADERS ADD "Accept" "application/json" ADD "User-Agent" "Coreflux-Broker REST_API route (https://www.coreflux.org)" ADD "Authorization" "{value.json.authorization}" ADD "Content-Type" "application/json" WITH BODY "{value.json.body}" # Platform apps (QEP Developer Guide 2020: api/qracore/apps?appURI={appURI}): Apps installed in the environment, for example query appURI=urn:app:com.qad.base. Good connectivity / token check. ADD EVENT GetApps WITH SOURCE_TOPIC "erp/qad/platform/apps/get" WITH DESTINATION_TOPIC "erp/qad/platform/apps/get/response" WITH METHOD "GET" WITH ENDPOINT "/clouderp/api/qracore/apps?{value.json.query}" WITH HEADERS ADD "Accept" "application/json" ADD "User-Agent" "Coreflux-Broker REST_API route (https://www.coreflux.org)" ADD "Authorization" "{value.json.authorization}" # Web shell workspaces (NOT in QAD's public guides; observed in public QAD client github.com/frantm05/QMA): Domains / workspaces the API user can open (reply key qad-qracore: key, name, description, default). Gives the domainCode values. ADD EVENT ListWorkspaces WITH SOURCE_TOPIC "erp/qad/platform/workspaces/list" WITH DESTINATION_TOPIC "erp/qad/platform/workspaces/list/response" WITH METHOD "GET" WITH ENDPOINT "/clouderp/api/webshell/workspaces" WITH HEADERS ADD "Accept" "application/json" ADD "User-Agent" "Coreflux-Broker REST_API route (https://www.coreflux.org)" ADD "Authorization" "{value.json.authorization}" # Any business component (QEP Developer Guide 2020): GET QEP Developer Guide 2020: api/qracore/be/?domainCode=..&=.. (doHttpGet examples). beUri: urn:be:, for example urn:be:com.qad.base.item.IItem; query: domainCode + key fields. ADD EVENT GetBusinessComponent WITH SOURCE_TOPIC "erp/qad/be/get" WITH DESTINATION_TOPIC "erp/qad/be/get/response" WITH METHOD "GET" WITH ENDPOINT "/clouderp/api/qracore/be/{value.json.beUri}?{value.json.query}" WITH HEADERS ADD "Accept" "application/json" ADD "User-Agent" "Coreflux-Broker REST_API route (https://www.coreflux.org)" ADD "Authorization" "{value.json.authorization}" # Business component ISite (URI from QAD Security Administration Guide 2020 QAE (field security appendix)): Read a site. GET QEP Developer Guide 2020: api/qracore/be/?domainCode=..&=.. (doHttpGet examples). query: domainCode + key fields (URL-encoded); reply data holds the record(s). ADD EVENT GetSite WITH SOURCE_TOPIC "erp/qad/sites/get" WITH DESTINATION_TOPIC "erp/qad/sites/get/response" WITH METHOD "GET" WITH ENDPOINT "/clouderp/api/qracore/be/urn:be:com.qad.base.item.ISite?{value.json.query}" WITH HEADERS ADD "Accept" "application/json" ADD "User-Agent" "Coreflux-Broker REST_API route (https://www.coreflux.org)" ADD "Authorization" "{value.json.authorization}" # Business component IItem (URI from QAD Security Administration Guide 2020 QAE (field security appendix)): Read an item (item master). GET QEP Developer Guide 2020: api/qracore/be/?domainCode=..&=.. (doHttpGet examples). query: domainCode + key fields (URL-encoded); reply data holds the record(s). ADD EVENT GetItem WITH SOURCE_TOPIC "erp/qad/items/get" WITH DESTINATION_TOPIC "erp/qad/items/get/response" WITH METHOD "GET" WITH ENDPOINT "/clouderp/api/qracore/be/urn:be:com.qad.base.item.IItem?{value.json.query}" WITH HEADERS ADD "Accept" "application/json" ADD "User-Agent" "Coreflux-Broker REST_API route (https://www.coreflux.org)" ADD "Authorization" "{value.json.authorization}" # Business component IItemSite (URI from QAD Security Administration Guide 2020 QAE (field security appendix)): Read item-site planning / inventory data. GET QEP Developer Guide 2020: api/qracore/be/?domainCode=..&=.. (doHttpGet examples). query: domainCode + key fields (URL-encoded); reply data holds the record(s). ADD EVENT GetItemSite WITH SOURCE_TOPIC "erp/qad/item-sites/get" WITH DESTINATION_TOPIC "erp/qad/item-sites/get/response" WITH METHOD "GET" WITH ENDPOINT "/clouderp/api/qracore/be/urn:be:com.qad.base.item.IItemSite?{value.json.query}" WITH HEADERS ADD "Accept" "application/json" ADD "User-Agent" "Coreflux-Broker REST_API route (https://www.coreflux.org)" ADD "Authorization" "{value.json.authorization}" # Business component IInventoryDetail (URI from QAD Security Administration Guide 2020 QAE (field security appendix)): Read inventory detail (on hand by site / location / lot / reference). GET QEP Developer Guide 2020: api/qracore/be/?domainCode=..&=.. (doHttpGet examples). query: domainCode + key fields (URL-encoded); reply data holds the record(s). ADD EVENT GetInventoryDetail WITH SOURCE_TOPIC "erp/qad/inventory/detail/get" WITH DESTINATION_TOPIC "erp/qad/inventory/detail/get/response" WITH METHOD "GET" WITH ENDPOINT "/clouderp/api/qracore/be/urn:be:com.qad.inventory.inv.IInventoryDetail?{value.json.query}" WITH HEADERS ADD "Accept" "application/json" ADD "User-Agent" "Coreflux-Broker REST_API route (https://www.coreflux.org)" ADD "Authorization" "{value.json.authorization}" # Business component ILocationMaster (URI from QAD Security Administration Guide 2020 QAE (field security appendix)): Read a location master record. GET QEP Developer Guide 2020: api/qracore/be/?domainCode=..&=.. (doHttpGet examples). query: domainCode + key fields (URL-encoded); reply data holds the record(s). ADD EVENT GetLocation WITH SOURCE_TOPIC "erp/qad/inventory/locations/get" WITH DESTINATION_TOPIC "erp/qad/inventory/locations/get/response" WITH METHOD "GET" WITH ENDPOINT "/clouderp/api/qracore/be/urn:be:com.qad.inventory.inv.ILocationMaster?{value.json.query}" WITH HEADERS ADD "Accept" "application/json" ADD "User-Agent" "Coreflux-Broker REST_API route (https://www.coreflux.org)" ADD "Authorization" "{value.json.authorization}" # Business component ILotMaster (URI from QAD Security Administration Guide 2020 QAE (field security appendix)): Read a lot master record. GET QEP Developer Guide 2020: api/qracore/be/?domainCode=..&=.. (doHttpGet examples). query: domainCode + key fields (URL-encoded); reply data holds the record(s). ADD EVENT GetLot WITH SOURCE_TOPIC "erp/qad/inventory/lots/get" WITH DESTINATION_TOPIC "erp/qad/inventory/lots/get/response" WITH METHOD "GET" WITH ENDPOINT "/clouderp/api/qracore/be/urn:be:com.qad.inventory.inv.ILotMaster?{value.json.query}" WITH HEADERS ADD "Accept" "application/json" ADD "User-Agent" "Coreflux-Broker REST_API route (https://www.coreflux.org)" ADD "Authorization" "{value.json.authorization}" # Business component IInventoryTransHist (URI from QAD Security Administration Guide 2020 QAE (field security appendix)): Read inventory transaction history. GET QEP Developer Guide 2020: api/qracore/be/?domainCode=..&=.. (doHttpGet examples). query: domainCode + key fields (URL-encoded); reply data holds the record(s). ADD EVENT GetInventoryTransactionHistory WITH SOURCE_TOPIC "erp/qad/inventory/transactions/history/get" WITH DESTINATION_TOPIC "erp/qad/inventory/transactions/history/get/response" WITH METHOD "GET" WITH ENDPOINT "/clouderp/api/qracore/be/urn:be:com.qad.transhist.transaction.IInventoryTransHist?{value.json.query}" WITH HEADERS ADD "Accept" "application/json" ADD "User-Agent" "Coreflux-Broker REST_API route (https://www.coreflux.org)" ADD "Authorization" "{value.json.authorization}" # Business component IWorkOrderMaster (URI from QAD Security Administration Guide 2020 QAE (field security appendix)): Read a work order (header, status, quantities). GET QEP Developer Guide 2020: api/qracore/be/?domainCode=..&=.. (doHttpGet examples). query: domainCode + key fields (URL-encoded); reply data holds the record(s). ADD EVENT GetWorkOrder WITH SOURCE_TOPIC "erp/qad/production/work-orders/get" WITH DESTINATION_TOPIC "erp/qad/production/work-orders/get/response" WITH METHOD "GET" WITH ENDPOINT "/clouderp/api/qracore/be/urn:be:com.qad.pushproduction.workorder.IWorkOrderMaster?{value.json.query}" WITH HEADERS ADD "Accept" "application/json" ADD "User-Agent" "Coreflux-Broker REST_API route (https://www.coreflux.org)" ADD "Authorization" "{value.json.authorization}" # Business component IWorkOrderRouting (URI from QAD Security Administration Guide 2020 QAE (field security appendix)): Read work order routing operations. GET QEP Developer Guide 2020: api/qracore/be/?domainCode=..&=.. (doHttpGet examples). query: domainCode + key fields (URL-encoded); reply data holds the record(s). ADD EVENT GetWorkOrderRouting WITH SOURCE_TOPIC "erp/qad/production/work-order-routings/get" WITH DESTINATION_TOPIC "erp/qad/production/work-order-routings/get/response" WITH METHOD "GET" WITH ENDPOINT "/clouderp/api/qracore/be/urn:be:com.qad.pushproduction.workorder.IWorkOrderRouting?{value.json.query}" WITH HEADERS ADD "Accept" "application/json" ADD "User-Agent" "Coreflux-Broker REST_API route (https://www.coreflux.org)" ADD "Authorization" "{value.json.authorization}" # Business component IWorkOrderDetail (URI from QAD Security Administration Guide 2020 QAE (field security appendix)): Read work order component requirements. GET QEP Developer Guide 2020: api/qracore/be/?domainCode=..&=.. (doHttpGet examples). query: domainCode + key fields (URL-encoded); reply data holds the record(s). ADD EVENT GetWorkOrderDetail WITH SOURCE_TOPIC "erp/qad/production/work-order-details/get" WITH DESTINATION_TOPIC "erp/qad/production/work-order-details/get/response" WITH METHOD "GET" WITH ENDPOINT "/clouderp/api/qracore/be/urn:be:com.qad.pushproduction.workorder.IWorkOrderDetail?{value.json.query}" WITH HEADERS ADD "Accept" "application/json" ADD "User-Agent" "Coreflux-Broker REST_API route (https://www.coreflux.org)" ADD "Authorization" "{value.json.authorization}" # Business component IOperationTransHist (URI from QAD Security Administration Guide 2020 QAE (field security appendix)): Read operation transaction history (labor, run, scrap reporting). GET QEP Developer Guide 2020: api/qracore/be/?domainCode=..&=.. (doHttpGet examples). query: domainCode + key fields (URL-encoded); reply data holds the record(s). ADD EVENT GetOperationHistory WITH SOURCE_TOPIC "erp/qad/production/operation-history/get" WITH DESTINATION_TOPIC "erp/qad/production/operation-history/get/response" WITH METHOD "GET" WITH ENDPOINT "/clouderp/api/qracore/be/urn:be:com.qad.transhist.transaction.IOperationTransHist?{value.json.query}" WITH HEADERS ADD "Accept" "application/json" ADD "User-Agent" "Coreflux-Broker REST_API route (https://www.coreflux.org)" ADD "Authorization" "{value.json.authorization}" # Business component IBom (URI from QAD Security Administration Guide 2020 QAE (field security appendix)): Read a product structure (BOM). GET QEP Developer Guide 2020: api/qracore/be/?domainCode=..&=.. (doHttpGet examples). query: domainCode + key fields (URL-encoded); reply data holds the record(s). ADD EVENT GetBom WITH SOURCE_TOPIC "erp/qad/engineering/boms/get" WITH DESTINATION_TOPIC "erp/qad/engineering/boms/get/response" WITH METHOD "GET" WITH ENDPOINT "/clouderp/api/qracore/be/urn:be:com.qad.engineering.productstructure.IBom?{value.json.query}" WITH HEADERS ADD "Accept" "application/json" ADD "User-Agent" "Coreflux-Broker REST_API route (https://www.coreflux.org)" ADD "Authorization" "{value.json.authorization}" # Business component IRoutingOperation (URI from QAD Security Administration Guide 2020 QAE (field security appendix)): Read routing operations. GET QEP Developer Guide 2020: api/qracore/be/?domainCode=..&=.. (doHttpGet examples). query: domainCode + key fields (URL-encoded); reply data holds the record(s). ADD EVENT GetRoutingOperation WITH SOURCE_TOPIC "erp/qad/engineering/routing-operations/get" WITH DESTINATION_TOPIC "erp/qad/engineering/routing-operations/get/response" WITH METHOD "GET" WITH ENDPOINT "/clouderp/api/qracore/be/urn:be:com.qad.engineering.routing.IRoutingOperation?{value.json.query}" WITH HEADERS ADD "Accept" "application/json" ADD "User-Agent" "Coreflux-Broker REST_API route (https://www.coreflux.org)" ADD "Authorization" "{value.json.authorization}" # Business component IProductionLine (URI from QAD Security Administration Guide 2020 QAE (field security appendix)): Read a production line. GET QEP Developer Guide 2020: api/qracore/be/?domainCode=..&=.. (doHttpGet examples). query: domainCode + key fields (URL-encoded); reply data holds the record(s). ADD EVENT GetProductionLine WITH SOURCE_TOPIC "erp/qad/engineering/production-lines/get" WITH DESTINATION_TOPIC "erp/qad/engineering/production-lines/get/response" WITH METHOD "GET" WITH ENDPOINT "/clouderp/api/qracore/be/urn:be:com.qad.engineering.setup.IProductionLine?{value.json.query}" WITH HEADERS ADD "Accept" "application/json" ADD "User-Agent" "Coreflux-Broker REST_API route (https://www.coreflux.org)" ADD "Authorization" "{value.json.authorization}" # Business component IPurchaseOrder (URI from QAD Security Administration Guide 2020 QAE (field security appendix)): Read a purchase order. GET QEP Developer Guide 2020: api/qracore/be/?domainCode=..&=.. (doHttpGet examples). query: domainCode + key fields (URL-encoded); reply data holds the record(s). ADD EVENT GetPurchaseOrder WITH SOURCE_TOPIC "erp/qad/purchasing/purchase-orders/get" WITH DESTINATION_TOPIC "erp/qad/purchasing/purchase-orders/get/response" WITH METHOD "GET" WITH ENDPOINT "/clouderp/api/qracore/be/urn:be:com.qad.purchasing.purchaseorders.IPurchaseOrder?{value.json.query}" WITH HEADERS ADD "Accept" "application/json" ADD "User-Agent" "Coreflux-Broker REST_API route (https://www.coreflux.org)" ADD "Authorization" "{value.json.authorization}" # Business component IPurchaseReceiptHistory (URI from QAD Security Administration Guide 2020 QAE (field security appendix)): Read purchase receipt history. GET QEP Developer Guide 2020: api/qracore/be/?domainCode=..&=.. (doHttpGet examples). query: domainCode + key fields (URL-encoded); reply data holds the record(s). ADD EVENT GetPurchaseReceiptHistory WITH SOURCE_TOPIC "erp/qad/purchasing/receipts/history/get" WITH DESTINATION_TOPIC "erp/qad/purchasing/receipts/history/get/response" WITH METHOD "GET" WITH ENDPOINT "/clouderp/api/qracore/be/urn:be:com.qad.purchasing.receipts.IPurchaseReceiptHistory?{value.json.query}" WITH HEADERS ADD "Accept" "application/json" ADD "User-Agent" "Coreflux-Broker REST_API route (https://www.coreflux.org)" ADD "Authorization" "{value.json.authorization}" # Business component ISalesOrder (URI from QAD Security Administration Guide 2020 QAE (field security appendix)): Read a sales order. GET QEP Developer Guide 2020: api/qracore/be/?domainCode=..&=.. (doHttpGet examples). query: domainCode + key fields (URL-encoded); reply data holds the record(s). ADD EVENT GetSalesOrder WITH SOURCE_TOPIC "erp/qad/sales/sales-orders/get" WITH DESTINATION_TOPIC "erp/qad/sales/sales-orders/get/response" WITH METHOD "GET" WITH ENDPOINT "/clouderp/api/qracore/be/urn:be:com.qad.sales.salesorder.ISalesOrder?{value.json.query}" WITH HEADERS ADD "Accept" "application/json" ADD "User-Agent" "Coreflux-Broker REST_API route (https://www.coreflux.org)" ADD "Authorization" "{value.json.authorization}" # Business component IShipperHeader (URI from QAD Security Administration Guide 2020 QAE (field security appendix)): Read a shipper header. GET QEP Developer Guide 2020: api/qracore/be/?domainCode=..&=.. (doHttpGet examples). query: domainCode + key fields (URL-encoded); reply data holds the record(s). ADD EVENT GetShipper WITH SOURCE_TOPIC "erp/qad/logistics/shippers/get" WITH DESTINATION_TOPIC "erp/qad/logistics/shippers/get/response" WITH METHOD "GET" WITH ENDPOINT "/clouderp/api/qracore/be/urn:be:com.qad.logistics.shipping.IShipperHeader?{value.json.query}" WITH HEADERS ADD "Accept" "application/json" ADD "User-Agent" "Coreflux-Broker REST_API route (https://www.coreflux.org)" ADD "Authorization" "{value.json.authorization}" # Business component ISupplier (URI from QAD Security Administration Guide 2020 QAE (field security appendix)): Read a supplier. GET QEP Developer Guide 2020: api/qracore/be/?domainCode=..&=.. (doHttpGet examples). query: domainCode + key fields (URL-encoded); reply data holds the record(s). ADD EVENT GetSupplier WITH SOURCE_TOPIC "erp/qad/suppliers/get" WITH DESTINATION_TOPIC "erp/qad/suppliers/get/response" WITH METHOD "GET" WITH ENDPOINT "/clouderp/api/qracore/be/urn:be:com.qad.base.supplier.ISupplier?{value.json.query}" WITH HEADERS ADD "Accept" "application/json" ADD "User-Agent" "Coreflux-Broker REST_API route (https://www.coreflux.org)" ADD "Authorization" "{value.json.authorization}" # Business component ISupplierCertification (URI from QAD Security Administration Guide 2020 QAE (field security appendix)): Read a supplier certification. GET QEP Developer Guide 2020: api/qracore/be/?domainCode=..&=.. (doHttpGet examples). query: domainCode + key fields (URL-encoded); reply data holds the record(s). ADD EVENT GetSupplierCertification WITH SOURCE_TOPIC "erp/qad/quality/supplier-certifications/get" WITH DESTINATION_TOPIC "erp/qad/quality/supplier-certifications/get/response" WITH METHOD "GET" WITH ENDPOINT "/clouderp/api/qracore/be/urn:be:com.qad.base.supplier.ISupplierCertification?{value.json.query}" WITH HEADERS ADD "Accept" "application/json" ADD "User-Agent" "Coreflux-Broker REST_API route (https://www.coreflux.org)" ADD "Authorization" "{value.json.authorization}" # Business component IItemCertification (URI from QAD Security Administration Guide 2020 QAE (field security appendix)): Read an item certification. GET QEP Developer Guide 2020: api/qracore/be/?domainCode=..&=.. (doHttpGet examples). query: domainCode + key fields (URL-encoded); reply data holds the record(s). ADD EVENT GetItemCertification WITH SOURCE_TOPIC "erp/qad/quality/item-certifications/get" WITH DESTINATION_TOPIC "erp/qad/quality/item-certifications/get/response" WITH METHOD "GET" WITH ENDPOINT "/clouderp/api/qracore/be/urn:be:com.qad.base.supplier.IItemCertification?{value.json.query}" WITH HEADERS ADD "Accept" "application/json" ADD "User-Agent" "Coreflux-Broker REST_API route (https://www.coreflux.org)" ADD "Authorization" "{value.json.authorization}" # Business component IReasonCode (URI from QAD Security Administration Guide 2020 QAE (field security appendix)): Read a reason code (downtime, scrap, rework, ...). GET QEP Developer Guide 2020: api/qracore/be/?domainCode=..&=.. (doHttpGet examples). query: domainCode + key fields (URL-encoded); reply data holds the record(s). ADD EVENT GetReasonCode WITH SOURCE_TOPIC "erp/qad/codes/reason-codes/get" WITH DESTINATION_TOPIC "erp/qad/codes/reason-codes/get/response" WITH METHOD "GET" WITH ENDPOINT "/clouderp/api/qracore/be/urn:be:com.qad.base.codes.IReasonCode?{value.json.query}" WITH HEADERS ADD "Accept" "application/json" ADD "User-Agent" "Coreflux-Broker REST_API route (https://www.coreflux.org)" ADD "Authorization" "{value.json.authorization}" # Business component IInstalledBaseItem (URI from QAD Security Administration Guide 2020 QAE (field security appendix)): Read an installed base item (equipment at a customer). GET QEP Developer Guide 2020: api/qracore/be/?domainCode=..&=.. (doHttpGet examples). query: domainCode + key fields (URL-encoded); reply data holds the record(s). ADD EVENT GetInstalledBaseItem WITH SOURCE_TOPIC "erp/qad/service/installed-base/get" WITH DESTINATION_TOPIC "erp/qad/service/installed-base/get/response" WITH METHOD "GET" WITH ENDPOINT "/clouderp/api/qracore/be/urn:be:com.qad.service.installedbase.IInstalledBaseItem?{value.json.query}" WITH HEADERS ADD "Accept" "application/json" ADD "User-Agent" "Coreflux-Broker REST_API route (https://www.coreflux.org)" ADD "Authorization" "{value.json.authorization}" # Browse query (browse URIs urn:browse:: are in the QEP Developer Guide; this REST path is NOT in QAD's public guides, observed in public QAD client github.com/frantm05/QMA): Multi-record read through a browse. query: filters= EQ '' (URL-encoded), fields=, pageSize=. Reply data is the row list. ADD EVENT BrowseQuery WITH SOURCE_TOPIC "erp/qad/browse/query" WITH DESTINATION_TOPIC "erp/qad/browse/query/response" WITH METHOD "GET" WITH ENDPOINT "/clouderp/api/qracore/browses?browseId={value.json.browseId}&{value.json.query}" WITH HEADERS ADD "Accept" "application/json" ADD "User-Agent" "Coreflux-Broker REST_API route (https://www.coreflux.org)" ADD "Authorization" "{value.json.authorization}" # Business Document API (Online Help Foundation: Business Document API Documentation; per-environment Swagger / OpenAPI): Query REST API of a business document (filtering, sorting, paging, partial data). path = API URL from the Business Document API Documentation screen. ADD EVENT BusinessDocumentQuery WITH SOURCE_TOPIC "erp/qad/business-document/query" WITH DESTINATION_TOPIC "erp/qad/business-document/query/response" WITH METHOD "GET" WITH ENDPOINT "{value.json.path}?{value.json.query}" WITH HEADERS ADD "Accept" "application/json" ADD "User-Agent" "Coreflux-Broker REST_API route (https://www.coreflux.org)" ADD "Authorization" "{value.json.authorization}" # WRITE Business Document API (Online Help Foundation; inbound integration of a whole document, e.g. sales order header + lines): WRITE: send a complete business document. Check the method and body schema in your environment's Swagger before use. ADD EVENT BusinessDocumentSubmit WITH SOURCE_TOPIC "erp/qad/business-document/submit" WITH DESTINATION_TOPIC "erp/qad/business-document/submit/response" WITH METHOD "POST" WITH ENDPOINT "{value.json.path}" WITH HEADERS ADD "Accept" "application/json" ADD "User-Agent" "Coreflux-Broker REST_API route (https://www.coreflux.org)" ADD "Authorization" "{value.json.authorization}" ADD "Content-Type" "application/json" WITH BODY "{value.json.body}" # WRITE Business Document API + e-signature API mode (Security Administration Guide): WRITE: same as BusinessDocumentSubmit with X-QAD-Esig-Token from GetEsignatureToken, for components with an active e-signature configuration. ADD EVENT BusinessDocumentSubmitEsig WITH SOURCE_TOPIC "erp/qad/business-document/submit-esig" WITH DESTINATION_TOPIC "erp/qad/business-document/submit-esig/response" WITH METHOD "POST" WITH ENDPOINT "{value.json.path}" WITH HEADERS ADD "Accept" "application/json" ADD "User-Agent" "Coreflux-Broker REST_API route (https://www.coreflux.org)" ADD "Authorization" "{value.json.authorization}" ADD "Content-Type" "application/json" ADD "X-QAD-Esig-Token" "{value.json.esig_token}" WITH BODY "{value.json.body}" # Business Service API, HTTP method GET + QUERY parameters (Online Help Foundation: Business Services panel): Call a business service (non-CRUD method exposed as REST) whose HTTP Method Type is GET. path = REST URL Path from the Business Service API Documentation screen. ADD EVENT BusinessServiceGet WITH SOURCE_TOPIC "erp/qad/business-service/get" WITH DESTINATION_TOPIC "erp/qad/business-service/get/response" WITH METHOD "GET" WITH ENDPOINT "{value.json.path}?{value.json.query}" WITH HEADERS ADD "Accept" "application/json" ADD "User-Agent" "Coreflux-Broker REST_API route (https://www.coreflux.org)" ADD "Authorization" "{value.json.authorization}" # WRITE Business Service API, HTTP method POST + BODY parameters (Online Help Foundation: Business Services panel): WRITE (may write): call a business service whose HTTP Method Type is POST, for example a production or inventory transaction service. ADD EVENT BusinessServicePost WITH SOURCE_TOPIC "erp/qad/business-service/post" WITH DESTINATION_TOPIC "erp/qad/business-service/post/response" WITH METHOD "POST" WITH ENDPOINT "{value.json.path}" WITH HEADERS ADD "Accept" "application/json" ADD "User-Agent" "Coreflux-Broker REST_API route (https://www.coreflux.org)" ADD "Authorization" "{value.json.authorization}" ADD "Content-Type" "application/json" WITH BODY "{value.json.body}" # WRITE Business Service API + e-signature API mode (Security Administration Guide): WRITE (may write): same as BusinessServicePost with X-QAD-Esig-Token. ADD EVENT BusinessServicePostEsig WITH SOURCE_TOPIC "erp/qad/business-service/post-esig" WITH DESTINATION_TOPIC "erp/qad/business-service/post-esig/response" WITH METHOD "POST" WITH ENDPOINT "{value.json.path}" WITH HEADERS ADD "Accept" "application/json" ADD "User-Agent" "Coreflux-Broker REST_API route (https://www.coreflux.org)" ADD "Authorization" "{value.json.authorization}" ADD "Content-Type" "application/json" ADD "X-QAD-Esig-Token" "{value.json.esig_token}" WITH BODY "{value.json.body}" ``` ## Not packaged * Create, update or delete through `api/qracore/be` (not in QAD's public guides), and any DELETE event. * QXtend (SOAP) inbound and outbound. * Business Events: QAD publishes them to an event streaming platform through the Integration Platform, not to HTTP callbacks, so there is no receiver connector. * QAD Integration Platform flows, analytics and composer admin APIs, and the Java extension framework. * Fixed events for single business documents or business services: their paths and schemas differ per environment, so use the generic events with the path from your API documentation screen. # QAD Redzone Source: https://docs.coreflux.org/latest/lot-language/routes/rest-api-apps/qad-redzone LoT connector for the QAD Redzone API v2: OAuth tokens, locations, start and stop runs, shifts, maintenance actions, data sheets, assets and event polling Technical Preview ## Overview Client-mode REST API connector for the QAD Redzone public API v2: OAuth token and locations plus runs and shifts and maintenance actions and data sheets and event polling. `BASE_ADDRESS` defaults to `https://api.rz-ops.com`. Topics use `mes/redzone//`. | Event | Request | Source topic | | - | - | - | | `OAuthToken` | `POST /public/v2/oauth/token` | `mes/redzone/oauth/token/create` | | `LocationsList` | `GET /public/v2/organizations/{value.json.organizationId}/enterprises/{value.json.enterpriseId}/locations?limit={value.json.limit}&offset={value.json.offset}` | `mes/redzone/locations/list` | | `RunsList` | `GET /public/v2/organizations/{value.json.organizationId}/enterprises/{value.json.enterpriseId}/runs?limit={value.json.limit}&offset={value.json.offset}&started-after={value.json.startedAfter}` | `mes/redzone/runs/list` | | `RunGet` | `GET /public/v2/organizations/{value.json.organizationId}/enterprises/{value.json.enterpriseId}/runs/{value.json.runId}` | `mes/redzone/runs/get` | | `CurrentRunsAtLocation` | `GET /public/v2/organizations/{value.json.organizationId}/enterprises/{value.json.enterpriseId}/locations/{value.json.locationId}/runs/current?limit={value.json.limit}&offset={value.json.offset}` | `mes/redzone/locations/runs/current` | | `CurrentShiftAtLocation` | `GET /public/v2/organizations/{value.json.organizationId}/enterprises/{value.json.enterpriseId}/locations/{value.json.locationId}/shifts/current` | `mes/redzone/locations/shifts/current` | | `RunStart` | `POST /public/v2/organizations/{value.json.organizationId}/enterprises/{value.json.enterpriseId}/runs/start` | `mes/redzone/runs/start` | | `RunStop` | `POST /public/v2/organizations/{value.json.organizationId}/enterprises/{value.json.enterpriseId}/runs/{value.json.runId}/stop` | `mes/redzone/runs/stop` | | `ScheduledRunsList` | `GET /public/v2/organizations/{value.json.organizationId}/enterprises/{value.json.enterpriseId}/locations/{value.json.locationId}/scheduled-runs?limit={value.json.limit}&offset={value.json.offset}` | `mes/redzone/locations/scheduled-runs/list` | | `ScheduledRunCreate` | `POST /public/v2/organizations/{value.json.organizationId}/enterprises/{value.json.enterpriseId}/locations/{value.json.locationId}/scheduled-runs` | `mes/redzone/locations/scheduled-runs/create` | | `MaintenanceActionsList` | `GET /public/v2/organizations/{value.json.organizationId}/enterprises/{value.json.enterpriseId}/maintenance/actions?limit={value.json.limit}&offset={value.json.offset}` | `mes/redzone/maintenance/actions/list` | | `MaintenanceActionCommentCreate` | `POST /public/v2/organizations/{value.json.organizationId}/enterprises/{value.json.enterpriseId}/maintenance/actions/{value.json.maintenanceActionId}/comments` | `mes/redzone/maintenance/actions/comments/create` | | `CompletedDataSheetsList` | `GET /public/v2/organizations/{value.json.organizationId}/enterprises/{value.json.enterpriseId}/completed-data-sheets?limit={value.json.limit}&offset={value.json.offset}` | `mes/redzone/data-sheets/completed/list` | | `AssetsList` | `GET /public/v2/organizations/{value.json.organizationId}/enterprises/{value.json.enterpriseId}/assets?limit={value.json.limit}&offset={value.json.offset}` | `mes/redzone/assets/list` | | `EventsList` | `GET /public/v2/organizations/{value.json.organizationId}/events?limit={value.json.limit}&visibility-timeout={value.json.visibilityTimeout}` | `mes/redzone/events/list` | ## Authentication OAuth 2.0 client credentials. POST /public/v2/oauth/token with \{"client\_id","client\_secret"} (JSON) returns an access token; send it as Authorization: Bearer \{value.json.access\_token}. Client ID and secret are issued by Redzone to the customer organization. Runs, shifts, data sheets and problem types need the enterprise-integration entitlement; maintenance actions also need reliability-integration (x-entitlements in the OpenAPI spec). > [!WARNING] > > Keep credentials in broker secrets and never retain topics that carry tokens or client secrets. ## Notes * The Redzone OpenAPI spec (REDZONE PUBLIC API 2.0.0) is public at [https://api.rz-ops.com/public/v2/openapi.json](https://api.rz-ops.com/public/v2/openapi.json); the Developer Hub UI itself is not public. * API credentials (client\_id / client\_secret) are issued by Redzone to the customer. Nothing in the public material requires a QAD partner agreement, but endpoints tagged x-entitlements enterprise-integration (runs, shifts, data sheets, problem types) and reliability-integration (maintenance actions) need those Redzone add-ons on the customer contract. * OAuthToken is the only event whose body carries a credential (client\_id / client\_secret in the token request), the same pattern as the Box OAuthToken event. Never retain mes/redzone/oauth/# and restrict it with ACLs. * List endpoints require limit (1..100) and offset (>= 0) query values; pass them on every list payload. ## Basic Syntax ```lot wrap theme={"theme":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} DEFINE CONNECTOR QadRedzoneRestApi WITH TYPE REST_API ADD REST_API_CONFIG WITH BASE_ADDRESS "https://api.rz-ops.com" WITH ENABLE_CLIENT "true" WITH USE_SSL "true" WITH REQUEST_TIMEOUT '60' WITH DEFAULT_CONTENT_TYPE "application/json" ADD EVENT OAuthToken WITH SOURCE_TOPIC "mes/redzone/oauth/token/create" WITH DESTINATION_TOPIC "mes/redzone/oauth/token/create/response" WITH METHOD "POST" WITH ENDPOINT "/public/v2/oauth/token" WITH HEADERS ADD "Accept" "application/json" ADD "Content-Type" "application/json" WITH BODY "{value.json.body}" ADD EVENT LocationsList WITH SOURCE_TOPIC "mes/redzone/locations/list" WITH DESTINATION_TOPIC "mes/redzone/locations/list/response" WITH METHOD "GET" WITH ENDPOINT "/public/v2/organizations/{value.json.organizationId}/enterprises/{value.json.enterpriseId}/locations?limit={value.json.limit}&offset={value.json.offset}" WITH HEADERS ADD "Accept" "application/json" ADD "Authorization" "Bearer {value.json.access_token}" ADD EVENT RunsList WITH SOURCE_TOPIC "mes/redzone/runs/list" WITH DESTINATION_TOPIC "mes/redzone/runs/list/response" WITH METHOD "GET" WITH ENDPOINT "/public/v2/organizations/{value.json.organizationId}/enterprises/{value.json.enterpriseId}/runs?limit={value.json.limit}&offset={value.json.offset}&started-after={value.json.startedAfter}" WITH HEADERS ADD "Accept" "application/json" ADD "Authorization" "Bearer {value.json.access_token}" ADD EVENT RunGet WITH SOURCE_TOPIC "mes/redzone/runs/get" WITH DESTINATION_TOPIC "mes/redzone/runs/get/response" WITH METHOD "GET" WITH ENDPOINT "/public/v2/organizations/{value.json.organizationId}/enterprises/{value.json.enterpriseId}/runs/{value.json.runId}" WITH HEADERS ADD "Accept" "application/json" ADD "Authorization" "Bearer {value.json.access_token}" ADD EVENT CurrentRunsAtLocation WITH SOURCE_TOPIC "mes/redzone/locations/runs/current" WITH DESTINATION_TOPIC "mes/redzone/locations/runs/current/response" WITH METHOD "GET" WITH ENDPOINT "/public/v2/organizations/{value.json.organizationId}/enterprises/{value.json.enterpriseId}/locations/{value.json.locationId}/runs/current?limit={value.json.limit}&offset={value.json.offset}" WITH HEADERS ADD "Accept" "application/json" ADD "Authorization" "Bearer {value.json.access_token}" ADD EVENT CurrentShiftAtLocation WITH SOURCE_TOPIC "mes/redzone/locations/shifts/current" WITH DESTINATION_TOPIC "mes/redzone/locations/shifts/current/response" WITH METHOD "GET" WITH ENDPOINT "/public/v2/organizations/{value.json.organizationId}/enterprises/{value.json.enterpriseId}/locations/{value.json.locationId}/shifts/current" WITH HEADERS ADD "Accept" "application/json" ADD "Authorization" "Bearer {value.json.access_token}" ADD EVENT RunStart WITH SOURCE_TOPIC "mes/redzone/runs/start" WITH DESTINATION_TOPIC "mes/redzone/runs/start/response" WITH METHOD "POST" WITH ENDPOINT "/public/v2/organizations/{value.json.organizationId}/enterprises/{value.json.enterpriseId}/runs/start" WITH HEADERS ADD "Accept" "application/json" ADD "Content-Type" "application/json" ADD "Authorization" "Bearer {value.json.access_token}" WITH BODY "{value.json.body}" ADD EVENT RunStop WITH SOURCE_TOPIC "mes/redzone/runs/stop" WITH DESTINATION_TOPIC "mes/redzone/runs/stop/response" WITH METHOD "POST" WITH ENDPOINT "/public/v2/organizations/{value.json.organizationId}/enterprises/{value.json.enterpriseId}/runs/{value.json.runId}/stop" WITH HEADERS ADD "Accept" "application/json" ADD "Authorization" "Bearer {value.json.access_token}" WITH BODY "" ADD EVENT ScheduledRunsList WITH SOURCE_TOPIC "mes/redzone/locations/scheduled-runs/list" WITH DESTINATION_TOPIC "mes/redzone/locations/scheduled-runs/list/response" WITH METHOD "GET" WITH ENDPOINT "/public/v2/organizations/{value.json.organizationId}/enterprises/{value.json.enterpriseId}/locations/{value.json.locationId}/scheduled-runs?limit={value.json.limit}&offset={value.json.offset}" WITH HEADERS ADD "Accept" "application/json" ADD "Authorization" "Bearer {value.json.access_token}" ADD EVENT ScheduledRunCreate WITH SOURCE_TOPIC "mes/redzone/locations/scheduled-runs/create" WITH DESTINATION_TOPIC "mes/redzone/locations/scheduled-runs/create/response" WITH METHOD "POST" WITH ENDPOINT "/public/v2/organizations/{value.json.organizationId}/enterprises/{value.json.enterpriseId}/locations/{value.json.locationId}/scheduled-runs" WITH HEADERS ADD "Accept" "application/json" ADD "Content-Type" "application/json" ADD "Authorization" "Bearer {value.json.access_token}" WITH BODY "{value.json.body}" ADD EVENT MaintenanceActionsList WITH SOURCE_TOPIC "mes/redzone/maintenance/actions/list" WITH DESTINATION_TOPIC "mes/redzone/maintenance/actions/list/response" WITH METHOD "GET" WITH ENDPOINT "/public/v2/organizations/{value.json.organizationId}/enterprises/{value.json.enterpriseId}/maintenance/actions?limit={value.json.limit}&offset={value.json.offset}" WITH HEADERS ADD "Accept" "application/json" ADD "Authorization" "Bearer {value.json.access_token}" ADD EVENT MaintenanceActionCommentCreate WITH SOURCE_TOPIC "mes/redzone/maintenance/actions/comments/create" WITH DESTINATION_TOPIC "mes/redzone/maintenance/actions/comments/create/response" WITH METHOD "POST" WITH ENDPOINT "/public/v2/organizations/{value.json.organizationId}/enterprises/{value.json.enterpriseId}/maintenance/actions/{value.json.maintenanceActionId}/comments" WITH HEADERS ADD "Accept" "application/json" ADD "Content-Type" "application/json" ADD "Authorization" "Bearer {value.json.access_token}" WITH BODY "{value.json.body}" ADD EVENT CompletedDataSheetsList WITH SOURCE_TOPIC "mes/redzone/data-sheets/completed/list" WITH DESTINATION_TOPIC "mes/redzone/data-sheets/completed/list/response" WITH METHOD "GET" WITH ENDPOINT "/public/v2/organizations/{value.json.organizationId}/enterprises/{value.json.enterpriseId}/completed-data-sheets?limit={value.json.limit}&offset={value.json.offset}" WITH HEADERS ADD "Accept" "application/json" ADD "Authorization" "Bearer {value.json.access_token}" ADD EVENT AssetsList WITH SOURCE_TOPIC "mes/redzone/assets/list" WITH DESTINATION_TOPIC "mes/redzone/assets/list/response" WITH METHOD "GET" WITH ENDPOINT "/public/v2/organizations/{value.json.organizationId}/enterprises/{value.json.enterpriseId}/assets?limit={value.json.limit}&offset={value.json.offset}" WITH HEADERS ADD "Accept" "application/json" ADD "Authorization" "Bearer {value.json.access_token}" ADD EVENT EventsList WITH SOURCE_TOPIC "mes/redzone/events/list" WITH DESTINATION_TOPIC "mes/redzone/events/list/response" WITH METHOD "GET" WITH ENDPOINT "/public/v2/organizations/{value.json.organizationId}/events?limit={value.json.limit}&visibility-timeout={value.json.visibilityTimeout}" WITH HEADERS ADD "Accept" "application/json" ADD "Authorization" "Bearer {value.json.access_token}" ``` ## Sources * [https://api.rz-ops.com/public/v2/openapi.json](https://api.rz-ops.com/public/v2/openapi.json) * [https://www.rzsoftware.com/product/integrations](https://www.rzsoftware.com/product/integrations) * [https://help.boomi.com/docs/Atomsphere/Integration/Connectors/int-QAD\_Redzone\_connection](https://help.boomi.com/docs/Atomsphere/Integration/Connectors/int-QAD_Redzone_connection) # Qlik Cloud Source: https://docs.coreflux.org/latest/lot-language/routes/rest-api-apps/qlik-cloud LoT connector for Qlik Cloud Analytics: reload apps on shift or batch close, upload CSV data files, schedule reloads, run automations and register webhooks Technical Preview ## Overview A client-mode connector for the [Qlik Cloud REST APIs](https://qlik.dev/apis/rest/). Use it to let plant events drive Qlik: reload an app when a shift, batch or ETL job closes, upload a CSV rollup (shift totals, OEE per line) as a data file the app loads, schedule or pause reload tasks, and run a Qlik Automate automation. It also puts Qlik state on MQTT: reload status and logs, data model metadata, items, spaces and audit events. The connector `QlikCloudRestApi` in `QlikCloudRestApi.lot` has 44 events (7 POST, 34 GET, 2 PUT, 1 PATCH). Nine are writes: `ReloadCreate`, `ReloadCancel`, `ReloadTaskCreate`, `ReloadTaskUpdate`, `DataFileUpload`, `DataFileReplace`, `AutomationRunCreate`, `WebhookCreate` and `WebhookUpdate`. There are no DELETE events. Set `BASE_ADDRESS` to your tenant URL with no trailing slash, for example `https://acme.eu.qlikcloud.com` (a tenant alias hostname also works). Every endpoint already starts with `/api/v1`, except `/oauth/token`. Topics use `analytics/qlik-cloud//`; replies land on `/response`. See the base guide: [REST API Connectors](/latest/lot-language/routes/rest-api-routes). > [!NOTE] > > **Auth:** every event except `OAuthToken` sends `Authorization: Bearer `. The token is either an API key (a tenant admin enables API keys and gives the user the Developer role, then the user creates a key under **Profile settings > API keys**) or an OAuth machine-to-machine token: publish `{"body":{"client_id":"...","client_secret":"...","grant_type":"client_credentials"}}` to `analytics/qlik-cloud/auth/token` and use the returned `access_token` until `expires_in` runs out. The key or token acts as its user, so give it only the spaces and roles the broker needs. Keep `QLIK_API_KEY` (or `QLIK_CLIENT_ID` and `QLIK_CLIENT_SECRET`) in broker secrets, read them with `GET SECRET` in the action that builds the payload, never retain topics that carry them, and restrict `analytics/qlik-cloud/auth/#` with ACLs. ## Events | Event | Request | Source topic | Notes | | - | - | - | - | | OAuthToken | `POST /oauth/token` | `analytics/qlik-cloud/auth/token` | Get an access token for an OAuth machine-to-machine client (lifetime in expires\_in). No Authorization header. | | UsersMe | `GET /api/v1/users/me` | `analytics/qlik-cloud/users/me` | Smoke: the user behind the API key or token (id, name, tenantId, roles). | | TenantsMe | `GET /api/v1/tenants/me` | `analytics/qlik-cloud/tenants/me` | The tenant behind the key (id, name, hostnames, datacenter). | | UsersList | `GET /api/v1/users?{value.json.query}` | `analytics/qlik-cloud/users/list` | Users in the tenant. | | ReloadsList | `GET /api/v1/reloads?{value.json.query}` | `analytics/qlik-cloud/reloads/list` | Reload history of an app. | | ReloadCreate | `POST /api/v1/reloads` | `analytics/qlik-cloud/reloads/create` | Write, starts a reload. Reload an app when a shift, batch or ETL job closes (catalog event). | | ReloadGet | `GET /api/v1/reloads/{value.json.reload_id}` | `analytics/qlik-cloud/reloads/get` | One reload: status QUEUED, RELOADING, SUCCEEDED, FAILED, CANCELED (catalog event), duration and log. | | ReloadCancel | `POST /api/v1/reloads/{value.json.reload_id}/actions/cancel` | `analytics/qlik-cloud/reloads/cancel` | Write. Cancel a queued or running reload (no body). | | ReloadTasksList | `GET /api/v1/reload-tasks?{value.json.query}` | `analytics/qlik-cloud/reload_tasks/list` | Scheduled reload tasks. | | ReloadTaskGet | `GET /api/v1/reload-tasks/{value.json.task_id}` | `analytics/qlik-cloud/reload_tasks/get` | One reload task (recurrence, state, next run). | | ReloadTaskCreate | `POST /api/v1/reload-tasks` | `analytics/qlik-cloud/reload_tasks/create` | Write. Schedule reloads. | | ReloadTaskUpdate | `PUT /api/v1/reload-tasks/{value.json.task_id}` | `analytics/qlik-cloud/reload_tasks/update` | Write. Replace or pause a reload task (no delete event: set state Disabled). | | AppGet | `GET /api/v1/apps/{value.json.app_id}` | `analytics/qlik-cloud/apps/get` | App attributes (name, owner, space, lastReloadTime). | | AppDataMetadata | `GET /api/v1/apps/{value.json.app_id}/data/metadata` | `analytics/qlik-cloud/apps/data_metadata` | Data model metadata: tables, fields, row counts, static byte size, reload\_meta. | | AppDataLineage | `GET /api/v1/apps/{value.json.app_id}/data/lineage` | `analytics/qlik-cloud/apps/data_lineage` | Data sources the app loads from (lineage). | | AppReloadLogs | `GET /api/v1/apps/{value.json.app_id}/reloads/logs` | `analytics/qlik-cloud/apps/reload_logs` | Reload log list for an app. | | AppReloadLog | `GET /api/v1/apps/{value.json.app_id}/reloads/logs/{value.json.reload_id}` | `analytics/qlik-cloud/apps/reload_log` | One reload log as plain text (the script output). The broker publishes the body as received. | | AppReloadMetadata | `GET /api/v1/apps/{value.json.app_id}/reloads/metadata/{value.json.reload_id}?{value.json.query}` | `analytics/qlik-cloud/apps/reload_metadata` | Metadata of one reload (duration, peak memory, rows per table). | | ItemsList | `GET /api/v1/items?{value.json.query}` | `analytics/qlik-cloud/items/list` | Search apps and other items. | | ItemGet | `GET /api/v1/items/{value.json.item_id}` | `analytics/qlik-cloud/items/get` | One item (resourceId is the app id for apps). | | SpacesList | `GET /api/v1/spaces?{value.json.query}` | `analytics/qlik-cloud/spaces/list` | Spaces. | | SpaceGet | `GET /api/v1/spaces/{value.json.space_id}` | `analytics/qlik-cloud/spaces/get` | One space. | | DataFilesList | `GET /api/v1/data-files?{value.json.query}` | `analytics/qlik-cloud/data_files/list` | Data files in the personal or a space data connection. | | DataFileGet | `GET /api/v1/data-files/{value.json.data_file_id}` | `analytics/qlik-cloud/data_files/get` | One data file (name, size, modifiedDate). | | DataFileUpload | `POST /api/v1/data-files` | `analytics/qlik-cloud/data_files/upload` | Write. Catalog 'POST data-files': upload a CSV rollup (shift totals, OEE per line) for apps to load. | | DataFileReplace | `PUT /api/v1/data-files/{value.json.data_file_id}` | `analytics/qlik-cloud/data_files/replace` | Write. Replace the content of an existing data file (same multipart shape, Json part may carry the name). Then ReloadCreate. | | DataFilesQuotas | `GET /api/v1/data-files/quotas` | `analytics/qlik-cloud/data_files/quotas` | Data file quotas (max file size, space used). | | DataFilesConnections | `GET /api/v1/data-files/connections?{value.json.query}` | `analytics/qlik-cloud/data_files/connections` | Data-file connections per space (use connectionId in DataFileUpload). | | DataConnectionsList | `GET /api/v1/data-connections` | `analytics/qlik-cloud/data_connections/list` | Data connections visible to the user (the spec documents no query parameters). | | AutomationsList | `GET /api/v1/automations?{value.json.query}` | `analytics/qlik-cloud/automations/list` | Qlik Automate automations. | | AutomationGet | `GET /api/v1/automations/{value.json.automation_id}` | `analytics/qlik-cloud/automations/get` | One automation. | | AutomationRunCreate | `POST /api/v1/automations/{value.json.automation_id}/runs` | `analytics/qlik-cloud/automations/run` | Write, runs the automation. Run an automation from a plant event. | | AutomationRunsList | `GET /api/v1/automations/{value.json.automation_id}/runs?{value.json.query}` | `analytics/qlik-cloud/automations/runs` | Runs of an automation. | | AutomationRunGet | `GET /api/v1/automations/{value.json.automation_id}/runs/{value.json.run_id}` | `analytics/qlik-cloud/automations/run_get` | One automation run (status, title, timestamps). | | WebhooksList | `GET /api/v1/webhooks?{value.json.query}` | `analytics/qlik-cloud/webhooks/list` | Webhooks. | | WebhookGet | `GET /api/v1/webhooks/{value.json.webhook_id}` | `analytics/qlik-cloud/webhooks/get` | One webhook. | | WebhookEventTypes | `GET /api/v1/webhooks/event-types` | `analytics/qlik-cloud/webhooks/event_types` | Event types a webhook can subscribe to (for example com.qlik.v1.reload.finished). | | WebhookCreate | `POST /api/v1/webhooks` | `analytics/qlik-cloud/webhooks/create` | Write. Send Qlik events (reload finished, app published) to a broker ENABLE\_SERVER connector. | | WebhookUpdate | `PATCH /api/v1/webhooks/{value.json.webhook_id}` | `analytics/qlik-cloud/webhooks/update` | Write. JSON Patch a webhook (no delete event: disable it). | | WebhookDeliveries | `GET /api/v1/webhooks/{value.json.webhook_id}/deliveries?{value.json.query}` | `analytics/qlik-cloud/webhooks/deliveries` | Delivery attempts of a webhook (status, response code). | | AuditsList | `GET /api/v1/audits?{value.json.query}` | `analytics/qlik-cloud/audits/list` | Audit events (tenant admin). | | AuditGet | `GET /api/v1/audits/{value.json.audit_id}` | `analytics/qlik-cloud/audits/get` | One audit event. | | LicensesOverview | `GET /api/v1/licenses/overview` | `analytics/qlik-cloud/licenses/overview` | License overview (tenant admin). | | LicensesConsumption | `GET /api/v1/licenses/consumption?{value.json.query}` | `analytics/qlik-cloud/licenses/consumption` | License consumption records (tenant admin). | Payload fields are JSON on the source topic: ids (`app_id`, `reload_id`, `task_id`, `item_id`, `space_id`, `data_file_id`, `automation_id`, `run_id`, `webhook_id`, `audit_id`), `query` (a URL query string without `?`, or `""`; `ReloadsList` needs `appId=...`), `body` (JSON for POST, PUT and PATCH events; a JSON Patch array for `WebhookUpdate`), `multipart` for data file uploads, and `access_token`. ## Basic Syntax An excerpt of the template (the full file has all 44 events and comments): ```lot wrap theme={"theme":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} DEFINE CONNECTOR QlikCloudRestApi WITH TYPE REST_API ADD REST_API_CONFIG WITH BASE_ADDRESS "https://YOUR_TENANT.YOUR_REGION.qlikcloud.com" WITH ENABLE_CLIENT "true" WITH USE_SSL "true" WITH REQUEST_TIMEOUT '120' WITH DEFAULT_CONTENT_TYPE "application/json" ADD EVENT UsersMe WITH SOURCE_TOPIC "analytics/qlik-cloud/users/me" WITH DESTINATION_TOPIC "analytics/qlik-cloud/users/me/response" WITH METHOD "GET" WITH ENDPOINT "/api/v1/users/me" WITH HEADERS ADD "Accept" "application/json" ADD "User-Agent" "Coreflux-Broker REST_API route (https://www.coreflux.org)" ADD "Authorization" "Bearer {value.json.access_token}" ADD EVENT ReloadCreate WITH SOURCE_TOPIC "analytics/qlik-cloud/reloads/create" WITH DESTINATION_TOPIC "analytics/qlik-cloud/reloads/create/response" WITH METHOD "POST" WITH ENDPOINT "/api/v1/reloads" WITH HEADERS ADD "Accept" "application/json" ADD "User-Agent" "Coreflux-Broker REST_API route (https://www.coreflux.org)" ADD "Authorization" "Bearer {value.json.access_token}" ADD "Content-Type" "application/json" WITH BODY "{value.json.body}" ADD EVENT ReloadGet WITH SOURCE_TOPIC "analytics/qlik-cloud/reloads/get" WITH DESTINATION_TOPIC "analytics/qlik-cloud/reloads/get/response" WITH METHOD "GET" WITH ENDPOINT "/api/v1/reloads/{value.json.reload_id}" WITH HEADERS ADD "Accept" "application/json" ADD "User-Agent" "Coreflux-Broker REST_API route (https://www.coreflux.org)" ADD "Authorization" "Bearer {value.json.access_token}" ADD EVENT DataFileUpload WITH SOURCE_TOPIC "analytics/qlik-cloud/data_files/upload" WITH DESTINATION_TOPIC "analytics/qlik-cloud/data_files/upload/response" WITH METHOD "POST" WITH ENDPOINT "/api/v1/data-files" WITH HEADERS ADD "Accept" "application/json" ADD "User-Agent" "Coreflux-Broker REST_API route (https://www.coreflux.org)" ADD "Authorization" "Bearer {value.json.access_token}" ADD "Content-Type" "multipart/form-data; boundary=CorefluxQlikBoundary" WITH BODY "{value.json.multipart}" ``` ## Examples ### Reload an app when a shift closes ```lot wrap theme={"theme":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} DEFINE ACTION QlikReloadOnShiftClose ON TOPIC "plant/line2/shift/closed" DO SET "key" WITH GET SECRET "QLIK_API_KEY" PUBLISH TOPIC "analytics/qlik-cloud/reloads/create" WITH { "access_token": {key}, "body": { "appId": "116dbfae-7fb9-4983-8e23-5ccd8c508722", "partial": false, "variables": { "vLine": "Line 2" } } } ``` The response on `analytics/qlik-cloud/reloads/create/response` carries the reload `id` and `status` `QUEUED`. Publish `{"access_token":"...","reload_id":""}` to `analytics/qlik-cloud/reloads/get` to follow it to `SUCCEEDED` or `FAILED`. ### Upload a CSV rollup as a data file `DataFileUpload` sends `multipart` unchanged with `Content-Type: multipart/form-data; boundary=CorefluxQlikBoundary`. Build the body with CRLF line endings: a `Json` part with the file name, then a `File` part with the CSV. ```text theme={"theme":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} --CorefluxQlikBoundary Content-Disposition: form-data; name="Json" Content-Type: application/json {"name":"plant/oee-2026-10-05.csv"} --CorefluxQlikBoundary Content-Disposition: form-data; name="File"; filename="oee-2026-10-05.csv" Content-Type: text/csv line,shift,good,scrap,oee Line 1,A,4210,37,0.81 --CorefluxQlikBoundary-- ``` A 201 response returns the file `id`. If the name already exists Qlik answers 409; publish to `analytics/qlik-cloud/data_files/replace` with `data_file_id` instead, then reload the app. ### Get Qlik events back on MQTT `WebhookCreate` registers a webhook that posts Qlik events, such as `com.qlik.v1.reload.finished`, to a URL you choose. Point it at a separate REST API connector with `ENABLE_SERVER` on the broker. Pause it later with `WebhookUpdate` and `[{"op":"replace","path":"/enabled","value":false}]`. ## Not packaged * DELETE operations (apps, data files, reload tasks, webhooks, spaces, users), on purpose. Pause reload tasks with `state` `Disabled` and webhooks with `enabled` false. * App import, export and publish; space, user, API key and tenant administration. * The Qlik associative engine (QIX), which uses WebSockets, not REST. * The webhook receiver itself (use a separate `ENABLE_SERVER` connector). # QuestDB Source: https://docs.coreflux.org/latest/lot-language/routes/rest-api-apps/questdb Client-mode REST API connector for QuestDB: ILP over HTTP writes, SQL queries via /exec, CSV export via /exp, and /ping health Technical Preview ## Overview A client-mode connector for the [QuestDB HTTP REST API](https://questdb.com/docs/reference/api/rest/) that streams MQTT telemetry into QuestDB tables and runs SQL from MQTT: | Catalog connector event | Endpoint | Notes | | - | - | - | | POST write (ILP) | `POST /write?precision=` | [ILP over HTTP](https://questdb.com/docs/reference/api/ilp/overview/), `text/plain` line protocol body, `204` on success. Tables and columns auto-create. | | GET exec | `GET /exec?query=` | SQL (SELECT, INSERT, CREATE TABLE) returning JSON `columns` + `dataset` | | GET exp | `GET /exp?query=` | SQL result as CSV | | GET ping | `GET /ping` | Health check, `204` | Default `BASE_ADDRESS` is `http://YOUR_QUESTDB_HOST:9000` (QuestDB's HTTP port). Topics use `data/questdb//`. > [!NOTE] > > **CSV import (`/imp`) is not a client event.** QuestDB only accepts `multipart/form-data` uploads on `/imp`; a raw body returns `400 Non-multipart POST not supported`. Use ILP `/write` for streaming rows, or `/exec` with `INSERT INTO …` for small batches. ## Auth QuestDB open source has no HTTP auth by default. When you enable `http.user` / `http.password` (OSS) or RBAC (QuestDB Enterprise), pass the full header value in `authorization`, such as `Basic base64(user:pass)` or `Bearer `. Send an empty string when the server has no auth. Keep the value in a Hub secret such as `QUESTDB_AUTH` and never commit it. ## Basic Syntax ```lot wrap theme={"theme":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} DEFINE CONNECTOR QuestdbRestApi WITH TYPE REST_API ADD REST_API_CONFIG WITH BASE_ADDRESS "http://YOUR_QUESTDB_HOST:9000" WITH ENABLE_CLIENT "true" WITH USE_SSL "false" WITH REQUEST_TIMEOUT '60' WITH DEFAULT_CONTENT_TYPE "application/json" ADD EVENT Ping WITH SOURCE_TOPIC "data/questdb/ping" WITH DESTINATION_TOPIC "data/questdb/ping/response" WITH METHOD "GET" WITH ENDPOINT "/ping" WITH HEADERS ADD "Authorization" "{value.json.authorization}" ADD EVENT ExecQuery WITH SOURCE_TOPIC "data/questdb/query/exec" WITH DESTINATION_TOPIC "data/questdb/query/exec/response" WITH METHOD "GET" WITH ENDPOINT "/exec?query={value.json.query}&count=true" WITH HEADERS ADD "Accept" "application/json" ADD "Authorization" "{value.json.authorization}" ADD EVENT WriteIlp WITH SOURCE_TOPIC "data/questdb/ilp/write" WITH DESTINATION_TOPIC "data/questdb/ilp/write/response" WITH METHOD "POST" WITH ENDPOINT "/write?precision={value.json.precision}" WITH HEADERS ADD "Content-Type" "text/plain; charset=utf-8" ADD "Authorization" "{value.json.authorization}" WITH BODY "{payload}" ADD EVENT ExportCsv WITH SOURCE_TOPIC "data/questdb/query/export" WITH DESTINATION_TOPIC "data/questdb/query/export/response" WITH METHOD "GET" WITH ENDPOINT "/exp?query={value.json.query}" WITH HEADERS ADD "Accept" "text/csv" ADD "Authorization" "{value.json.authorization}" ``` ## Example MQTT payloads **Write telemetry (topic `data/questdb/ilp/write`).** JSON fields fill the query placeholders; the body is line protocol: ```json theme={"theme":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} { "authorization": "", "precision": "ms" } ``` ```text theme={"theme":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} site_readings,site=porto value=21.5 1791234567890 ``` **SQL query (topic `data/questdb/query/exec`).** URL-encode the SQL before publishing: ```json theme={"theme":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} { "authorization": "", "query": "SELECT%20site,value%20FROM%20site_readings%20LIMIT%2010" } ``` The response topic receives QuestDB's JSON, for example `{"columns":[…],"dataset":[["porto",21.5]],"count":1}`. **CSV export (topic `data/questdb/query/export`)** takes the same payload as `exec`. **Health (topic `data/questdb/ping`)** takes `{ "authorization": "" }`. ## Related * [REST API Connectors](/latest/lot-language/routes/rest-api-routes) * [REST API Apps](/latest/lot-language/routes/rest-api-apps/index) * [InfluxDB](/latest/lot-language/routes/rest-api-apps/influxdb) # QuickBooks Online Source: https://docs.coreflux.org/latest/lot-language/routes/rest-api-apps/quickbooks-online LoT connector for QuickBooks Online: create invoices from production events, adjust inventory quantities, query items and customers, and refresh OAuth tokens Technical Preview ## Overview A client-mode connector for the [QuickBooks Online Accounting API](https://developer.intuit.com/app/developer/qbo/docs/api/accounting/most-commonly-used/account) (v3). Use it to close the loop between the shop floor and accounting: bill a customer when a batch ships, write scrap or consumption back as an inventory quantity adjustment, and read item stock levels or recent changes into MQTT. The file holds two connectors. `QuickBooksOnlineRestApi` talks to the accounting API, and `QuickBooksOnlineOAuth` refreshes the OAuth access token on Intuit's token host. | Event | Request | Notes | | - | - | - | | GetCompanyInfo | `GET /v3/company/{realm}/companyinfo/{realm}` | Smoke and realm check | | Query | `GET /v3/company/{realm}/query?query=` | SQL-like query, URL-encoded (for example `select * from Item`) | | ReadItem | `GET /v3/company/{realm}/item/{id}` | `QtyOnHand` for inventory items | | CreateItem | `POST /v3/company/{realm}/item` | Create, or sparse update with `Id` and `SyncToken` | | CreateInventoryAdjustment | `POST /v3/company/{realm}/inventoryadjustment` | Item quantity adjustment (`QtyDiff`) | | CreateInvoice | `POST /v3/company/{realm}/invoice` | `CustomerRef` and `Line[]` | | ReadInvoice | `GET /v3/company/{realm}/invoice/{id}` | | | SendInvoice | `POST /v3/company/{realm}/invoice/{id}/send?sendTo=` | Emails the invoice; empty body | | ReadCustomer | `GET /v3/company/{realm}/customer/{id}` | | | CreateCustomer | `POST /v3/company/{realm}/customer` | `DisplayName` must be unique | | ChangeDataCapture | `GET /v3/company/{realm}/cdc?entities=&changedSince=` | Changes in the last 30 days | | Batch | `POST /v3/company/{realm}/batch` | Up to 30 operations per call | | ProfitAndLossReport | `GET /v3/company/{realm}/reports/ProfitAndLoss?start_date=&end_date=` | | | RefreshToken | `POST /oauth2/v1/tokens/bearer` (on `oauth.platform.intuit.com`) | New access token and refresh token | Every accounting call adds `minorversion=75`. Since August 2025 QuickBooks ignores minor versions 1 to 74 and answers as 75, so the template pins the current base. Set `BASE_ADDRESS` to `https://quickbooks.api.intuit.com` for real companies or `https://sandbox-quickbooks.api.intuit.com` for sandbox companies. Topics use `finance/quickbooks//`; replies land on `/response`. See the base guide: [REST API Connectors](/latest/lot-language/routes/rest-api-routes). > [!NOTE] > > **Auth:** QuickBooks Online uses OAuth 2.0 authorization code with the `com.intuit.quickbooks.accounting` scope. Do the one-time consent outside the broker (the Intuit [OAuth 2.0 Playground](https://developer.intuit.com/app/developer/playground) or your own app) to get a refresh token and the company's `realmId`. After that the broker can stay connected on its own: publish to the RefreshToken event to get a fresh access token (valid 60 minutes) and send it as `access_token` with `realm_id` on every accounting call. Keep the client id, client secret and refresh token in Hub secrets (suggested names `QBO_CLIENT_ID`, `QBO_CLIENT_SECRET`, `QBO_REFRESH_TOKEN`, `QBO_REALM_ID`). Never commit them into the `.lot`. > [!WARNING] > > The refresh token **rotates**. The token response can return a new `refresh_token`, and the old one stops working soon after. Always store the latest value. A refresh token that is not used for about 100 days expires, and the user has to reconnect. ## Basic Syntax ```lot wrap theme={"theme":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} DEFINE CONNECTOR QuickBooksOnlineRestApi WITH TYPE REST_API ADD REST_API_CONFIG WITH BASE_ADDRESS "https://quickbooks.api.intuit.com" WITH ENABLE_CLIENT "true" WITH USE_SSL "true" WITH REQUEST_TIMEOUT '60' WITH DEFAULT_CONTENT_TYPE "application/json" # Sandbox: https://sandbox-quickbooks.api.intuit.com. Auth: Bearer {value.json.access_token}; realm {value.json.realm_id}. Never commit tokens. ADD EVENT GetCompanyInfo WITH SOURCE_TOPIC "finance/quickbooks/company/info/get" WITH DESTINATION_TOPIC "finance/quickbooks/company/info/get/response" WITH METHOD "GET" WITH ENDPOINT "/v3/company/{value.json.realm_id}/companyinfo/{value.json.realm_id}?minorversion=75" WITH HEADERS ADD "Accept" "application/json" ADD "Authorization" "Bearer {value.json.access_token}" ADD EVENT Query WITH SOURCE_TOPIC "finance/quickbooks/query" WITH DESTINATION_TOPIC "finance/quickbooks/query/response" WITH METHOD "GET" WITH ENDPOINT "/v3/company/{value.json.realm_id}/query?query={value.json.query}&minorversion=75" WITH HEADERS ADD "Accept" "application/json" ADD "Authorization" "Bearer {value.json.access_token}" ADD EVENT ReadItem WITH SOURCE_TOPIC "finance/quickbooks/item/get" WITH DESTINATION_TOPIC "finance/quickbooks/item/get/response" WITH METHOD "GET" WITH ENDPOINT "/v3/company/{value.json.realm_id}/item/{value.json.item_id}?minorversion=75" WITH HEADERS ADD "Accept" "application/json" ADD "Authorization" "Bearer {value.json.access_token}" ADD EVENT CreateItem WITH SOURCE_TOPIC "finance/quickbooks/item/create" WITH DESTINATION_TOPIC "finance/quickbooks/item/create/response" WITH METHOD "POST" WITH ENDPOINT "/v3/company/{value.json.realm_id}/item?minorversion=75" WITH HEADERS ADD "Accept" "application/json" ADD "Content-Type" "application/json" ADD "Authorization" "Bearer {value.json.access_token}" WITH BODY "{value.json.body}" ADD EVENT CreateInventoryAdjustment WITH SOURCE_TOPIC "finance/quickbooks/inventoryadjustment/create" WITH DESTINATION_TOPIC "finance/quickbooks/inventoryadjustment/create/response" WITH METHOD "POST" WITH ENDPOINT "/v3/company/{value.json.realm_id}/inventoryadjustment?minorversion=75" WITH HEADERS ADD "Accept" "application/json" ADD "Content-Type" "application/json" ADD "Authorization" "Bearer {value.json.access_token}" WITH BODY "{value.json.body}" ADD EVENT CreateInvoice WITH SOURCE_TOPIC "finance/quickbooks/invoice/create" WITH DESTINATION_TOPIC "finance/quickbooks/invoice/create/response" WITH METHOD "POST" WITH ENDPOINT "/v3/company/{value.json.realm_id}/invoice?minorversion=75" WITH HEADERS ADD "Accept" "application/json" ADD "Content-Type" "application/json" ADD "Authorization" "Bearer {value.json.access_token}" WITH BODY "{value.json.body}" ADD EVENT ReadInvoice WITH SOURCE_TOPIC "finance/quickbooks/invoice/get" WITH DESTINATION_TOPIC "finance/quickbooks/invoice/get/response" WITH METHOD "GET" WITH ENDPOINT "/v3/company/{value.json.realm_id}/invoice/{value.json.invoice_id}?minorversion=75" WITH HEADERS ADD "Accept" "application/json" ADD "Authorization" "Bearer {value.json.access_token}" ADD EVENT SendInvoice WITH SOURCE_TOPIC "finance/quickbooks/invoice/send" WITH DESTINATION_TOPIC "finance/quickbooks/invoice/send/response" WITH METHOD "POST" WITH ENDPOINT "/v3/company/{value.json.realm_id}/invoice/{value.json.invoice_id}/send?sendTo={value.json.send_to}&minorversion=75" WITH HEADERS ADD "Accept" "application/json" ADD "Content-Type" "application/octet-stream" ADD "Authorization" "Bearer {value.json.access_token}" WITH BODY "" ADD EVENT ReadCustomer WITH SOURCE_TOPIC "finance/quickbooks/customer/get" WITH DESTINATION_TOPIC "finance/quickbooks/customer/get/response" WITH METHOD "GET" WITH ENDPOINT "/v3/company/{value.json.realm_id}/customer/{value.json.customer_id}?minorversion=75" WITH HEADERS ADD "Accept" "application/json" ADD "Authorization" "Bearer {value.json.access_token}" ADD EVENT CreateCustomer WITH SOURCE_TOPIC "finance/quickbooks/customer/create" WITH DESTINATION_TOPIC "finance/quickbooks/customer/create/response" WITH METHOD "POST" WITH ENDPOINT "/v3/company/{value.json.realm_id}/customer?minorversion=75" WITH HEADERS ADD "Accept" "application/json" ADD "Content-Type" "application/json" ADD "Authorization" "Bearer {value.json.access_token}" WITH BODY "{value.json.body}" ADD EVENT ChangeDataCapture WITH SOURCE_TOPIC "finance/quickbooks/cdc/get" WITH DESTINATION_TOPIC "finance/quickbooks/cdc/get/response" WITH METHOD "GET" WITH ENDPOINT "/v3/company/{value.json.realm_id}/cdc?entities={value.json.entities}&changedSince={value.json.changed_since}&minorversion=75" WITH HEADERS ADD "Accept" "application/json" ADD "Authorization" "Bearer {value.json.access_token}" ADD EVENT Batch WITH SOURCE_TOPIC "finance/quickbooks/batch" WITH DESTINATION_TOPIC "finance/quickbooks/batch/response" WITH METHOD "POST" WITH ENDPOINT "/v3/company/{value.json.realm_id}/batch?minorversion=75" WITH HEADERS ADD "Accept" "application/json" ADD "Content-Type" "application/json" ADD "Authorization" "Bearer {value.json.access_token}" WITH BODY "{value.json.body}" ADD EVENT ProfitAndLossReport WITH SOURCE_TOPIC "finance/quickbooks/reports/profitandloss/get" WITH DESTINATION_TOPIC "finance/quickbooks/reports/profitandloss/get/response" WITH METHOD "GET" WITH ENDPOINT "/v3/company/{value.json.realm_id}/reports/ProfitAndLoss?start_date={value.json.start_date}&end_date={value.json.end_date}&minorversion=75" WITH HEADERS ADD "Accept" "application/json" ADD "Authorization" "Bearer {value.json.access_token}" # OAuth 2.0 token refresh (different host). basic_auth = base64(":") from Hub secrets. # Reply: {"access_token":"...","expires_in":3600,"refresh_token":"...","x_refresh_token_expires_in":...,"token_type":"bearer"}. # Store the returned refresh_token every time (it rotates). Errors: 400 invalid_grant (refresh token expired/revoked), 401 invalid_client. DEFINE CONNECTOR QuickBooksOnlineOAuth WITH TYPE REST_API ADD REST_API_CONFIG WITH BASE_ADDRESS "https://oauth.platform.intuit.com" WITH ENABLE_CLIENT "true" WITH USE_SSL "true" WITH REQUEST_TIMEOUT '30' WITH DEFAULT_CONTENT_TYPE "application/x-www-form-urlencoded" ADD EVENT RefreshToken WITH SOURCE_TOPIC "finance/quickbooks/oauth/refresh" WITH DESTINATION_TOPIC "finance/quickbooks/oauth/refresh/response" WITH METHOD "POST" WITH ENDPOINT "/oauth2/v1/tokens/bearer" WITH HEADERS ADD "Accept" "application/json" ADD "Content-Type" "application/x-www-form-urlencoded" ADD "Authorization" "Basic {value.json.basic_auth}" WITH BODY "grant_type=refresh_token&refresh_token={value.json.refresh_token}" ``` ## MQTT examples Control fields (`access_token`, `realm_id`, ids, `query`, dates) only fill the URL and headers. Only `body` is sent as the request body. **RefreshToken** (`finance/quickbooks/oauth/refresh`). `basic_auth` is the Base64 of `client_id:client_secret`. Expect **200**: ```json theme={"theme":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} { "basic_auth": "", "refresh_token": "" } ``` Response: ```json theme={"theme":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} { "token_type": "bearer", "access_token": "eyJlbmMiOi...", "expires_in": 3600, "refresh_token": "AB11...", "x_refresh_token_expires_in": 8726400 } ``` **Query** (`finance/quickbooks/query`). The query is placed in the URL, so URL-encode it. This asks for inventory items: ```json theme={"theme":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} { "access_token": "", "realm_id": "9341455000000001", "query": "select%20*%20from%20Item%20where%20Type%20%3D%20%27Inventory%27%20MAXRESULTS%20100" } ``` Response (trimmed): ```json theme={"theme":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} { "QueryResponse": { "Item": [ { "Id": "11", "Name": "Pump Seal Kit", "Type": "Inventory", "TrackQtyOnHand": true, "QtyOnHand": 10, "SyncToken": "0" } ], "startPosition": 1, "maxResults": 1 } } ``` **CreateInventoryAdjustment** (`finance/quickbooks/inventoryadjustment/create`). Writes scrap back to stock: two units off item 11, posted to the adjustment account 91. Expect **200** with the new `InventoryAdjustment`: ```json theme={"theme":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} { "access_token": "", "realm_id": "9341455000000001", "body": { "AdjustAccountRef": { "value": "91" }, "TxnDate": "2026-10-05", "Line": [ { "DetailType": "ItemAdjustmentLineDetail", "ItemAdjustmentLineDetail": { "ItemRef": { "value": "11" }, "QtyDiff": -2 } } ] } } ``` Use a positive `QtyDiff` to add stock. Inventory adjustments need QuickBooks Online Plus or Advanced with quantity tracking turned on, and the item must be an `Inventory` item. **CreateInvoice** (`finance/quickbooks/invoice/create`), expect **200** with the new `Invoice` (`Id`, `DocNumber`, `TotalAmt`): ```json theme={"theme":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} { "access_token": "", "realm_id": "9341455000000001", "body": { "CustomerRef": { "value": "58" }, "Line": [ { "DetailType": "SalesItemLineDetail", "Amount": 150.0, "SalesItemLineDetail": { "ItemRef": { "value": "11" }, "Qty": 2, "UnitPrice": 75.0 } } ] } } ``` **SendInvoice** (`finance/quickbooks/invoice/send`) takes `invoice_id` and a URL-encoded `send_to` address, for example `"send_to": "ap%40customer.example"`. The body is empty. **Updates.** QuickBooks updates are sparse `POST`s to the create endpoint. Send the entity's `Id`, its current `SyncToken` and `"sparse": true` together with the fields to change. An old `SyncToken` returns **400** with code `5010`; read the entity again and retry. ## Keeping the token A small action can keep the latest tokens when the refresh reply arrives: ```lot theme={"theme":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} DEFINE ACTION QuickBooksKeepToken ON TOPIC "finance/quickbooks/oauth/refresh/response" DO SET "access" WITH GET JSON "access_token" IN PAYLOAD AS STRING SET "refresh" WITH GET JSON "refresh_token" IN PAYLOAD AS STRING KEEP TOPIC "finance/quickbooks/session/access_token" WITH {access} KEEP TOPIC "finance/quickbooks/session/refresh_token" WITH {refresh} ``` Refresh before the access token expires (for example every 50 minutes), or when a call returns **401** with code `3200`. Restrict who can read the `finance/quickbooks/#` topics with a rule, because these payloads carry tokens and client credentials. ## Webhooks (companion, not packaged) QuickBooks [webhooks](https://developer.intuit.com/app/developer/qbo/docs/develop/webhooks) can `POST` entity-change notifications (for example Invoice, Item or Payment created or updated) to an HTTPS URL. Each request carries an `intuit-signature` header, an HMAC-SHA256 of the body with your app's verifier token, which the receiver should check. Receiving them needs a separate `ENABLE_SERVER` REST API connector on a publicly reachable HTTPS address. That companion connector is not part of this template. Change data capture (`ChangeDataCapture`) is the polling alternative. ## Notes * Limits: 500 requests per minute and 10 concurrent requests per company and app; a batch holds at most 30 operations, with 40 batch calls per minute. Above that QuickBooks answers **429**. * A query returns at most 1000 rows. Page with `STARTPOSITION` and `MAXRESULTS`, for example `select * from Item STARTPOSITION 1001 MAXRESULTS 1000`. * Common errors: **401** code `3200` (expired or invalid token), **403** code `3100` (the app is not connected to that `realm_id`), **400** `ValidationFault` with codes such as `2500` (invalid reference), `6000` (business rule), `6240` (duplicate name), `5010` (stale `SyncToken`). * Sandbox and production use different hosts and different companies. A sandbox token does not work on `quickbooks.api.intuit.com`. * Not covered: invoice PDF download (binary), attachments (multipart upload), the Payments and Payroll APIs, and token revoke (`POST https://developer.api.intuit.com/v2/oauth2/tokens/revoke`). ## Related * [REST API Connectors](/latest/lot-language/routes/rest-api-routes) * [QuickBooks Online Accounting API reference](https://developer.intuit.com/app/developer/qbo/docs/api/accounting/all-entities/invoice) * [InventoryAdjustment entity](https://developer.intuit.com/app/developer/qbo/docs/api/accounting/all-entities/inventoryadjustment) * [OAuth 2.0 for QuickBooks Online](https://developer.intuit.com/app/developer/qbo/docs/develop/authentication-and-authorization/oauth-2.0) * [Minor versions](https://developer.intuit.com/app/developer/qbo/docs/learn/explore-the-quickbooks-online-api/minor-versions) * [QuickBooks Online Accounting API Postman collection](https://www.postman.com/intuit-developer/intuit-developer-quickbooks-online-accounting-api/overview) # Ren Data Hub Source: https://docs.coreflux.org/latest/lot-language/routes/rest-api-apps/ren-data-hub LoT connector for Portuguese grid open data with no API key: E-REDES consumption and production datasets and REN DataHub balances and market prices Technical Preview ## Overview Client-mode REST API connectors for Portuguese grid open data with no API key: E-REDES distribution datasets (15-minute national consumption and production, consumption forecast, consumption by municipality) and REN DataHub electricity and gas balances, production by source and market prices. This is one of the templates in the E-REDES and REN open data (Portugal) REST API app (`e-redes-and-ren-open-data`). Deploy it alongside the other connectors of that app when they depend on each other (for example a token connector plus an API connector). ## Auth * **schemes**: \["none"] * **secrets**: \[] * **note**: Both keyless. E-REDES optional portal API key (Authorization: Apikey ...) for higher quota; not wired by default. ## Template parameters | Placeholder | Type | Description | Required | | - | - | - | - | | `RouteName` | string | Name of the Ren Data Hub connector | no | | `BaseUrl` | string | Ren Data Hub base URL without trailing slash (e.g. [https://servicebus.ren.pt](https://servicebus.ren.pt)) | yes | `BASE_ADDRESS` default: `https://servicebus.ren.pt`. ## Events | Event | Method | Endpoint | Request topic | Response topic | Payload fields | | - | - | - | - | - | - | | `RenElectricityBalanceDaily` | GET | `/datahubapi/electricity/ElectricityConsumptionSupplyDaily?culture={value.json.culture}&date={value.json.date}` | `energy/pt/ren/electricity/balance-daily/get` | `energy/pt/ren/electricity/balance-daily/get/response` | `culture`, `date` | | `RenElectricityProductionDaily` | GET | `/datahubapi/electricity/ElectricityProductionBreakdownDaily?culture={value.json.culture}&date={value.json.date}` | `energy/pt/ren/electricity/production-daily/get` | `energy/pt/ren/electricity/production-daily/get/response` | `culture`, `date` | | `RenElectricityBalanceMonthly` | GET | `/datahubapi/electricity/ElectricityConsumptionSupplyMonthly?culture={value.json.culture}&year={value.json.year}&month={value.json.month}` | `energy/pt/ren/electricity/balance-monthly/get` | `energy/pt/ren/electricity/balance-monthly/get/response` | `culture`, `month`, `year` | | `RenElectricityInstalledPowerMonthly` | GET | `/datahubapi/electricity/ElectricityInstalledPowerMonthly?culture={value.json.culture}&year={value.json.year}&month={value.json.month}` | `energy/pt/ren/electricity/installed-power-monthly/get` | `energy/pt/ren/electricity/installed-power-monthly/get/response` | `culture`, `month`, `year` | | `RenElectricityConsumptionYearly` | GET | `/datahubapi/electricity/ElectricityConsumptionVariationYearly?culture={value.json.culture}&year={value.json.year}` | `energy/pt/ren/electricity/consumption-yearly/get` | `energy/pt/ren/electricity/consumption-yearly/get/response` | `culture`, `year` | | `RenElectricityPricesDaily` | GET | `/datahubapi/electricity/ElectricityMarketPricesDaily?culture={value.json.culture}&date={value.json.date}` | `energy/pt/ren/electricity/prices-daily/get` | `energy/pt/ren/electricity/prices-daily/get/response` | `culture`, `date` | | `RenElectricityPricesMonthly` | GET | `/datahubapi/electricity/ElectricityMarketPricesMonthly?culture={value.json.culture}&year={value.json.year}&month={value.json.month}` | `energy/pt/ren/electricity/prices-monthly/get` | `energy/pt/ren/electricity/prices-monthly/get/response` | `culture`, `month`, `year` | | `RenGasBalanceDaily` | GET | `/datahubapi/gas/GasConsumptionSupplyDaily?culture={value.json.culture}&date={value.json.date}` | `energy/pt/ren/gas/balance-daily/get` | `energy/pt/ren/gas/balance-daily/get/response` | `culture`, `date` | | `RenGasConsumptionBreakdownDaily` | GET | `/datahubapi/gas/GasConsumptionBreakdownDaily?culture={value.json.culture}&date={value.json.date}` | `energy/pt/ren/gas/consumption-breakdown-daily/get` | `energy/pt/ren/gas/consumption-breakdown-daily/get/response` | `culture`, `date` | | `RenGasRntgBalanceDaily` | GET | `/datahubapi/gas/GasRNTGBalanceDaily?culture={value.json.culture}&date={value.json.date}` | `energy/pt/ren/gas/rntg-balance-daily/get` | `energy/pt/ren/gas/rntg-balance-daily/get/response` | `culture`, `date` | | `RenGasLngTerminalDaily` | GET | `/datahubapi/gas/GasLNGTerminalBalanceDaily?culture={value.json.culture}&date={value.json.date}` | `energy/pt/ren/gas/lng-terminal-daily/get` | `energy/pt/ren/gas/lng-terminal-daily/get/response` | `culture`, `date` | | `RenGasStorageDaily` | GET | `/datahubapi/gas/GasUndergroundStorageBalanceDaily?culture={value.json.culture}&date={value.json.date}` | `energy/pt/ren/gas/storage-daily/get` | `energy/pt/ren/gas/storage-daily/get/response` | `culture`, `date` | | `RenGasBalanceMonthly` | GET | `/datahubapi/gas/GasConsumptionSupplyMonthly?culture={value.json.culture}&year={value.json.year}&month={value.json.month}` | `energy/pt/ren/gas/balance-monthly/get` | `energy/pt/ren/gas/balance-monthly/get/response` | `culture`, `month`, `year` | | `RenGasRntgBalanceMonthly` | GET | `/datahubapi/gas/GasRNTGBalanceMonthly?culture={value.json.culture}&year={value.json.year}&month={value.json.month}` | `energy/pt/ren/gas/rntg-balance-monthly/get` | `energy/pt/ren/gas/rntg-balance-monthly/get/response` | `culture`, `month`, `year` | | `RenGasLngTerminalMonthly` | GET | `/datahubapi/gas/GasLNGTerminalBalanceMonthly?culture={value.json.culture}&year={value.json.year}&month={value.json.month}` | `energy/pt/ren/gas/lng-terminal-monthly/get` | `energy/pt/ren/gas/lng-terminal-monthly/get/response` | `culture`, `month`, `year` | | `RenGasStorageMonthly` | GET | `/datahubapi/gas/GasUndergroundStorageBalanceMonthly?culture={value.json.culture}&year={value.json.year}&month={value.json.month}` | `energy/pt/ren/gas/storage-monthly/get` | `energy/pt/ren/gas/storage-monthly/get/response` | `culture`, `month`, `year` | | `RenGasSupplyBreakdownMonthly` | GET | `/datahubapi/gas/GasBreakdownOfSupplyMonthly?culture={value.json.culture}&year={value.json.year}&month={value.json.month}` | `energy/pt/ren/gas/supply-breakdown-monthly/get` | `energy/pt/ren/gas/supply-breakdown-monthly/get/response` | `culture`, `month`, `year` | | `RenGasConsumptionYearly` | GET | `/datahubapi/gas/GasConsumptionVariationYearly?culture={value.json.culture}&year={value.json.year}` | `energy/pt/ren/gas/consumption-yearly/get` | `energy/pt/ren/gas/consumption-yearly/get/response` | `culture`, `year` | | `RenGasPricesDaily` | GET | `/datahubapi/gas/GasMarketPricesDaily?culture={value.json.culture}&year={value.json.year}&month={value.json.month}` | `energy/pt/ren/gas/prices-daily/get` | `energy/pt/ren/gas/prices-daily/get/response` | `culture`, `month`, `year` | | `RenGasPricesMonthly` | GET | `/datahubapi/gas/GasMarketPricesMonthly?culture={value.json.culture}&year={value.json.year}&month={value.json.month}` | `energy/pt/ren/gas/prices-monthly/get` | `energy/pt/ren/gas/prices-monthly/get/response` | `culture`, `month`, `year` | ## Deploy Get the template card (JSON with `template` and `metadata`) from the broker, then deploy the connector: ```bash theme={"theme":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} mosquitto_pub -t '$SYS/Coreflux/Command' -m "-connectorCode REN_DATA_HUB" ``` The deployable connector from the REST API app, with `BASE_ADDRESS` already filled in: ```lot theme={"theme":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} DEFINE CONNECTOR RenDataHub WITH TYPE REST_API ADD REST_API_CONFIG WITH BASE_ADDRESS "https://servicebus.ren.pt" WITH ENABLE_CLIENT "true" WITH USE_SSL "true" WITH REQUEST_TIMEOUT '30' WITH DEFAULT_CONTENT_TYPE "application/json" ADD EVENT RenElectricityBalanceDaily WITH SOURCE_TOPIC "energy/pt/ren/electricity/balance-daily/get" WITH DESTINATION_TOPIC "energy/pt/ren/electricity/balance-daily/get/response" WITH METHOD "GET" WITH ENDPOINT "/datahubapi/electricity/ElectricityConsumptionSupplyDaily?culture={value.json.culture}&date={value.json.date}" WITH HEADERS ADD "Accept" "*/*" ADD "User-Agent" "Coreflux-Broker REST_API route (https://www.coreflux.org)" ADD EVENT RenElectricityProductionDaily WITH SOURCE_TOPIC "energy/pt/ren/electricity/production-daily/get" WITH DESTINATION_TOPIC "energy/pt/ren/electricity/production-daily/get/response" WITH METHOD "GET" WITH ENDPOINT "/datahubapi/electricity/ElectricityProductionBreakdownDaily?culture={value.json.culture}&date={value.json.date}" WITH HEADERS ADD "Accept" "*/*" ADD "User-Agent" "Coreflux-Broker REST_API route (https://www.coreflux.org)" ADD EVENT RenElectricityBalanceMonthly WITH SOURCE_TOPIC "energy/pt/ren/electricity/balance-monthly/get" WITH DESTINATION_TOPIC "energy/pt/ren/electricity/balance-monthly/get/response" WITH METHOD "GET" WITH ENDPOINT "/datahubapi/electricity/ElectricityConsumptionSupplyMonthly?culture={value.json.culture}&year={value.json.year}&month={value.json.month}" WITH HEADERS ADD "Accept" "*/*" ADD "User-Agent" "Coreflux-Broker REST_API route (https://www.coreflux.org)" ADD EVENT RenElectricityInstalledPowerMonthly WITH SOURCE_TOPIC "energy/pt/ren/electricity/installed-power-monthly/get" WITH DESTINATION_TOPIC "energy/pt/ren/electricity/installed-power-monthly/get/response" WITH METHOD "GET" WITH ENDPOINT "/datahubapi/electricity/ElectricityInstalledPowerMonthly?culture={value.json.culture}&year={value.json.year}&month={value.json.month}" WITH HEADERS ADD "Accept" "*/*" ADD "User-Agent" "Coreflux-Broker REST_API route (https://www.coreflux.org)" ADD EVENT RenElectricityConsumptionYearly WITH SOURCE_TOPIC "energy/pt/ren/electricity/consumption-yearly/get" WITH DESTINATION_TOPIC "energy/pt/ren/electricity/consumption-yearly/get/response" WITH METHOD "GET" WITH ENDPOINT "/datahubapi/electricity/ElectricityConsumptionVariationYearly?culture={value.json.culture}&year={value.json.year}" WITH HEADERS ADD "Accept" "*/*" ADD "User-Agent" "Coreflux-Broker REST_API route (https://www.coreflux.org)" ADD EVENT RenElectricityPricesDaily WITH SOURCE_TOPIC "energy/pt/ren/electricity/prices-daily/get" WITH DESTINATION_TOPIC "energy/pt/ren/electricity/prices-daily/get/response" WITH METHOD "GET" WITH ENDPOINT "/datahubapi/electricity/ElectricityMarketPricesDaily?culture={value.json.culture}&date={value.json.date}" WITH HEADERS ADD "Accept" "*/*" ADD "User-Agent" "Coreflux-Broker REST_API route (https://www.coreflux.org)" ADD EVENT RenElectricityPricesMonthly WITH SOURCE_TOPIC "energy/pt/ren/electricity/prices-monthly/get" WITH DESTINATION_TOPIC "energy/pt/ren/electricity/prices-monthly/get/response" WITH METHOD "GET" WITH ENDPOINT "/datahubapi/electricity/ElectricityMarketPricesMonthly?culture={value.json.culture}&year={value.json.year}&month={value.json.month}" WITH HEADERS ADD "Accept" "*/*" ADD "User-Agent" "Coreflux-Broker REST_API route (https://www.coreflux.org)" ADD EVENT RenGasBalanceDaily WITH SOURCE_TOPIC "energy/pt/ren/gas/balance-daily/get" WITH DESTINATION_TOPIC "energy/pt/ren/gas/balance-daily/get/response" WITH METHOD "GET" WITH ENDPOINT "/datahubapi/gas/GasConsumptionSupplyDaily?culture={value.json.culture}&date={value.json.date}" WITH HEADERS ADD "Accept" "*/*" ADD "User-Agent" "Coreflux-Broker REST_API route (https://www.coreflux.org)" ADD EVENT RenGasConsumptionBreakdownDaily WITH SOURCE_TOPIC "energy/pt/ren/gas/consumption-breakdown-daily/get" WITH DESTINATION_TOPIC "energy/pt/ren/gas/consumption-breakdown-daily/get/response" WITH METHOD "GET" WITH ENDPOINT "/datahubapi/gas/GasConsumptionBreakdownDaily?culture={value.json.culture}&date={value.json.date}" WITH HEADERS ADD "Accept" "*/*" ADD "User-Agent" "Coreflux-Broker REST_API route (https://www.coreflux.org)" ADD EVENT RenGasRntgBalanceDaily WITH SOURCE_TOPIC "energy/pt/ren/gas/rntg-balance-daily/get" WITH DESTINATION_TOPIC "energy/pt/ren/gas/rntg-balance-daily/get/response" WITH METHOD "GET" WITH ENDPOINT "/datahubapi/gas/GasRNTGBalanceDaily?culture={value.json.culture}&date={value.json.date}" WITH HEADERS ADD "Accept" "*/*" ADD "User-Agent" "Coreflux-Broker REST_API route (https://www.coreflux.org)" ADD EVENT RenGasLngTerminalDaily WITH SOURCE_TOPIC "energy/pt/ren/gas/lng-terminal-daily/get" WITH DESTINATION_TOPIC "energy/pt/ren/gas/lng-terminal-daily/get/response" WITH METHOD "GET" WITH ENDPOINT "/datahubapi/gas/GasLNGTerminalBalanceDaily?culture={value.json.culture}&date={value.json.date}" WITH HEADERS ADD "Accept" "*/*" ADD "User-Agent" "Coreflux-Broker REST_API route (https://www.coreflux.org)" ADD EVENT RenGasStorageDaily WITH SOURCE_TOPIC "energy/pt/ren/gas/storage-daily/get" WITH DESTINATION_TOPIC "energy/pt/ren/gas/storage-daily/get/response" WITH METHOD "GET" WITH ENDPOINT "/datahubapi/gas/GasUndergroundStorageBalanceDaily?culture={value.json.culture}&date={value.json.date}" WITH HEADERS ADD "Accept" "*/*" ADD "User-Agent" "Coreflux-Broker REST_API route (https://www.coreflux.org)" ADD EVENT RenGasBalanceMonthly WITH SOURCE_TOPIC "energy/pt/ren/gas/balance-monthly/get" WITH DESTINATION_TOPIC "energy/pt/ren/gas/balance-monthly/get/response" WITH METHOD "GET" WITH ENDPOINT "/datahubapi/gas/GasConsumptionSupplyMonthly?culture={value.json.culture}&year={value.json.year}&month={value.json.month}" WITH HEADERS ADD "Accept" "*/*" ADD "User-Agent" "Coreflux-Broker REST_API route (https://www.coreflux.org)" ADD EVENT RenGasRntgBalanceMonthly WITH SOURCE_TOPIC "energy/pt/ren/gas/rntg-balance-monthly/get" WITH DESTINATION_TOPIC "energy/pt/ren/gas/rntg-balance-monthly/get/response" WITH METHOD "GET" WITH ENDPOINT "/datahubapi/gas/GasRNTGBalanceMonthly?culture={value.json.culture}&year={value.json.year}&month={value.json.month}" WITH HEADERS ADD "Accept" "*/*" ADD "User-Agent" "Coreflux-Broker REST_API route (https://www.coreflux.org)" ADD EVENT RenGasLngTerminalMonthly WITH SOURCE_TOPIC "energy/pt/ren/gas/lng-terminal-monthly/get" WITH DESTINATION_TOPIC "energy/pt/ren/gas/lng-terminal-monthly/get/response" WITH METHOD "GET" WITH ENDPOINT "/datahubapi/gas/GasLNGTerminalBalanceMonthly?culture={value.json.culture}&year={value.json.year}&month={value.json.month}" WITH HEADERS ADD "Accept" "*/*" ADD "User-Agent" "Coreflux-Broker REST_API route (https://www.coreflux.org)" ADD EVENT RenGasStorageMonthly WITH SOURCE_TOPIC "energy/pt/ren/gas/storage-monthly/get" WITH DESTINATION_TOPIC "energy/pt/ren/gas/storage-monthly/get/response" WITH METHOD "GET" WITH ENDPOINT "/datahubapi/gas/GasUndergroundStorageBalanceMonthly?culture={value.json.culture}&year={value.json.year}&month={value.json.month}" WITH HEADERS ADD "Accept" "*/*" ADD "User-Agent" "Coreflux-Broker REST_API route (https://www.coreflux.org)" ADD EVENT RenGasSupplyBreakdownMonthly WITH SOURCE_TOPIC "energy/pt/ren/gas/supply-breakdown-monthly/get" WITH DESTINATION_TOPIC "energy/pt/ren/gas/supply-breakdown-monthly/get/response" WITH METHOD "GET" WITH ENDPOINT "/datahubapi/gas/GasBreakdownOfSupplyMonthly?culture={value.json.culture}&year={value.json.year}&month={value.json.month}" WITH HEADERS ADD "Accept" "*/*" ADD "User-Agent" "Coreflux-Broker REST_API route (https://www.coreflux.org)" ADD EVENT RenGasConsumptionYearly WITH SOURCE_TOPIC "energy/pt/ren/gas/consumption-yearly/get" WITH DESTINATION_TOPIC "energy/pt/ren/gas/consumption-yearly/get/response" WITH METHOD "GET" WITH ENDPOINT "/datahubapi/gas/GasConsumptionVariationYearly?culture={value.json.culture}&year={value.json.year}" WITH HEADERS ADD "Accept" "*/*" ADD "User-Agent" "Coreflux-Broker REST_API route (https://www.coreflux.org)" ADD EVENT RenGasPricesDaily WITH SOURCE_TOPIC "energy/pt/ren/gas/prices-daily/get" WITH DESTINATION_TOPIC "energy/pt/ren/gas/prices-daily/get/response" WITH METHOD "GET" WITH ENDPOINT "/datahubapi/gas/GasMarketPricesDaily?culture={value.json.culture}&year={value.json.year}&month={value.json.month}" WITH HEADERS ADD "Accept" "*/*" ADD "User-Agent" "Coreflux-Broker REST_API route (https://www.coreflux.org)" ADD EVENT RenGasPricesMonthly WITH SOURCE_TOPIC "energy/pt/ren/gas/prices-monthly/get" WITH DESTINATION_TOPIC "energy/pt/ren/gas/prices-monthly/get/response" WITH METHOD "GET" WITH ENDPOINT "/datahubapi/gas/GasMarketPricesMonthly?culture={value.json.culture}&year={value.json.year}&month={value.json.month}" WITH HEADERS ADD "Accept" "*/*" ADD "User-Agent" "Coreflux-Broker REST_API route (https://www.coreflux.org)" ``` Deploy it with `-addConnector` (do not include `ADD METADATA`). ## Usage example Call `RenElectricityBalanceDaily` by publishing a JSON payload to its request topic, and read the reply on the response topic: ```bash theme={"theme":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} mosquitto_sub -t 'energy/pt/ren/electricity/balance-daily/get/response' & mosquitto_pub -t 'energy/pt/ren/electricity/balance-daily/get' -m '{"culture": "", "date": ""}' ``` ## Related * [REST API Connectors](/latest/lot-language/routes/rest-api-routes) * [REST API Apps catalog](/latest/lot-language/routes/rest-api-apps) # Rhize Api Source: https://docs.coreflux.org/latest/lot-language/routes/rest-api-apps/rhize-api LoT connector for the Rhize ISA-95 data hub GraphQL API: record job responses and equipment actuals, query material lots and KPIs, and start BPMN workflows Technical Preview ## Overview Client-mode REST API connector for Rhize, the ISA-95 manufacturing data hub: record job responses and equipment actuals from production topics, add and update equipment, query equipment, job responses, material lots, data sources, KPIs and the audit log, and start BPMN workflows, through the Rhize GraphQL API with Keycloak tokens. This is one of the templates in the Rhize (ISA-95 manufacturing data hub) REST API app (`rhize`). Deploy it alongside the other connectors of that app when they depend on each other (for example a token connector plus an API connector). ## Auth Keycloak OpenID Connect. RhizeAuth GetAccessToken (client\_credentials) or GetPasswordToken (password grant) returns a JWT (docs example expires\_in 28800); publish authorization 'Bearer \' on every RhizeApi event. Hub secrets RHIZE\_REALM, RHIZE\_CLIENT\_ID, RHIZE\_CLIENT\_SECRET (+ RHIZE\_USERNAME / RHIZE\_PASSWORD for the password grant). Auth headers used by the template: `Authorization: {value.json.authorization}`. ## Template parameters | Placeholder | Type | Description | Required | | - | - | - | - | | `RouteName` | string | Name of the Rhize Api connector | no | | `BaseUrl` | string | Rhize Api base URL without trailing slash (e.g. https\://YOUR\_RHIZE\_HOST) | yes | `BASE_ADDRESS` default: `https://YOUR_RHIZE_HOST`. ## Events | Event | Method | Endpoint | Request topic | Response topic | Payload fields | | - | - | - | - | - | - | | `QueryEquipment` | POST | `/graphql` | `historian/rhize/equipment/query` | `historian/rhize/equipment/query/response` | `authorization`, `request` | | `GetEquipment` | POST | `/graphql` | `historian/rhize/equipment/get` | `historian/rhize/equipment/get/response` | `authorization`, `request` | | `AggregateEquipment` | POST | `/graphql` | `historian/rhize/equipment/aggregate` | `historian/rhize/equipment/aggregate/response` | `authorization`, `request` | | `AddEquipment` | POST | `/graphql` | `historian/rhize/equipment/add` | `historian/rhize/equipment/add/response` | `authorization`, `request` | | `UpdateEquipmentVersion` | POST | `/graphql` | `historian/rhize/equipment-version/update` | `historian/rhize/equipment-version/update/response` | `authorization`, `request` | | `QueryJobResponse` | POST | `/graphql` | `historian/rhize/job-response/query` | `historian/rhize/job-response/query/response` | `authorization`, `request` | | `AddJobResponse` | POST | `/graphql` | `historian/rhize/job-response/add` | `historian/rhize/job-response/add/response` | `authorization`, `request` | | `AddEquipmentActual` | POST | `/graphql` | `historian/rhize/equipment-actual/add` | `historian/rhize/equipment-actual/add/response` | `authorization`, `request` | | `QueryMaterialLot` | POST | `/graphql` | `historian/rhize/material-lot/query` | `historian/rhize/material-lot/query/response` | `authorization`, `request` | | `QueryDataSource` | POST | `/graphql` | `historian/rhize/data-source/query` | `historian/rhize/data-source/query/response` | `authorization`, `request` | | `QueryKpi` | POST | `/graphql` | `historian/rhize/kpi/query` | `historian/rhize/kpi/query/response` | `authorization`, `request` | | `QueryAuditLog` | POST | `/graphql` | `historian/rhize/audit-log/query` | `historian/rhize/audit-log/query/response` | `authorization`, `request` | | `StartWorkflow` | POST | `/graphql` | `historian/rhize/workflow/start` | `historian/rhize/workflow/start/response` | `authorization`, `request` | | `RunWorkflowSync` | POST | `/graphql` | `historian/rhize/workflow/run` | `historian/rhize/workflow/run/response` | `authorization`, `request` | ## Deploy Get the template card (JSON with `template` and `metadata`) from the broker, then deploy the connector: ```bash theme={"theme":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} mosquitto_pub -t '$SYS/Coreflux/Command' -m "-connectorCode RHIZE" ``` The deployable connector from the REST API app, with `BASE_ADDRESS` already filled in: ```lot theme={"theme":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} DEFINE CONNECTOR RhizeApi WITH TYPE REST_API ADD REST_API_CONFIG WITH BASE_ADDRESS "https://YOUR_RHIZE_HOST" WITH ENABLE_CLIENT "true" WITH USE_SSL "true" WITH REQUEST_TIMEOUT '30' WITH DEFAULT_CONTENT_TYPE "application/json" ADD EVENT QueryEquipment WITH SOURCE_TOPIC "historian/rhize/equipment/query" WITH DESTINATION_TOPIC "historian/rhize/equipment/query/response" WITH METHOD "POST" WITH ENDPOINT "/graphql" WITH HEADERS ADD "Accept" "application/json" ADD "Authorization" "{value.json.authorization}" ADD "Content-Type" "application/json" WITH BODY "{value.json.request}" ADD EVENT GetEquipment WITH SOURCE_TOPIC "historian/rhize/equipment/get" WITH DESTINATION_TOPIC "historian/rhize/equipment/get/response" WITH METHOD "POST" WITH ENDPOINT "/graphql" WITH HEADERS ADD "Accept" "application/json" ADD "Authorization" "{value.json.authorization}" ADD "Content-Type" "application/json" WITH BODY "{value.json.request}" ADD EVENT AggregateEquipment WITH SOURCE_TOPIC "historian/rhize/equipment/aggregate" WITH DESTINATION_TOPIC "historian/rhize/equipment/aggregate/response" WITH METHOD "POST" WITH ENDPOINT "/graphql" WITH HEADERS ADD "Accept" "application/json" ADD "Authorization" "{value.json.authorization}" ADD "Content-Type" "application/json" WITH BODY "{value.json.request}" ADD EVENT AddEquipment WITH SOURCE_TOPIC "historian/rhize/equipment/add" WITH DESTINATION_TOPIC "historian/rhize/equipment/add/response" WITH METHOD "POST" WITH ENDPOINT "/graphql" WITH HEADERS ADD "Accept" "application/json" ADD "Authorization" "{value.json.authorization}" ADD "Content-Type" "application/json" WITH BODY "{value.json.request}" ADD EVENT UpdateEquipmentVersion WITH SOURCE_TOPIC "historian/rhize/equipment-version/update" WITH DESTINATION_TOPIC "historian/rhize/equipment-version/update/response" WITH METHOD "POST" WITH ENDPOINT "/graphql" WITH HEADERS ADD "Accept" "application/json" ADD "Authorization" "{value.json.authorization}" ADD "Content-Type" "application/json" WITH BODY "{value.json.request}" ADD EVENT QueryJobResponse WITH SOURCE_TOPIC "historian/rhize/job-response/query" WITH DESTINATION_TOPIC "historian/rhize/job-response/query/response" WITH METHOD "POST" WITH ENDPOINT "/graphql" WITH HEADERS ADD "Accept" "application/json" ADD "Authorization" "{value.json.authorization}" ADD "Content-Type" "application/json" WITH BODY "{value.json.request}" ADD EVENT AddJobResponse WITH SOURCE_TOPIC "historian/rhize/job-response/add" WITH DESTINATION_TOPIC "historian/rhize/job-response/add/response" WITH METHOD "POST" WITH ENDPOINT "/graphql" WITH HEADERS ADD "Accept" "application/json" ADD "Authorization" "{value.json.authorization}" ADD "Content-Type" "application/json" WITH BODY "{value.json.request}" ADD EVENT AddEquipmentActual WITH SOURCE_TOPIC "historian/rhize/equipment-actual/add" WITH DESTINATION_TOPIC "historian/rhize/equipment-actual/add/response" WITH METHOD "POST" WITH ENDPOINT "/graphql" WITH HEADERS ADD "Accept" "application/json" ADD "Authorization" "{value.json.authorization}" ADD "Content-Type" "application/json" WITH BODY "{value.json.request}" ADD EVENT QueryMaterialLot WITH SOURCE_TOPIC "historian/rhize/material-lot/query" WITH DESTINATION_TOPIC "historian/rhize/material-lot/query/response" WITH METHOD "POST" WITH ENDPOINT "/graphql" WITH HEADERS ADD "Accept" "application/json" ADD "Authorization" "{value.json.authorization}" ADD "Content-Type" "application/json" WITH BODY "{value.json.request}" ADD EVENT QueryDataSource WITH SOURCE_TOPIC "historian/rhize/data-source/query" WITH DESTINATION_TOPIC "historian/rhize/data-source/query/response" WITH METHOD "POST" WITH ENDPOINT "/graphql" WITH HEADERS ADD "Accept" "application/json" ADD "Authorization" "{value.json.authorization}" ADD "Content-Type" "application/json" WITH BODY "{value.json.request}" ADD EVENT QueryKpi WITH SOURCE_TOPIC "historian/rhize/kpi/query" WITH DESTINATION_TOPIC "historian/rhize/kpi/query/response" WITH METHOD "POST" WITH ENDPOINT "/graphql" WITH HEADERS ADD "Accept" "application/json" ADD "Authorization" "{value.json.authorization}" ADD "Content-Type" "application/json" WITH BODY "{value.json.request}" ADD EVENT QueryAuditLog WITH SOURCE_TOPIC "historian/rhize/audit-log/query" WITH DESTINATION_TOPIC "historian/rhize/audit-log/query/response" WITH METHOD "POST" WITH ENDPOINT "/graphql" WITH HEADERS ADD "Accept" "application/json" ADD "Authorization" "{value.json.authorization}" ADD "Content-Type" "application/json" WITH BODY "{value.json.request}" ADD EVENT StartWorkflow WITH SOURCE_TOPIC "historian/rhize/workflow/start" WITH DESTINATION_TOPIC "historian/rhize/workflow/start/response" WITH METHOD "POST" WITH ENDPOINT "/graphql" WITH HEADERS ADD "Accept" "application/json" ADD "Authorization" "{value.json.authorization}" ADD "Content-Type" "application/json" WITH BODY "{value.json.request}" ADD EVENT RunWorkflowSync WITH SOURCE_TOPIC "historian/rhize/workflow/run" WITH DESTINATION_TOPIC "historian/rhize/workflow/run/response" WITH METHOD "POST" WITH ENDPOINT "/graphql" WITH HEADERS ADD "Accept" "application/json" ADD "Authorization" "{value.json.authorization}" ADD "Content-Type" "application/json" WITH BODY "{value.json.request}" ``` Deploy it with `-addConnector` (do not include `ADD METADATA`). ## Usage example Call `QueryEquipment` by publishing a JSON payload to its request topic, and read the reply on the response topic: ```bash theme={"theme":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} mosquitto_sub -t 'historian/rhize/equipment/query/response' & mosquitto_pub -t 'historian/rhize/equipment/query' -m '{"authorization": "", "request": ""}' ``` ## Related * [REST API Connectors](/latest/lot-language/routes/rest-api-routes) * [REST API Apps catalog](/latest/lot-language/routes/rest-api-apps) # Rockwell Plex Smart Manufacturing Platform Source: https://docs.coreflux.org/latest/lot-language/routes/rest-api-apps/rockwell-plex-smart-manufacturing-platform LoT connector for Rockwell Plex: read workcenters, downtime, production, jobs and parts, and post workcenter status, production, checksheets and scrap Technical Preview ## Overview One client-mode connector, **RockwellPlex**, for the Plex Connect REST APIs of the Rockwell Automation Plex Smart Manufacturing Platform (Plex MES, ERP and quality). It covers what a broker typically exchanges with Plex on a discrete production line. Read workcenter definitions and their live setup, the status and downtime history, production and scrap records, batches, scheduled jobs, parts, inventory containers, equipment and quality control plans into your Unified Namespace. In the other direction, a PLC state change on the broker can set a workcenter's status in Plex (downtime), set up the next job, record batch production, add a checksheet with measurements, or scrap a container. | Event | Request | Notes | | - | - | - | | ListWorkcenters | `GET /production/v1/production-definitions/workcenters?{query}` | Filter by `workcenterCode`, `name`, `workcenterGroup`, `buildingId`, `plcName`, `ipAddress` | | GetWorkcenter | `GET /production/v1/production-definitions/workcenters/{workcenter_id}` | One workcenter definition | | GetWorkcenterSetup | `GET /production/v1/control/workcenters/{workcenter_id}` | Live setup: current job, part, status | | ListWorkcenterStatuses | `GET /production/v1/production-definitions/workcenter-statuses?{query}` | Status ids for `SetWorkcenterStatus` | | ListWorkcenterEvents | `GET /production/v1/production-definitions/workcenter-events?{query}` | Event (reason) ids | | ListWorkcenterStatusEntries | `GET /production/v1/production-history/workcenter-status-entries?{query}` | Status and downtime history; `beginDate`, `endDate` required | | ListProductionEntries | `GET /production/v1/production-history/production-entries?{query}` | Production records; `beginDate`, `endDate` required | | ListProductionEntriesSummary | `GET /production/v1/production-history/production-entries-summary?{query}` | Summarised production | | ListScrapEntries | `GET /production/v1/production-history/scrap-entries?{query}` | Scrap records; `beginDate`, `endDate` required | | ListBatches | `GET /production/v1/control/workcenters/{workcenter_id}/batches?{query}` | Batches on a workcenter | | GetNextBatch | `GET /production/v1/control/workcenters/{workcenter_id}/next-batch?{query}` | Next batch to produce | | ListJobs | `GET /scheduling/v1/jobs?{query}` | Filter by `jobStatuses`, `partId`, `buildingId`, due dates | | GetJob | `GET /scheduling/v1/jobs/{job_id}` | One job | | ListJobOperations | `GET /scheduling/v1/jobs/{job_id}/operations?{query}` | Operations of a job; ids for `SetupJob` | | ListParts | `GET /mdm/v2/parts?{query}` | Part master data | | ListContainers | `GET /inventory/v1/inventory-tracking/containers?{query}` | Inventory containers | | ListEquipment | `GET /maintenance/v1/equipment?{query}` | Equipment records | | ListControlPlans | `GET /quality/v1/control-plans?{query}` | Quality control plans | | GetChecksheet | `GET /quality/v1/checksheets/{checksheet_id}` | One checksheet | | SetWorkcenterStatus | `POST /production/v1/control/workcenters/{workcenter_id}/status` | Downtime or running status | | SetupJob | `POST /production/v1/control/workcenters/{workcenter_id}/setup-job` | Set up a job operation on a workcenter | | RecordBatchProduction | `POST /production/v1/control/workcenters/{workcenter_id}/record-batch-production` | Record produced quantity for a batch | | CreateChecksheet | `POST /quality/v1/checksheets` | Add a checksheet with measurements (`201`) | | ScrapContainer | `POST /inventory/v1/inventory-control/containers/{serial_no}/scrap` | Scrap quantity from a container (`204`) | Topics are `mes/rockwell-plex-smart-manufacturing-platform//`; every reply goes to `/response`. ## Authentication Every event sends `X-Plex-Connect-Api-Key: {api_key}`. The key is the Consumer Key of an app you create under **My Apps** in the [Plex Developer Portal](https://developers.plex.com/), subscribed to the API products this connector uses: Production Operations Management, Job Scheduling, Parts, Inventory Operations Management, Equipment, Control Plans and Checksheet. Access to the portal requires Plex IAM, which your Plex Champion arranges through a support case. Keep the key in a broker secret (suggested `PLEX_CONNECT_API_KEY`) and map it in with `GET SECRET`. Requests run against the key's default tenant (PCN). To work with another PCN, add `ADD "X-Plex-Connect-Tenant-Id" "{value.json.tenant_id}"` under `WITH HEADERS` and send `tenant_id` in the payload. Plex has a test tier at `https://test.connect.plex.com`, and test and production keys must be different. A missing or wrong key, including a production key used on the test tier, returns `401` with `REQUEST_NOT_AUTHENTICATED`. An app that is not subscribed to an API product gets `403`. ## Basic Syntax ```lot theme={"theme":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} DEFINE CONNECTOR RockwellPlex WITH TYPE REST_API ADD REST_API_CONFIG WITH BASE_ADDRESS "https://connect.plex.com" WITH ENABLE_CLIENT "true" WITH USE_SSL "true" WITH REQUEST_TIMEOUT '60' WITH DEFAULT_CONTENT_TYPE "application/json" ADD EVENT ListProductionEntries WITH SOURCE_TOPIC "mes/rockwell-plex-smart-manufacturing-platform/production-entry/list" WITH DESTINATION_TOPIC "mes/rockwell-plex-smart-manufacturing-platform/production-entry/list/response" WITH METHOD "GET" WITH ENDPOINT "/production/v1/production-history/production-entries?{value.json.query}" WITH HEADERS ADD "Accept" "application/json" ADD "X-Plex-Connect-Api-Key" "{value.json.api_key}" ADD EVENT SetWorkcenterStatus WITH SOURCE_TOPIC "mes/rockwell-plex-smart-manufacturing-platform/workcenter-status/set" WITH DESTINATION_TOPIC "mes/rockwell-plex-smart-manufacturing-platform/workcenter-status/set/response" WITH METHOD "POST" WITH ENDPOINT "/production/v1/control/workcenters/{value.json.workcenter_id}/status" WITH HEADERS ADD "Accept" "application/json" ADD "Content-Type" "application/json" ADD "X-Plex-Connect-Api-Key" "{value.json.api_key}" WITH BODY "{value.json.body}" ``` Use `https://test.connect.plex.com` as `BASE_ADDRESS` while you develop. The full template, with all 24 events, is `RockwellPlexSmartManufacturingPlatformRestApi.lot` in the REST API app. ## MQTT examples Request bodies go under `body`, so control fields like `api_key`, `workcenter_id` and `query` are not sent to Plex. Ids are Plex GUIDs. **Find a workcenter by code**: publish to `mes/rockwell-plex-smart-manufacturing-platform/workcenter/list`: ```json theme={"theme":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} { "api_key": "", "query": "workcenterCode=PRESS-01" } ``` **Read this shift's production**: publish to `mes/rockwell-plex-smart-manufacturing-platform/production-entry/list`. Plex requires `beginDate` and `endDate`, and the query must be URL-encoded: ```json theme={"theme":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} { "api_key": "", "query": "beginDate=2026-10-05T06:00:00Z&endDate=2026-10-05T14:00:00Z&workcenterId=" } ``` Send `"query": ""` on list events when you do not need filters. **Put a workcenter into downtime**: look up the status id with `workcenter-status/list`, then publish to `mes/rockwell-plex-smart-manufacturing-platform/workcenter-status/set`: ```json theme={"theme":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} { "api_key": "", "workcenter_id": "", "body": { "workcenterStatusId": "", "workcenterEventId": "" } } ``` **Record batch production**: publish to `mes/rockwell-plex-smart-manufacturing-platform/batch-production/record` with `"body": { "batchId": "", "quantity": 12 }`. Get the batch id from `batch/next` or `batch/list`. **Scrap from a container**: publish to `mes/rockwell-plex-smart-manufacturing-platform/container/scrap` with `"serial_no": ""` and `"body": { "scrapReason": "", "quantity": 1 }`. Plex answers `204` with an empty body. ## Helper actions Keep the key in a broker secret, read a workcenter's live setup every minute, and turn a PLC status change into a Plex workcenter status: ```lot theme={"theme":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} DEFINE ACTION PlexPollWorkcenterStatus ON EVERY 1 MINUTE DO SET "key" WITH GET SECRET "PLEX_CONNECT_API_KEY" PUBLISH TOPIC "mes/rockwell-plex-smart-manufacturing-platform/workcenter-setup/get" WITH { "api_key": {key}, "workcenter_id": "" } DEFINE ACTION PlexDowntimeFromLine ON TOPIC "porto/line1/press01/plex-status" DO SET "key" WITH GET SECRET "PLEX_CONNECT_API_KEY" SET "status" WITH GET JSON "workcenterStatusId" IN PAYLOAD AS STRING PUBLISH TOPIC "mes/rockwell-plex-smart-manufacturing-platform/workcenter-status/set" WITH { "api_key": {key}, "workcenter_id": "", "body": { "workcenterStatusId": {status} } } ``` Restrict who can publish to `mes/rockwell-plex-smart-manufacturing-platform/#` with a rule: its payloads carry the API key, and the POST events change production records. ## Notes * Errors come back on the response topic: `401 REQUEST_NOT_AUTHENTICATED` for a missing or wrong key, `403` when the app is not subscribed to that API product, `404` for an unknown id, `400` with the failing fields for a validation error, and `429` when you hit Plex rate limits. * Plex history endpoints need a date range. Keep ranges short (one shift) and page by time to stay inside rate limits. * Status and event ids differ per tenant. Read `workcenter-status/list` and `workcenter-event/list` once and keep the ids in a model or broker configuration. * `CreateChecksheet` needs `operationId`, `inspectionMode` and `measurements`; take the field names from the control plan (`control-plan/list`) and the Checksheet API in the Plex portal. * Plex publishes many more APIs (sales orders, purchasing, EDI, accounting, digital work instructions, Plex APM). Add events for them the same way: one `ADD EVENT` per operation, with the path from the Developer Portal. ## Related * [REST API Connectors](/latest/lot-language/routes/rest-api-routes) * [REST API app templates](/latest/lot-language/routes/rest-api-apps/index) * [Siemens Opcenter Execution](/latest/lot-language/routes/rest-api-apps/siemens-opcenter-execution) * [SAP Digital Manufacturing](/latest/lot-language/routes/rest-api-apps/sap-digital-manufacturing) # Salesforce Source: https://docs.coreflux.org/latest/lot-language/routes/rest-api-apps/salesforce Client-mode REST API connector for a curated Salesforce REST API v62.0 slice: Cases from alarms, Asset telemetry, SOQL, Platform Events, and Composite Technical Preview ## Overview A client-mode connector for a **curated CRM / Field Service / IIoT bridge** slice of the [Salesforce REST API](https://developer.salesforce.com/docs/atlas.en-us.api_rest.meta/api_rest/) under `https://YOUR_INSTANCE.my.salesforce.com/services/data/v62.0/`: | Catalog connector event | Entity / path | Notes | | - | - | - | | (discovery) | `GET /services/data/v62.0/sobjects` | Global sObject catalog; a good first call | | GET SOQL query?q= | `GET /services/data/v62.0/query?q=` | URL-encode the SOQL string | | POST Case from alarm | `POST /services/data/v62.0/sobjects/Case` | Map IIoT alerts → Case | | PATCH Asset with telemetry | `PATCH /services/data/v62.0/sobjects/Asset/{id}` | Update status / custom fields | | POST Platform Event | `POST /services/data/v62.0/sobjects/{Event__e}` | `eventName` must end with `__e` | | (batch) | `POST /services/data/v62.0/composite` | Multi-request in one round-trip | `BASE_ADDRESS` is the org **instance URL** from the OAuth token response (no trailing slash). Topics use `crm/salesforce//`. > [!NOTE] > > Use the `instance_url` returned with the access token (for example `https://mycompany.my.salesforce.com`). `login.salesforce.com` and `test.salesforce.com` are for OAuth token exchange only — calling `/services/data/` on the login host returns `URL_NOT_RESET`. > [!NOTE] > > API version is pinned to **v62.0**. Bump the path version in the `.lot` when you standardize on a newer org API version. Bulk 2.0 and Pub/Sub (gRPC) are out of scope for this REST\_API client template. Prefer Hub secrets for tokens. ## Basic Syntax ```lot wrap theme={"theme":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} # Coreflux REST_API template — Salesforce (REST API v62.0 curated slice) # Auth: Bearer {value.json.access_token} # BASE_ADDRESS: https://YOUR_INSTANCE.my.salesforce.com DEFINE CONNECTOR SalesforceRestApi WITH TYPE REST_API ADD REST_API_CONFIG WITH BASE_ADDRESS "https://YOUR_INSTANCE.my.salesforce.com" WITH ENABLE_CLIENT "true" WITH USE_SSL "true" WITH REQUEST_TIMEOUT '60' WITH DEFAULT_CONTENT_TYPE "application/json" ADD EVENT Listsobjects WITH SOURCE_TOPIC "crm/salesforce/sobjects/list" WITH DESTINATION_TOPIC "crm/salesforce/sobjects/list/response" WITH METHOD "GET" WITH ENDPOINT "/services/data/v62.0/sobjects" WITH HEADERS ADD "Accept" "application/json" ADD "Authorization" "Bearer {value.json.access_token}" ADD EVENT Querysoql WITH SOURCE_TOPIC "crm/salesforce/query/soql" WITH DESTINATION_TOPIC "crm/salesforce/query/soql/response" WITH METHOD "GET" WITH ENDPOINT "/services/data/v62.0/query?q={value.json.q}" WITH HEADERS ADD "Accept" "application/json" ADD "Authorization" "Bearer {value.json.access_token}" ADD EVENT Createcase WITH SOURCE_TOPIC "crm/salesforce/case/create" WITH DESTINATION_TOPIC "crm/salesforce/case/create/response" WITH METHOD "POST" WITH ENDPOINT "/services/data/v62.0/sobjects/Case" WITH HEADERS ADD "Accept" "application/json" ADD "Content-Type" "application/json" ADD "Authorization" "Bearer {value.json.access_token}" WITH BODY "{payload}" ADD EVENT Patchasset WITH SOURCE_TOPIC "crm/salesforce/asset/patch" WITH DESTINATION_TOPIC "crm/salesforce/asset/patch/response" WITH METHOD "PATCH" WITH ENDPOINT "/services/data/v62.0/sobjects/Asset/{value.json.assetId}" WITH HEADERS ADD "Accept" "application/json" ADD "Content-Type" "application/json" ADD "Authorization" "Bearer {value.json.access_token}" WITH BODY "{payload}" ADD EVENT Createplatformevent WITH SOURCE_TOPIC "crm/salesforce/platform-event/create" WITH DESTINATION_TOPIC "crm/salesforce/platform-event/create/response" WITH METHOD "POST" WITH ENDPOINT "/services/data/v62.0/sobjects/{value.json.eventName}" WITH HEADERS ADD "Accept" "application/json" ADD "Content-Type" "application/json" ADD "Authorization" "Bearer {value.json.access_token}" WITH BODY "{payload}" ADD EVENT Composite WITH SOURCE_TOPIC "crm/salesforce/composite/post" WITH DESTINATION_TOPIC "crm/salesforce/composite/post/response" WITH METHOD "POST" WITH ENDPOINT "/services/data/v62.0/composite" WITH HEADERS ADD "Accept" "application/json" ADD "Content-Type" "application/json" ADD "Authorization" "Bearer {value.json.access_token}" WITH BODY "{payload}" ``` ## Events | Event | Method | Endpoint | Request topic | Response topic | | - | - | - | - | - | | Listsobjects | GET | `/services/data/v62.0/sobjects` | `crm/salesforce/sobjects/list` | `crm/salesforce/sobjects/list/response` | | Querysoql | GET | `/services/data/v62.0/query?q={q}` | `crm/salesforce/query/soql` | `crm/salesforce/query/soql/response` | | Createcase | POST | `/services/data/v62.0/sobjects/Case` | `crm/salesforce/case/create` | `crm/salesforce/case/create/response` | | Patchasset | PATCH | `/services/data/v62.0/sobjects/Asset/{assetId}` | `crm/salesforce/asset/patch` | `crm/salesforce/asset/patch/response` | | Createplatformevent | POST | `/services/data/v62.0/sobjects/{eventName}` | `crm/salesforce/platform-event/create` | `crm/salesforce/platform-event/create/response` | | Composite | POST | `/services/data/v62.0/composite` | `crm/salesforce/composite/post` | `crm/salesforce/composite/post/response` | ## Auth **OAuth 2.0 Bearer access token — default in the template:** 1. In Salesforce Setup, create a **Connected App** with the OAuth scopes your integration needs (at least API access). 2. Obtain an `access_token` (authorization code, JWT bearer, or client credentials where the org enables it). 3. Set connector `BASE_ADDRESS` to the token response `instance_url` (My Domain host). 4. Pass `access_token` on the MQTT request payload so headers resolve: * `Authorization: Bearer {value.json.access_token}` 5. Store the token in Hub secrets / `GET SECRET`. Never commit real credentials. ## Example payloads **List sObjects:** include `access_token` on the MQTT JSON root used for header substitution. **SOQL:** ```json theme={"theme":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} { "access_token": "YOUR_ACCESS_TOKEN", "q": "SELECT+Id,+Subject,+Status+FROM+Case+LIMIT+10" } ``` **Create Case:** ```json theme={"theme":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} { "Subject": "Line sensor fault — Zone A", "Description": "Vibration above threshold", "Origin": "IoT", "Priority": "High", "Status": "New" } ``` **PATCH Asset:** path uses `assetId`; body is the field map. **Platform Event:** set `eventName` to the API name ending in `__e` (for example `DeviceAlert__e`); body is the event field map. ## Deploy notes * Deploy **`SalesforceRestApi.lot` only** (`-addConnector`). Do **not** deploy `hub-metadata.lot` as a connector — it exists for Hub / `-listTemplates` (`ADD METADATA` + `WITH IMAGE` base64). * Hub category: **REST API Connectors**. * Doc link: [https://docs.coreflux.org/latest/lot-language/routes/rest-api-apps/salesforce](https://docs.coreflux.org/latest/lot-language/routes/rest-api-apps/salesforce) ## Related * [REST API Connectors](/latest/lot-language/routes/rest-api-routes) * [REST API Apps catalog](/latest/lot-language/routes/rest-api-apps) # Samsara REST API Source: https://docs.coreflux.org/latest/lot-language/routes/rest-api-apps/samsara Client-mode REST API connector for Samsara fleet vehicle stats, assets, and alerts Technical Preview ## Overview Client-mode REST API connector for Samsara fleet vehicle stats, assets, and alerts. `BASE_ADDRESS` defaults to `https://api.samsara.com`. Auth: **Bearer API token**. Pass secrets only at runtime via MQTT JSON placeholders or Hub `GET SECRET` — never commit real credentials. ## Auth Bearer API token. Prefer Hub secrets / `GET SECRET`. Never hard-code tokens into the `.lot`. ## Basic Syntax ```lot wrap theme={"theme":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} DEFINE CONNECTOR SamsaraRestApi WITH TYPE REST_API ADD REST_API_CONFIG WITH BASE_ADDRESS "https://api.samsara.com" WITH ENABLE_CLIENT "true" WITH USE_SSL "true" WITH REQUEST_TIMEOUT '30' WITH DEFAULT_CONTENT_TYPE "application/json" # Auth: Authorization Bearer {value.json.api_token} — prefer Hub secrets / GET SECRET. # Optional Basic: WITH USERNAME "YOUR_USER" / WITH PASSWORD GET SECRET "APP_PASSWORD" ADD EVENT Listvehicles WITH SOURCE_TOPIC "integrations/samsara/request/Listvehicles" WITH DESTINATION_TOPIC "integrations/samsara/response/Listvehicles" WITH METHOD "GET" WITH ENDPOINT "/fleet/vehicles" WITH HEADERS ADD "Accept" "application/json" ADD "Authorization" "Bearer {value.json.api_token}" ADD EVENT Getvehiclestats WITH SOURCE_TOPIC "integrations/samsara/request/Getvehiclestats" WITH DESTINATION_TOPIC "integrations/samsara/response/Getvehiclestats" WITH METHOD "GET" WITH ENDPOINT "/fleet/vehicles/stats?types={value.json.types}" WITH HEADERS ADD "Accept" "application/json" ADD "Authorization" "Bearer {value.json.api_token}" ADD EVENT Getvehiclestatsfeed WITH SOURCE_TOPIC "integrations/samsara/request/Getvehiclestatsfeed" WITH DESTINATION_TOPIC "integrations/samsara/response/Getvehiclestatsfeed" WITH METHOD "GET" WITH ENDPOINT "/fleet/vehicles/stats/feed?types={value.json.types}" WITH HEADERS ADD "Accept" "application/json" ADD "Authorization" "Bearer {value.json.api_token}" ADD EVENT Getvehicle # ... additional EVENTs in package .lot ... ``` ## MQTT topics Request topics under `integrations/samsara/request/...`; responses on `integrations/samsara/response/...`. ## Template reference Generated from the shipped broker template `samsara` (`-connectorCode SAMSARA`). It lists every event the template defines. ### Auth Bearer API token Auth headers used by the template: `Authorization: Bearer {value.json.api_token}`. ### Template parameters | Placeholder | Type | Description | Required | | - | - | - | - | | `RouteName` | string | Name of the Samsara connector | no | | `BaseUrl` | string | Samsara base URL without trailing slash (e.g. [https://api.samsara.com](https://api.samsara.com)) | yes | `BASE_ADDRESS` default: `https://api.samsara.com`. ### Events | Event | Method | Endpoint | Request topic | Response topic | Payload fields | | - | - | - | - | - | - | | `Listvehicles` | GET | `/fleet/vehicles` | `integrations/samsara/request/Listvehicles` | `integrations/samsara/response/Listvehicles` | `api_token` | | `Getvehiclestats` | GET | `/fleet/vehicles/stats?types={value.json.types}` | `integrations/samsara/request/Getvehiclestats` | `integrations/samsara/response/Getvehiclestats` | `api_token`, `types` | | `Getvehiclestatsfeed` | GET | `/fleet/vehicles/stats/feed?types={value.json.types}` | `integrations/samsara/request/Getvehiclestatsfeed` | `integrations/samsara/response/Getvehiclestatsfeed` | `api_token`, `types` | | `Getvehicle` | GET | `/fleet/vehicles/{value.json.id}` | `integrations/samsara/request/Getvehicle` | `integrations/samsara/response/Getvehicle` | `api_token`, `id` | | `Listindustrialassets` | GET | `/industrial/assets` | `integrations/samsara/request/Listindustrialassets` | `integrations/samsara/response/Listindustrialassets` | `api_token` | | `Listdrivers` | GET | `/fleet/drivers` | `integrations/samsara/request/Listdrivers` | `integrations/samsara/response/Listdrivers` | `api_token` | | `Listequipment` | GET | `/fleet/equipment` | `integrations/samsara/request/Listequipment` | `integrations/samsara/response/Listequipment` | `api_token` | | `Listaddresses` | GET | `/addresses` | `integrations/samsara/request/Listaddresses` | `integrations/samsara/response/Listaddresses` | `api_token` | ### Deploy Get the template card (JSON with `template` and `metadata`) from the broker, then deploy the connector: ```bash theme={"theme":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} mosquitto_pub -t '$SYS/Coreflux/Command' -m "-connectorCode SAMSARA" ``` The deployable connector from the REST API app, with `BASE_ADDRESS` already filled in: ```lot theme={"theme":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} DEFINE CONNECTOR SamsaraRestApi WITH TYPE REST_API ADD REST_API_CONFIG WITH BASE_ADDRESS "https://api.samsara.com" WITH ENABLE_CLIENT "true" WITH USE_SSL "true" WITH REQUEST_TIMEOUT '30' WITH DEFAULT_CONTENT_TYPE "application/json" ADD EVENT Listvehicles WITH SOURCE_TOPIC "integrations/samsara/request/Listvehicles" WITH DESTINATION_TOPIC "integrations/samsara/response/Listvehicles" WITH METHOD "GET" WITH ENDPOINT "/fleet/vehicles" WITH HEADERS ADD "Accept" "application/json" ADD "Authorization" "Bearer {value.json.api_token}" ADD EVENT Getvehiclestats WITH SOURCE_TOPIC "integrations/samsara/request/Getvehiclestats" WITH DESTINATION_TOPIC "integrations/samsara/response/Getvehiclestats" WITH METHOD "GET" WITH ENDPOINT "/fleet/vehicles/stats?types={value.json.types}" WITH HEADERS ADD "Accept" "application/json" ADD "Authorization" "Bearer {value.json.api_token}" ADD EVENT Getvehiclestatsfeed WITH SOURCE_TOPIC "integrations/samsara/request/Getvehiclestatsfeed" WITH DESTINATION_TOPIC "integrations/samsara/response/Getvehiclestatsfeed" WITH METHOD "GET" WITH ENDPOINT "/fleet/vehicles/stats/feed?types={value.json.types}" WITH HEADERS ADD "Accept" "application/json" ADD "Authorization" "Bearer {value.json.api_token}" ADD EVENT Getvehicle WITH SOURCE_TOPIC "integrations/samsara/request/Getvehicle" WITH DESTINATION_TOPIC "integrations/samsara/response/Getvehicle" WITH METHOD "GET" WITH ENDPOINT "/fleet/vehicles/{value.json.id}" WITH HEADERS ADD "Accept" "application/json" ADD "Authorization" "Bearer {value.json.api_token}" ADD EVENT Listindustrialassets WITH SOURCE_TOPIC "integrations/samsara/request/Listindustrialassets" WITH DESTINATION_TOPIC "integrations/samsara/response/Listindustrialassets" WITH METHOD "GET" WITH ENDPOINT "/industrial/assets" WITH HEADERS ADD "Accept" "application/json" ADD "Authorization" "Bearer {value.json.api_token}" ADD EVENT Listdrivers WITH SOURCE_TOPIC "integrations/samsara/request/Listdrivers" WITH DESTINATION_TOPIC "integrations/samsara/response/Listdrivers" WITH METHOD "GET" WITH ENDPOINT "/fleet/drivers" WITH HEADERS ADD "Accept" "application/json" ADD "Authorization" "Bearer {value.json.api_token}" ADD EVENT Listequipment WITH SOURCE_TOPIC "integrations/samsara/request/Listequipment" WITH DESTINATION_TOPIC "integrations/samsara/response/Listequipment" WITH METHOD "GET" WITH ENDPOINT "/fleet/equipment" WITH HEADERS ADD "Accept" "application/json" ADD "Authorization" "Bearer {value.json.api_token}" ADD EVENT Listaddresses WITH SOURCE_TOPIC "integrations/samsara/request/Listaddresses" WITH DESTINATION_TOPIC "integrations/samsara/response/Listaddresses" WITH METHOD "GET" WITH ENDPOINT "/addresses" WITH HEADERS ADD "Accept" "application/json" ADD "Authorization" "Bearer {value.json.api_token}" ``` Deploy it with `-addConnector` (do not include `ADD METADATA`). ### Usage example Call `Listvehicles` by publishing a JSON payload to its request topic, and read the reply on the response topic: ```bash theme={"theme":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} mosquitto_sub -t 'integrations/samsara/response/Listvehicles' & mosquitto_pub -t 'integrations/samsara/request/Listvehicles' -m '{"api_token": ""}' ``` ## Related * [REST API Connectors](/latest/lot-language/routes/rest-api-routes) * [REST API apps index](/latest/lot-language/routes/rest-api-apps) # Sap Ariba Open Api Source: https://docs.coreflux.org/latest/lot-language/routes/rest-api-apps/sap-ariba-open-api LoT connector for SAP Ariba: purchase orders on SAP Business Network, supplier catalog items, procurement reporting, and requisition and invoice approvals Technical Preview ## Overview Client-mode REST API connectors for SAP Ariba: purchase orders and line items on SAP Business Network for suppliers and buyers, supplier catalog items, Operational Reporting for Procurement, and Document Approval for requisitions and invoices. This is one of the templates in the SAP Ariba REST API app (`sap-ariba`). Deploy it alongside the other connectors of that app when they depend on each other (for example a token connector plus an API connector). ## Auth * **schemes**: \["OAuth 2.0 client credentials at /v2/oauth/token with Basic base64(clientId:clientSecret)", "apiKey header (application key) + Authorization: Bearer \ on every gateway call", "X-ARIBA-NETWORK-ID header (ANID) on SAP Business Network APIs", "realm query parameter on realm APIs"] * **secrets**: \["ARIBA\_OAUTH\_BASIC", "ARIBA\_API\_KEY", "ARIBA\_ANID"] * **note**: One Developer Portal application per API; key and token must come from the same application. Auth headers used by the template: `Authorization: Bearer {value.json.token}`, `apiKey: {value.json.apikey}`. ## Template parameters | Placeholder | Type | Description | Required | | - | - | - | - | | `RouteName` | string | Name of the Sap Ariba Open Api connector | no | | `BaseUrl` | string | Sap Ariba Open Api base URL without trailing slash (e.g. [https://openapi.ariba.com](https://openapi.ariba.com)) | yes | `BASE_ADDRESS` default: `https://openapi.ariba.com`. ## Events | Event | Method | Endpoint | Request topic | Response topic | Payload fields | | - | - | - | - | - | - | | `SupplierOrders` | GET | `/api/purchase-orders-supplier/v1/prod/orders?{value.json.query}` | `erp/sap-ariba/supplier/orders/get` | `erp/sap-ariba/supplier/orders/get/response` | `anid`, `apikey`, `query`, `token` | | `SupplierOrderItems` | GET | `/api/purchase-orders-supplier/v1/prod/items?{value.json.query}` | `erp/sap-ariba/supplier/order-items/get` | `erp/sap-ariba/supplier/order-items/get/response` | `anid`, `apikey`, `query`, `token` | | `BuyerOrders` | GET | `/api/purchase-orders/v1/prod/orders?{value.json.query}` | `erp/sap-ariba/buyer/orders/get` | `erp/sap-ariba/buyer/orders/get/response` | `anid`, `apikey`, `query`, `token` | | `BuyerOrderItems` | GET | `/api/purchase-orders/v1/prod/items?{value.json.query}` | `erp/sap-ariba/buyer/order-items/get` | `erp/sap-ariba/buyer/order-items/get/response` | `anid`, `apikey`, `query`, `token` | | `CatalogSubscriptions` | GET | `/api/catalogcontent/v1/prod/catalogs?realm={value.json.realm}&date={value.json.date}{value.json.params}` | `erp/sap-ariba/catalog/subscriptions/get` | `erp/sap-ariba/catalog/subscriptions/get/response` | `apikey`, `date`, `params`, `realm`, `token` | | `CatalogItems` | GET | `/api/catalogcontent/v1/prod/catalogname/{value.json.catalogname}/supplierid/{value.json.supplierid}/items?realm={value.json.realm}{value.json.params}` | `erp/sap-ariba/catalog/items/get` | `erp/sap-ariba/catalog/items/get/response` | `apikey`, `catalogname`, `params`, `realm`, `supplierid`, `token` | | `CatalogItemSchema` | GET | `/api/catalogcontent/v1/prod/items/schema?realm={value.json.realm}` | `erp/sap-ariba/catalog/items/schema/get` | `erp/sap-ariba/catalog/items/schema/get/response` | `apikey`, `realm`, `token` | | `ReportingViewTemplates` | GET | `/api/procurement-reporting-view/v2/prod/viewTemplates?realm={value.json.realm}{value.json.params}` | `erp/sap-ariba/reporting/view-templates/get` | `erp/sap-ariba/reporting/view-templates/get/response` | `apikey`, `params`, `realm`, `token` | | `ReportingView` | GET | `/api/procurement-reporting-details/v2/prod/views/{value.json.template}?realm={value.json.realm}{value.json.params}` | `erp/sap-ariba/reporting/view/get` | `erp/sap-ariba/reporting/view/get/response` | `apikey`, `params`, `realm`, `template`, `token` | | `ReportingViewCount` | GET | `/api/procurement-reporting-details/v2/prod/views/{value.json.template}/count?realm={value.json.realm}{value.json.params}` | `erp/sap-ariba/reporting/view/count` | `erp/sap-ariba/reporting/view/count/response` | `apikey`, `params`, `realm`, `template`, `token` | | `ReportingJobSubmit` | POST | `/api/procurement-reporting-job/v2/prod/jobs?realm={value.json.realm}` | `erp/sap-ariba/reporting/jobs/submit` | `erp/sap-ariba/reporting/jobs/submit/response` | `apikey`, `body`, `realm`, `token` | | `ReportingJobStatus` | GET | `/api/procurement-reporting-jobresult/v2/prod/jobs/{value.json.jobid}?realm={value.json.realm}` | `erp/sap-ariba/reporting/jobs/status/get` | `erp/sap-ariba/reporting/jobs/status/get/response` | `apikey`, `jobid`, `realm`, `token` | | `ApprovalChanges` | GET | `/api/approval/v1/prod/changes?realm={value.json.realm}{value.json.params}` | `erp/sap-ariba/approval/changes/get` | `erp/sap-ariba/approval/changes/get/response` | `apikey`, `params`, `realm`, `token` | | `PendingApprovables` | GET | `/api/approval/v1/prod/pendingApprovables?realm={value.json.realm}{value.json.params}` | `erp/sap-ariba/approval/pending/get` | `erp/sap-ariba/approval/pending/get/response` | `apikey`, `params`, `realm`, `token` | | `GetApprovable` | GET | `/api/approval/v1/prod/{value.json.approvabletype}/{value.json.approvableid}?realm={value.json.realm}` | `erp/sap-ariba/approval/approvable/get` | `erp/sap-ariba/approval/approvable/get/response` | `apikey`, `approvableid`, `approvabletype`, `realm`, `token` | | `SetApprovableState` | PATCH | `/api/approval/v1/prod/{value.json.approvabletype}/{value.json.approvableid}?realm={value.json.realm}&user={value.json.user}&passwordadapter={value.json.passwordadapter}` | `erp/sap-ariba/approval/approvable/set` | `erp/sap-ariba/approval/approvable/set/response` | `apikey`, `approvableid`, `approvabletype`, `body`, `passwordadapter`, `realm`, `token`, `user` | ## Deploy Get the template card (JSON with `template` and `metadata`) from the broker, then deploy the connector: ```bash theme={"theme":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} mosquitto_pub -t '$SYS/Coreflux/Command' -m "-connectorCode SAP_ARIBA" ``` The deployable connector from the REST API app, with `BASE_ADDRESS` already filled in: ```lot theme={"theme":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} DEFINE CONNECTOR SapAribaOpenApi WITH TYPE REST_API ADD REST_API_CONFIG WITH BASE_ADDRESS "https://openapi.ariba.com" WITH ENABLE_CLIENT "true" WITH USE_SSL "true" WITH REQUEST_TIMEOUT '60' WITH DEFAULT_CONTENT_TYPE "application/json" ADD EVENT SupplierOrders WITH SOURCE_TOPIC "erp/sap-ariba/supplier/orders/get" WITH DESTINATION_TOPIC "erp/sap-ariba/supplier/orders/get/response" WITH METHOD "GET" WITH ENDPOINT "/api/purchase-orders-supplier/v1/prod/orders?{value.json.query}" WITH HEADERS ADD "Accept" "application/json" ADD "User-Agent" "Coreflux-Broker REST_API route (https://www.coreflux.org)" ADD "apiKey" "{value.json.apikey}" ADD "Authorization" "Bearer {value.json.token}" ADD "X-ARIBA-NETWORK-ID" "{value.json.anid}" ADD EVENT SupplierOrderItems WITH SOURCE_TOPIC "erp/sap-ariba/supplier/order-items/get" WITH DESTINATION_TOPIC "erp/sap-ariba/supplier/order-items/get/response" WITH METHOD "GET" WITH ENDPOINT "/api/purchase-orders-supplier/v1/prod/items?{value.json.query}" WITH HEADERS ADD "Accept" "application/json" ADD "User-Agent" "Coreflux-Broker REST_API route (https://www.coreflux.org)" ADD "apiKey" "{value.json.apikey}" ADD "Authorization" "Bearer {value.json.token}" ADD "X-ARIBA-NETWORK-ID" "{value.json.anid}" ADD EVENT BuyerOrders WITH SOURCE_TOPIC "erp/sap-ariba/buyer/orders/get" WITH DESTINATION_TOPIC "erp/sap-ariba/buyer/orders/get/response" WITH METHOD "GET" WITH ENDPOINT "/api/purchase-orders/v1/prod/orders?{value.json.query}" WITH HEADERS ADD "Accept" "application/json" ADD "User-Agent" "Coreflux-Broker REST_API route (https://www.coreflux.org)" ADD "apiKey" "{value.json.apikey}" ADD "Authorization" "Bearer {value.json.token}" ADD "X-ARIBA-NETWORK-ID" "{value.json.anid}" ADD EVENT BuyerOrderItems WITH SOURCE_TOPIC "erp/sap-ariba/buyer/order-items/get" WITH DESTINATION_TOPIC "erp/sap-ariba/buyer/order-items/get/response" WITH METHOD "GET" WITH ENDPOINT "/api/purchase-orders/v1/prod/items?{value.json.query}" WITH HEADERS ADD "Accept" "application/json" ADD "User-Agent" "Coreflux-Broker REST_API route (https://www.coreflux.org)" ADD "apiKey" "{value.json.apikey}" ADD "Authorization" "Bearer {value.json.token}" ADD "X-ARIBA-NETWORK-ID" "{value.json.anid}" ADD EVENT CatalogSubscriptions WITH SOURCE_TOPIC "erp/sap-ariba/catalog/subscriptions/get" WITH DESTINATION_TOPIC "erp/sap-ariba/catalog/subscriptions/get/response" WITH METHOD "GET" WITH ENDPOINT "/api/catalogcontent/v1/prod/catalogs?realm={value.json.realm}&date={value.json.date}{value.json.params}" WITH HEADERS ADD "Accept" "application/json" ADD "User-Agent" "Coreflux-Broker REST_API route (https://www.coreflux.org)" ADD "apiKey" "{value.json.apikey}" ADD "Authorization" "Bearer {value.json.token}" ADD EVENT CatalogItems WITH SOURCE_TOPIC "erp/sap-ariba/catalog/items/get" WITH DESTINATION_TOPIC "erp/sap-ariba/catalog/items/get/response" WITH METHOD "GET" WITH ENDPOINT "/api/catalogcontent/v1/prod/catalogname/{value.json.catalogname}/supplierid/{value.json.supplierid}/items?realm={value.json.realm}{value.json.params}" WITH HEADERS ADD "Accept" "application/json" ADD "User-Agent" "Coreflux-Broker REST_API route (https://www.coreflux.org)" ADD "apiKey" "{value.json.apikey}" ADD "Authorization" "Bearer {value.json.token}" ADD EVENT CatalogItemSchema WITH SOURCE_TOPIC "erp/sap-ariba/catalog/items/schema/get" WITH DESTINATION_TOPIC "erp/sap-ariba/catalog/items/schema/get/response" WITH METHOD "GET" WITH ENDPOINT "/api/catalogcontent/v1/prod/items/schema?realm={value.json.realm}" WITH HEADERS ADD "Accept" "application/json" ADD "User-Agent" "Coreflux-Broker REST_API route (https://www.coreflux.org)" ADD "apiKey" "{value.json.apikey}" ADD "Authorization" "Bearer {value.json.token}" ADD EVENT ReportingViewTemplates WITH SOURCE_TOPIC "erp/sap-ariba/reporting/view-templates/get" WITH DESTINATION_TOPIC "erp/sap-ariba/reporting/view-templates/get/response" WITH METHOD "GET" WITH ENDPOINT "/api/procurement-reporting-view/v2/prod/viewTemplates?realm={value.json.realm}{value.json.params}" WITH HEADERS ADD "Accept" "application/json" ADD "User-Agent" "Coreflux-Broker REST_API route (https://www.coreflux.org)" ADD "apiKey" "{value.json.apikey}" ADD "Authorization" "Bearer {value.json.token}" ADD EVENT ReportingView WITH SOURCE_TOPIC "erp/sap-ariba/reporting/view/get" WITH DESTINATION_TOPIC "erp/sap-ariba/reporting/view/get/response" WITH METHOD "GET" WITH ENDPOINT "/api/procurement-reporting-details/v2/prod/views/{value.json.template}?realm={value.json.realm}{value.json.params}" WITH HEADERS ADD "Accept" "application/json" ADD "User-Agent" "Coreflux-Broker REST_API route (https://www.coreflux.org)" ADD "apiKey" "{value.json.apikey}" ADD "Authorization" "Bearer {value.json.token}" ADD EVENT ReportingViewCount WITH SOURCE_TOPIC "erp/sap-ariba/reporting/view/count" WITH DESTINATION_TOPIC "erp/sap-ariba/reporting/view/count/response" WITH METHOD "GET" WITH ENDPOINT "/api/procurement-reporting-details/v2/prod/views/{value.json.template}/count?realm={value.json.realm}{value.json.params}" WITH HEADERS ADD "Accept" "application/json" ADD "User-Agent" "Coreflux-Broker REST_API route (https://www.coreflux.org)" ADD "apiKey" "{value.json.apikey}" ADD "Authorization" "Bearer {value.json.token}" ADD EVENT ReportingJobSubmit WITH SOURCE_TOPIC "erp/sap-ariba/reporting/jobs/submit" WITH DESTINATION_TOPIC "erp/sap-ariba/reporting/jobs/submit/response" WITH METHOD "POST" WITH ENDPOINT "/api/procurement-reporting-job/v2/prod/jobs?realm={value.json.realm}" WITH HEADERS ADD "Accept" "application/json" ADD "User-Agent" "Coreflux-Broker REST_API route (https://www.coreflux.org)" ADD "apiKey" "{value.json.apikey}" ADD "Authorization" "Bearer {value.json.token}" ADD "Content-Type" "application/json" WITH BODY "{value.json.body}" ADD EVENT ReportingJobStatus WITH SOURCE_TOPIC "erp/sap-ariba/reporting/jobs/status/get" WITH DESTINATION_TOPIC "erp/sap-ariba/reporting/jobs/status/get/response" WITH METHOD "GET" WITH ENDPOINT "/api/procurement-reporting-jobresult/v2/prod/jobs/{value.json.jobid}?realm={value.json.realm}" WITH HEADERS ADD "Accept" "application/json" ADD "User-Agent" "Coreflux-Broker REST_API route (https://www.coreflux.org)" ADD "apiKey" "{value.json.apikey}" ADD "Authorization" "Bearer {value.json.token}" ADD EVENT ApprovalChanges WITH SOURCE_TOPIC "erp/sap-ariba/approval/changes/get" WITH DESTINATION_TOPIC "erp/sap-ariba/approval/changes/get/response" WITH METHOD "GET" WITH ENDPOINT "/api/approval/v1/prod/changes?realm={value.json.realm}{value.json.params}" WITH HEADERS ADD "Accept" "application/json" ADD "User-Agent" "Coreflux-Broker REST_API route (https://www.coreflux.org)" ADD "apiKey" "{value.json.apikey}" ADD "Authorization" "Bearer {value.json.token}" ADD EVENT PendingApprovables WITH SOURCE_TOPIC "erp/sap-ariba/approval/pending/get" WITH DESTINATION_TOPIC "erp/sap-ariba/approval/pending/get/response" WITH METHOD "GET" WITH ENDPOINT "/api/approval/v1/prod/pendingApprovables?realm={value.json.realm}{value.json.params}" WITH HEADERS ADD "Accept" "application/json" ADD "User-Agent" "Coreflux-Broker REST_API route (https://www.coreflux.org)" ADD "apiKey" "{value.json.apikey}" ADD "Authorization" "Bearer {value.json.token}" ADD EVENT GetApprovable WITH SOURCE_TOPIC "erp/sap-ariba/approval/approvable/get" WITH DESTINATION_TOPIC "erp/sap-ariba/approval/approvable/get/response" WITH METHOD "GET" WITH ENDPOINT "/api/approval/v1/prod/{value.json.approvabletype}/{value.json.approvableid}?realm={value.json.realm}" WITH HEADERS ADD "Accept" "application/json" ADD "User-Agent" "Coreflux-Broker REST_API route (https://www.coreflux.org)" ADD "apiKey" "{value.json.apikey}" ADD "Authorization" "Bearer {value.json.token}" ADD EVENT SetApprovableState WITH SOURCE_TOPIC "erp/sap-ariba/approval/approvable/set" WITH DESTINATION_TOPIC "erp/sap-ariba/approval/approvable/set/response" WITH METHOD "PATCH" WITH ENDPOINT "/api/approval/v1/prod/{value.json.approvabletype}/{value.json.approvableid}?realm={value.json.realm}&user={value.json.user}&passwordadapter={value.json.passwordadapter}" WITH HEADERS ADD "Accept" "application/json" ADD "User-Agent" "Coreflux-Broker REST_API route (https://www.coreflux.org)" ADD "apiKey" "{value.json.apikey}" ADD "Authorization" "Bearer {value.json.token}" ADD "Content-Type" "application/json" WITH BODY "{value.json.body}" ``` Deploy it with `-addConnector` (do not include `ADD METADATA`). ## Usage example Call `SupplierOrders` by publishing a JSON payload to its request topic, and read the reply on the response topic: ```bash theme={"theme":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} mosquitto_sub -t 'erp/sap-ariba/supplier/orders/get/response' & mosquitto_pub -t 'erp/sap-ariba/supplier/orders/get' -m '{"anid": "", "apikey": "", "query": "", "token": ""}' ``` ## Related * [REST API Connectors](/latest/lot-language/routes/rest-api-routes) * [REST API Apps catalog](/latest/lot-language/routes/rest-api-apps) # SAP Business One Service Layer Source: https://docs.coreflux.org/latest/lot-language/routes/rest-api-apps/sap-b1 LoT connector for the SAP Business One Service Layer: read items and warehouse stock, business partners, and post receipts from production Technical Preview ## Overview A client-mode connector for a **curated inventory and master-data** slice of [SAP Business One Service Layer](https://help.sap.com/doc/056f69366b5345a386bb8149f1700c19/10.0/en-US/Service%20Layer%20API%20Reference.html) OData v4 (`/b1s/v2`): | Catalog connector event | Entity / path | Notes | | - | - | - | | GET Items and warehouse stock | `Items`, `Items('{ItemCode}')` + `$expand=ItemWarehouseInfoCollection` | QuantityOnStock + per-warehouse collection | | POST ProductionOrders receipt | `InventoryGenEntries` | Receipt from production: `DocumentLines.BaseType = 202` | | GET BusinessPartners | `BusinessPartners` | Customers / vendors | | Session | `Login`, `Logout` | `B1SESSION` (+ `ROUTEID`) cookie | `BASE_ADDRESS` is the Service Layer root (`https://{host}:50000/b1s/v2`). Topics use `erp/sap-b1//`. > [!NOTE] > > Service Layer requires a real company database and a session from `POST /Login`. Prefer Hub secrets for CompanyDB / UserName / Password. ## Basic Syntax ```lot wrap theme={"theme":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} # Coreflux REST_API template — SAP Business One Service Layer (curated Items / stock / BP / receipt slice) # Auth: Session login POST /Login → SessionId / Set-Cookie B1SESSION (+ ROUTEID). # BASE_ADDRESS: https://YOUR_HOST:50000/b1s/v2 DEFINE CONNECTOR SapB1RestApi WITH TYPE REST_API ADD REST_API_CONFIG WITH BASE_ADDRESS "https://YOUR_HOST:50000/b1s/v2" WITH ENABLE_CLIENT "true" WITH USE_SSL "true" WITH REQUEST_TIMEOUT '60' WITH DEFAULT_CONTENT_TYPE "application/json" ADD EVENT Login WITH SOURCE_TOPIC "erp/sap-b1/session/login" WITH DESTINATION_TOPIC "erp/sap-b1/session/login/response" WITH METHOD "POST" WITH ENDPOINT "/Login" WITH HEADERS ADD "Accept" "application/json" ADD "Content-Type" "application/json" WITH BODY "{payload}" ADD EVENT Logout WITH SOURCE_TOPIC "erp/sap-b1/session/logout" WITH DESTINATION_TOPIC "erp/sap-b1/session/logout/response" WITH METHOD "GET" WITH ENDPOINT "/Logout" WITH HEADERS ADD "Accept" "application/json" ADD "Cookie" "B1SESSION={value.json.B1SESSION}; ROUTEID={value.json.ROUTEID}" ADD EVENT Getitems WITH SOURCE_TOPIC "erp/sap-b1/items/get" WITH DESTINATION_TOPIC "erp/sap-b1/items/get/response" WITH METHOD "GET" WITH ENDPOINT "/Items?$select=ItemCode,ItemName,QuantityOnStock&$top={value.json.top}" WITH HEADERS ADD "Accept" "application/json" ADD "Cookie" "B1SESSION={value.json.B1SESSION}; ROUTEID={value.json.ROUTEID}" ADD EVENT Getitemwarehousestock WITH SOURCE_TOPIC "erp/sap-b1/items/warehouse-stock/get" WITH DESTINATION_TOPIC "erp/sap-b1/items/warehouse-stock/get/response" WITH METHOD "GET" WITH ENDPOINT "/Items('{value.json.ItemCode}')?$select=ItemCode,ItemName,QuantityOnStock&$expand=ItemWarehouseInfoCollection" WITH HEADERS ADD "Accept" "application/json" ADD "Cookie" "B1SESSION={value.json.B1SESSION}; ROUTEID={value.json.ROUTEID}" ADD EVENT Getbusinesspartners WITH SOURCE_TOPIC "erp/sap-b1/business-partners/get" WITH DESTINATION_TOPIC "erp/sap-b1/business-partners/get/response" WITH METHOD "GET" WITH ENDPOINT "/BusinessPartners?$select=CardCode,CardName,CardType&$top={value.json.top}" WITH HEADERS ADD "Accept" "application/json" ADD "Cookie" "B1SESSION={value.json.B1SESSION}; ROUTEID={value.json.ROUTEID}" ADD EVENT Createinventorygenentry WITH SOURCE_TOPIC "erp/sap-b1/inventory-gen-entry/create" WITH DESTINATION_TOPIC "erp/sap-b1/inventory-gen-entry/create/response" WITH METHOD "POST" WITH ENDPOINT "/InventoryGenEntries" WITH HEADERS ADD "Accept" "application/json" ADD "Content-Type" "application/json" ADD "Cookie" "B1SESSION={value.json.B1SESSION}; ROUTEID={value.json.ROUTEID}" WITH BODY "{payload}" ``` ## Events | Event | Method | Endpoint | Request topic | Response topic | | - | - | - | - | - | | Login | POST | `/Login` | `erp/sap-b1/session/login` | `erp/sap-b1/session/login/response` | | Logout | GET | `/Logout` | `erp/sap-b1/session/logout` | `erp/sap-b1/session/logout/response` | | Getitems | GET | `/Items` | `erp/sap-b1/items/get` | `erp/sap-b1/items/get/response` | | Getitemwarehousestock | GET | `/Items('{ItemCode}')` + expand | `erp/sap-b1/items/warehouse-stock/get` | `erp/sap-b1/items/warehouse-stock/get/response` | | Getbusinesspartners | GET | `/BusinessPartners` | `erp/sap-b1/business-partners/get` | `erp/sap-b1/business-partners/get/response` | | Createinventorygenentry | POST | `/InventoryGenEntries` | `erp/sap-b1/inventory-gen-entry/create` | `erp/sap-b1/inventory-gen-entry/create/response` | ## Auth Session login (required before data calls): ```json theme={"theme":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} {"CompanyDB": "YOUR_COMPANY_DB", "UserName": "YOUR_B1_USER", "Password": "YOUR_B1_PASSWORD"} ``` Cookie on later events: ```lot wrap theme={"theme":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} ADD "Cookie" "B1SESSION={value.json.B1SESSION}; ROUTEID={value.json.ROUTEID}" ``` **CompanyDB** `string` *required* Company database name for Login. Prefer a Hub secret. Never commit the password or live DB name into git. **UserName** `string` *required* SAP Business One user for Login. **Password** `string` *required* Password for Login. Prefer Hub `GET SECRET`. Never commit. **B1SESSION** `string` *required* Session id from Login response (`SessionId` / `B1SESSION` cookie). Required on all events after Login. **ROUTEID** `string` *required* Load-balancer cookie from Set-Cookie (often `.node1`). Use the value your Service Layer returns. **ItemCode** `string` Item key for Getitemwarehousestock. **top** `integer` OData `$top` for list GETs (Items / BusinessPartners). High-level setup (no secrets in the template): 1. Point `BASE_ADDRESS` at `https://{host}:50000/b1s/v2` (TLS; trust the SL certificate in the lab). 2. Login with CompanyDB / UserName / Password (Hub secrets). 3. Copy `SessionId` → `B1SESSION` and `ROUTEID` into subsequent MQTT payloads. 4. Call GET Items / BusinessPartners; for receipt from production POST InventoryGenEntries with `BaseType: 202`. 5. Logout. Login payload: ```json theme={"theme":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} {"CompanyDB": "YOUR_COMPANY_DB", "UserName": "YOUR_B1_USER", "Password": "YOUR_B1_PASSWORD"} ``` GET Items (after login): ```json theme={"theme":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} {"B1SESSION": "YOUR_SESSION_ID", "ROUTEID": ".node1", "top": "10"} ``` POST receipt from production (connector body is `{payload}` — include Cookie fields in the same JSON envelope your broker expects, or set Cookie via header placeholders): ```json theme={"theme":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} { "B1SESSION": "YOUR_SESSION_ID", "ROUTEID": ".node1", "DocDate": "2026-10-03", "Comments": "Receipt from production via Coreflux", "DocumentLines": [ { "BaseType": 202, "BaseEntry": 1, "BaseLine": 0, "Quantity": 10, "WarehouseCode": "01" } ] } ``` ## Try it 1. Replace `YOUR_HOST` in `BASE_ADDRESS` and add the connector on your broker. 2. Publish login JSON to `erp/sap-b1/session/login`. 3. Subscribe to `erp/sap-b1/session/login/response`, then GET Items or BusinessPartners with `B1SESSION` / `ROUTEID`. 4. Optionally POST InventoryGenEntries for a known production order (`BaseType` 202). 5. Logout. ## Related * [REST API Connectors](/latest/lot-language/routes/rest-api-routes) * [REST API Apps](/latest/lot-language/routes/rest-api-apps) * [Service Layer API Reference](https://help.sap.com/doc/056f69366b5345a386bb8149f1700c19/10.0/en-US/Service%20Layer%20API%20Reference.html) * [Working with SAP Business One Service Layer (PDF)](https://help.sap.com/doc/fc2f5477516c404c8bf9ad1315a17238/10.0/en-US/Working_with_SAP_Business_One_Service_Layer.pdf) # SAP Digital Manufacturing Source: https://docs.coreflux.org/latest/lot-language/routes/rest-api-apps/sap-digital-manufacturing LoT connector for SAP Digital Manufacturing: start and complete SFCs, log nonconformances, post quantity confirmations and read production orders Technical Preview ## Overview A client-mode connector for a **curated MES / shop-floor** slice of [SAP Digital Manufacturing](https://api.sap.com/package/SAPDigitalManufacturingCloud) (Digital Manufacturing Cloud) public REST APIs on the Business Accelerator Hub: | Catalog connector event | API | Path | | - | - | - | | POST sfc/v1/sfcs/start | `sapdme_sfc` | `POST /sfc/v1/sfcs/start` | | POST sfc/v1/sfcs/complete | `sapdme_sfc` | `POST /sfc/v1/sfcs/complete` | | POST nonconformance | `sapdme_nonconformance` | `POST /nonconformance/v1/log` | | POST quantityConfirmation | `sapdme_quantityConfirmation` | `POST /quantityConfirmation/v1/confirm` | | GET order/v1/orders | `sapdme_order` | `GET /order/v1/orders?plant=&order=` | | list plants | `sapdme_plant` | `GET /plant/v1/plants` | | SFC detail | `sapdme_sfc` | `GET /sfc/v1/sfcdetail?plant=&sfc=` | `BASE_ADDRESS` is the public API host root (`https://api.{regionHost}.dmc.cloud.sap`, no trailing slash). Example placeholder uses `eu20`. Topics use `mes/sap-digital-manufacturing//`. See the base guide: [REST API Connectors](/latest/lot-language/routes/rest-api-routes). > [!NOTE] > > **Auth:** OAuth 2.0 **client credentials** from a BTP service key on the SAP Digital Manufacturing subaccount. Exchange at `{url}/oauth/token`, then pass `Authorization: Bearer {value.json.access_token}` on every MQTT payload. Prefer Hub secrets — never commit real tokens. `public-api-endpoint` from the service key must match `BASE_ADDRESS`. > [!NOTE] > > This template has **not** been verified against a live authenticated DMC tenant. ## Basic Syntax ```lot wrap theme={"theme":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} # Coreflux REST_API template — SAP Digital Manufacturing (curated MES slice) # Auth: Bearer {value.json.access_token} from BTP OAuth client credentials # BASE_ADDRESS: https://api.eu20.dmc.cloud.sap → substitute {regionHost} DEFINE CONNECTOR SapDigitalManufacturingRestApi WITH TYPE REST_API ADD REST_API_CONFIG WITH BASE_ADDRESS "https://api.eu20.dmc.cloud.sap" WITH ENABLE_CLIENT "true" WITH USE_SSL "true" WITH REQUEST_TIMEOUT '60' WITH DEFAULT_CONTENT_TYPE "application/json" ADD EVENT Listplants WITH SOURCE_TOPIC "mes/sap-digital-manufacturing/plant/list" WITH DESTINATION_TOPIC "mes/sap-digital-manufacturing/plant/list/response" WITH METHOD "GET" WITH ENDPOINT "/plant/v1/plants" WITH HEADERS ADD "Accept" "application/json" ADD "Authorization" "Bearer {value.json.access_token}" ADD EVENT Startsfc WITH SOURCE_TOPIC "mes/sap-digital-manufacturing/sfc/start" WITH DESTINATION_TOPIC "mes/sap-digital-manufacturing/sfc/start/response" WITH METHOD "POST" WITH ENDPOINT "/sfc/v1/sfcs/start" WITH HEADERS ADD "Accept" "application/json" ADD "Content-Type" "application/json" ADD "Authorization" "Bearer {value.json.access_token}" WITH BODY "{payload}" ADD EVENT Completesfc WITH SOURCE_TOPIC "mes/sap-digital-manufacturing/sfc/complete" WITH DESTINATION_TOPIC "mes/sap-digital-manufacturing/sfc/complete/response" WITH METHOD "POST" WITH ENDPOINT "/sfc/v1/sfcs/complete" WITH HEADERS ADD "Accept" "application/json" ADD "Content-Type" "application/json" ADD "Authorization" "Bearer {value.json.access_token}" WITH BODY "{payload}" ADD EVENT Lognonconformance WITH SOURCE_TOPIC "mes/sap-digital-manufacturing/nonconformance/log" WITH DESTINATION_TOPIC "mes/sap-digital-manufacturing/nonconformance/log/response" WITH METHOD "POST" WITH ENDPOINT "/nonconformance/v1/log" WITH HEADERS ADD "Accept" "application/json" ADD "Content-Type" "application/json" ADD "Authorization" "Bearer {value.json.access_token}" WITH BODY "{payload}" ADD EVENT Quantityconfirmation WITH SOURCE_TOPIC "mes/sap-digital-manufacturing/quantity-confirmation/confirm" WITH DESTINATION_TOPIC "mes/sap-digital-manufacturing/quantity-confirmation/confirm/response" WITH METHOD "POST" WITH ENDPOINT "/quantityConfirmation/v1/confirm" WITH HEADERS ADD "Accept" "application/json" ADD "Content-Type" "application/json" ADD "Authorization" "Bearer {value.json.access_token}" WITH BODY "{payload}" ADD EVENT Getorder WITH SOURCE_TOPIC "mes/sap-digital-manufacturing/order/get" WITH DESTINATION_TOPIC "mes/sap-digital-manufacturing/order/get/response" WITH METHOD "GET" WITH ENDPOINT "/order/v1/orders?plant={value.json.plant}&order={value.json.order}" WITH HEADERS ADD "Accept" "application/json" ADD "Authorization" "Bearer {value.json.access_token}" ``` ## Events | Event | MQTT request topic | Method | Endpoint | | - | - | - | - | | Listplants | `mes/sap-digital-manufacturing/plant/list` | GET | `/plant/v1/plants` | | Getsfcdetail | `mes/sap-digital-manufacturing/sfc/get` | GET | `/sfc/v1/sfcdetail?plant=&sfc=` | | Startsfc | `mes/sap-digital-manufacturing/sfc/start` | POST | `/sfc/v1/sfcs/start` | | Completesfc | `mes/sap-digital-manufacturing/sfc/complete` | POST | `/sfc/v1/sfcs/complete` | | Lognonconformance | `mes/sap-digital-manufacturing/nonconformance/log` | POST | `/nonconformance/v1/log` | | Quantityconfirmation | `mes/sap-digital-manufacturing/quantity-confirmation/confirm` | POST | `/quantityConfirmation/v1/confirm` | | Getorder | `mes/sap-digital-manufacturing/order/get` | GET | `/order/v1/orders?plant=&order=` | Each request topic has a matching `/response` destination. POST bodies use `{payload}` (full MQTT JSON body forwarded to DMC). Include `access_token` in the JSON for Bearer auth. ### Example MQTT payloads **GET plants:** ```json theme={"theme":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} { "access_token": "YOUR_BTP_ACCESS_TOKEN" } ``` **Start SFC:** ```json theme={"theme":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} { "access_token": "YOUR_BTP_ACCESS_TOKEN", "plant": "P001", "operation": "10", "resource": "RES01", "sfcs": ["SFC0001"] } ``` **GET order:** ```json theme={"theme":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} { "access_token": "YOUR_BTP_ACCESS_TOKEN", "plant": "P001", "order": "1000123" } ``` ## Configuration | Setting | Value | | - | - | | `BASE_ADDRESS` | `https://api.{regionHost}.dmc.cloud.sap` (example `eu20`; no trailing slash) | | Auth | BTP OAuth 2.0 client credentials → Bearer `access_token` | | Token URL | `https://{subdomain}.authentication.{region}.hana.ondemand.com/oauth/token` | | Content-Type | `application/json` | | SSL | required (`USE_SSL true`) | Region hosts commonly include `eu10`, `eu20`, `us10`, `us20`, and matching `test.*` hosts. Use the `public-api-endpoint` from your DM service key. ## Related * [REST API Connectors](/latest/lot-language/routes/rest-api-routes) * [REST API Apps](/latest/lot-language/routes/rest-api-apps) * [SAP Business Accelerator Hub — Digital Manufacturing](https://api.sap.com/package/SAPDigitalManufacturingCloud) * [SAP Help — APIs for SAP Digital Manufacturing](https://help.sap.com/docs/sap-digital-manufacturing) # SAP Field Service Management Source: https://docs.coreflux.org/latest/lot-language/routes/rest-api-apps/sap-field-service-management LoT connector for SAP Field Service Management: create service calls and activities from alarms, update equipment run hours and run CoreSQL queries Technical Preview ## Overview [SAP Field Service Management](https://www.sap.com/products/scm/field-service-management.html) (now documented as **SAP Field Service and Asset Management**) plans and runs field service: **service calls** (tickets), **activities** (the jobs technicians do) and **equipment** at customer sites. This connector lets the broker open a service call and an activity when a machine raises an alarm, put run hours and other machine data on the equipment, and close the loop with business actions, so planners and technicians see what the machine sees. The template has two connectors: * **SapFsmApi** (`SapFieldServiceManagementRestApi.lot`), a client connector on your cluster host with 24 events: 13 writes, 10 reads and 1 token event. It has no `DELETE` or `PUT` events; cancel actions are business actions, not deletes. * **SapFsmBusinessRuleReceiver** (`cmms_sap_fsm_webhook.lot`), an optional server connector on port `9012` that receives calls from a Business Rule **Webhook** action and publishes them on MQTT. > [!NOTE] > > SAP publishes the FSM API specs on the SAP Business Accelerator Hub, which needs an SAP login, so the events were picked by hand from the SAP Help Portal ([Data API v4](https://help.sap.com/docs/SAP_FIELD_SERVICE_MANAGEMENT/fsm_data_api/data-api-introduction.html), [Data Model](https://help.sap.com/docs/SAP_FIELD_SERVICE_MANAGEMENT/fsm_data_model/servicecalldto_v27.html), [Query API](https://help.sap.com/docs/SAP_FIELD_SERVICE_MANAGEMENT/fsm_query_api/query-api-intro.html), [Service API](https://help.sap.com/docs/SAP_FIELD_SERVICE_MANAGEMENT/fsm_service_api/supported-requests.html), [OAuth](https://help.sap.com/docs/SAP_FIELD_SERVICE_MANAGEMENT/fsm_access_api/get-access-token.html)) and the [SAP/fsm-sdk](https://github.com/SAP/fsm-sdk) source. ## Events | Event | Topic | Request | Kind | What it does | | - | - | - | - | - | | `GetToken` | `cmms/sapfsm/token/create` | `POST /api/oauth2/v2/token` | Auth | Get an OAuth 2.0 access token (client credentials, HTTP Basic with the FSM OAuth client id:secret). Reply access\_token, token\_type, expires\_in; send access\_token on every other event | | `ListServiceCalls` | `cmms/sapfsm/service-call/list` | `GET /api/data/v4/ServiceCall?dtos=ServiceCall.27&page={page}&pageSize={page_size}` | Read | Service calls, paged (default 20 per page, keep pageSize at 1000 or less). Reply data\[].serviceCall plus currentPage / lastPage | | `FindServiceCalls` | `cmms/sapfsm/service-call/find` | `GET /api/data/v4/ServiceCall?dtos=ServiceCall.27&query={query}&pageSize={page_size}` | Read | Service calls matching a URL-encoded Data API query condition, e.g. lastChanged>=\ for incremental polls | | `GetServiceCall` | `cmms/sapfsm/service-call/get` | `GET /api/data/v4/ServiceCall/{id}?dtos=ServiceCall.27` | Read | One service call by cloud id | | `GetServiceCallByExternalId` | `cmms/sapfsm/service-call/get-by-external-id` | `GET /api/data/v4/ServiceCall/externalId/{external_id}?dtos=ServiceCall.27` | Read | One service call by your externalId (e.g. the alarm id) | | `CreateServiceCall` | `cmms/sapfsm/service-call/create` | `POST /api/data/v4/ServiceCall?dtos=ServiceCall.27` | Write | Create a service call from an alarm; reply 201 \{data:\[\{serviceCall:\{id, code, lastChanged...}}]} | | `UpdateServiceCall` | `cmms/sapfsm/service-call/update` | `PATCH /api/data/v4/ServiceCall/{id}?dtos=ServiceCall.27&forceUpdate={force_update}` | Write | Patch service call fields; send the current lastChanged (optimistic lock) or force\_update true. Reply 201 | | `UpsertServiceCallByExternalId` | `cmms/sapfsm/service-call/upsert` | `PATCH /api/data/v4/ServiceCall/externalId/{external_id}?dtos=ServiceCall.27` | Write | Create or update the service call keyed by your externalId (one service call per alarm id; creating needs all mandatory fields) | | `FindActivities` | `cmms/sapfsm/activity/find` | `GET /api/data/v4/Activity?dtos=Activity.43&query={query}&pageSize={page_size}` | Read | Activities matching a URL-encoded Data API query condition | | `GetActivity` | `cmms/sapfsm/activity/get` | `GET /api/data/v4/Activity/{id}?dtos=Activity.43` | Read | One activity by cloud id (status, executionStage, responsibles, object -> service call) | | `CreateActivity` | `cmms/sapfsm/activity/create` | `POST /api/data/v4/Activity?dtos=Activity.43` | Write | Create an activity under a service call (object = \{objectId, objectType SERVICECALL}) | | `UpdateActivity` | `cmms/sapfsm/activity/update` | `PATCH /api/data/v4/Activity/{id}?dtos=Activity.43&forceUpdate={force_update}` | Write | Patch activity fields (send lastChanged or force\_update true). Use the Service Management actions to change planning state | | `FindEquipment` | `cmms/sapfsm/equipment/find` | `GET /api/data/v4/Equipment?dtos=Equipment.24&query={query}&pageSize={page_size}` | Read | Equipment matching a URL-encoded query condition (e.g. serialNumber='...') | | `GetEquipment` | `cmms/sapfsm/equipment/get` | `GET /api/data/v4/Equipment/{id}?dtos=Equipment.24` | Read | One equipment by cloud id (read lastChanged before a PATCH) | | `GetEquipmentByExternalId` | `cmms/sapfsm/equipment/get-by-external-id` | `GET /api/data/v4/Equipment/externalId/{external_id}?dtos=Equipment.24` | Read | One equipment by externalId (usually the S/4HANA or ERP equipment number) | | `UpdateEquipment` | `cmms/sapfsm/equipment/update` | `PATCH /api/data/v4/Equipment/{id}?dtos=Equipment.24&forceUpdate={force_update}` | Write | Patch equipment with machine data (remarks, status, UDF values). force\_update true skips the lastChanged lock | | `UpsertEquipmentByExternalId` | `cmms/sapfsm/equipment/upsert` | `PATCH /api/data/v4/Equipment/externalId/{external_id}?dtos=Equipment.24` | Write | Patch equipment by externalId, e.g. a run-hours UDF referenced by its UDF meta externalId | | `Query` | `cmms/sapfsm/query/run` | `POST /api/query/v1?dtos={dtos}&page={page}&pageSize={page_size}` | Read | Read-only CoreSQL query (POST with \{"query": ...}); dtos lists every DTO used, separated by ; | | `CreateServiceCallWithActivities` | `cmms/sapfsm/service-call-tree/create` | `POST /service-management/api/v2/composite-tree/service-calls` | Write | Service Management API composite tree: create a service call and its activities in one request | | `CancelServiceCall` | `cmms/sapfsm/service-call/cancel` | `POST /service-management/api/v2/servicecalls/{id}/actions/cancel` | Write | Business action: cancel a service call (not a delete) | | `CompleteServiceCall` | `cmms/sapfsm/service-call/technically-complete` | `POST /service-management/api/v2/servicecalls/{id}/actions/technically-complete` | Write | Business action: mark a service call technically complete | | `ReleaseActivity` | `cmms/sapfsm/activity/release` | `POST /service-management/api/v2/activities/{id}/actions/release` | Write | Business action: release a planned activity to the technician | | `CloseActivity` | `cmms/sapfsm/activity/close` | `POST /service-management/api/v2/activities/{id}/actions/close` | Write | Business action: close an activity | | `CancelActivity` | `cmms/sapfsm/activity/cancel` | `POST /service-management/api/v2/activities/{id}/actions/cancel` | Write | Business action: cancel an activity (not a delete) | Every reply is published on the event topic plus `/response`. Data API replies wrap objects as `{"data": [{"serviceCall": {...}}]}` (lower-case DTO name). ## Prerequisites * An SAP FSM company and its **cluster**: `eu`, `de`, `us` or `au` (China uses `cn.fsm.sapcloud.cn`). Change `BASE_ADDRESS` to `https://.fsm.cloud.sap`. The older `coresuite.com` / `coresystems.net` hosts are being migrated to `fsm.cloud.sap`. * An **OAuth client** created in Account Admin (client id + secret) with a permission group that can read and write service calls, activities and equipment and run the Service Management actions. * The numeric **account id** and **company id** (Account Admin). * Outbound HTTPS from the broker to the cluster host. * For Business Rule webhooks only: a public HTTPS URL that forwards to the broker's port `9012`. ## Authentication > [!WARNING] > > The client secret and every access token act with the client's permission group. Store `base64(client_id:client_secret)` in the Hub secret `SAP_FSM_BASIC_AUTH` (and the ids in `SAP_FSM_ACCOUNT_ID` / `SAP_FSM_COMPANY_ID`), read them with `GET SECRET`, and never put them or a token in a connector file. `GetToken` posts `grant_type=client_credentials` to `/api/oauth2/v2/token` with HTTP Basic: ```json theme={"theme":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} { "basic": "", "form": "grant_type=client_credentials" } ``` The reply carries `access_token`, `token_type` and `expires_in`. Every other event sends `Authorization: Bearer {value.json.access_token}`, `X-Account-ID` and `X-Company-ID` from the payload, and the fixed `X-Client-ID: custom-coreflux-broker` / `X-Client-Version: 1.0` (SAP asks custom clients to use a `custom-` prefix). Call `GetToken` again before `expires_in` runs out. Answers seen from the real hosts without valid credentials: a fake client returns `401 {"error":"unauthorized","error_description":"Bad credentials"}`, a Data API call without a token `401 INVALID_LOGIN`, and any call with a fake token `401 {"error":"invalid_token"}`. ## Payload conventions Each payload carries the context and the event's own fields; the request body comes from `body`: ```json theme={"theme":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} { "access_token": "", "account_id": "", "company_id": "", "id": "", "force_update": "false", "body": { "lastChanged": 1759737600000, "priority": "LOW", "remarks": "Alarm cleared (Coreflux)" } } ``` * `id` is the 32-character FSM id; `external_id` is your id (alarm id, ERP equipment number). Both go into the path, so pass only values you trust. * `query` is a URL-encoded Data API condition, for example `lastChanged%3E%3D1759737600000` (`lastChanged>=1759737600000`) for incremental polls. * Updates use **optimistic locking**: send the current `lastChanged` from a GET, or set `force_update` to `"true"`. * `PATCH .../externalId/` creates the object when it does not exist yet (send all mandatory fields). * Pages: default 20, `page_size` 1000 at most; replies carry `currentPage`, `lastPage`, `totalObjectCount`. * DTO versions are pinned: `ServiceCall.27`, `Activity.43`, `Equipment.24`. ### Alarm to service call ```json theme={"theme":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} { "access_token": "", "account_id": "", "company_id": "", "body": { "subject": "Alarm TT-101 = 92.4 C on compressor C-7 (Coreflux)", "priority": "HIGH", "businessPartner": "", "equipments": [ "" ], "startDateTime": "2026-10-06T08:00:00Z", "dueDateTime": "2026-10-07T17:00:00Z", "typeCode": "", "statusCode": "", "originCode": "", "remarks": "Opened by Coreflux broker" } } ``` Then attach the job (`object` points to the service call): ```json theme={"theme":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} { "access_token": "", "account_id": "", "company_id": "", "body": { "subject": "Inspect compressor C-7", "type": "ASSIGNMENT", "object": { "objectId": "", "objectType": "SERVICECALL" }, "businessPartner": "", "equipment": "", "startDateTime": "2026-10-06T09:00:00Z", "endDateTime": "2026-10-06T10:00:00Z", "durationInMinutes": 60, "remarks": "Alarm TT-101 (Coreflux)" } } ``` Or create both in one call with the Service Management API: ```json theme={"theme":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} { "access_token": "", "account_id": "", "company_id": "", "body": { "subject": "Alarm TT-101 on compressor C-7 (Coreflux)", "priority": "HIGH", "businessPartner": { "id": "" }, "activities": [ { "subject": "Inspect compressor C-7", "durationInMinutes": 60 } ] } } ``` ### Machine data on equipment ```json theme={"theme":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} { "access_token": "", "account_id": "", "company_id": "", "external_id": "", "body": { "udfValues": [ { "meta": { "externalId": "" }, "value": "1287.5" } ] } } ``` `udfValues` reference a user-defined field you create for the equipment in FSM (here by its UDF meta `externalId`). `UpdateEquipment` patches standard fields such as `remarks` by FSM id. ### Query API ```json theme={"theme":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} { "access_token": "", "account_id": "", "company_id": "", "dtos": "ServiceCall.27;Equipment.24", "page": "1", "page_size": "100", "body": { "query": "SELECT sc.id, sc.code, sc.subject, sc.priority FROM ServiceCall sc WHERE sc.priority = 'HIGH'" } } ``` `dtos` must list every DTO the CoreSQL statement uses, separated by `;`. ## Business Rule receiver FSM has no webhook subscription API. Instead, a **Business Rule** (Admin > Company > Business Rules) with a **Webhook** action calls any URL with your headers and body when a service call, activity or other object is created or changed. Point it at: | Event | Path | Publishes on | | - | - | - | | `NotifyServiceCalls` | `POST /sapfsm/notify/service-calls` | `cmms/sapfsm/service-call-notification/received` | | `NotifyActivities` | `POST /sapfsm/notify/activities` | `cmms/sapfsm/activity-notification/received` | | `NotifyEvents` | `POST /sapfsm/notify/events` | `cmms/sapfsm/notification/received` | > [!WARNING] > > Deploying the receiver opens port `9012`. FSM calls out from the SAP cloud, so put a reverse proxy or tunnel with a public TLS certificate in front of it, allow SAP's FSM egress addresses and check a shared secret header there: the connector does not authenticate callers. ## Connector ```lot theme={"theme":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} # Coreflux REST_API template — SAP Field Service Management (SAP Field Service and Asset Management) (beta): get an # OAuth 2.0 token, create service calls from alarms (Data API v4 ServiceCall.27), upsert them by your own alarm id, # find / read / patch service calls, activities (Activity.43) and equipment (Equipment.24, e.g. run hours as a UDF), # run read-only CoreSQL queries (Query API v1), create a service call with activities in one call and run the # Service Management API v2 business actions (cancel / technically complete service calls, release / close / cancel # activities). # the Business Accelerator Hub specs need an SAP login). Credentials are placeholders only. # Docs: https://docs.coreflux.org/latest/lot-language/routes/rest-api-apps/sap-field-service-management # # Host: your cluster, https://YOUR_CLUSTER.fsm.cloud.sap with YOUR_CLUSTER = eu | de | us | au (China: # https://cn.fsm.sapcloud.cn). The older eu.coresuite.com / de.coresystems.net hosts are being migrated to # fsm.cloud.sap. Use the cluster_url your account shows in Account Admin. # Auth: OAuth 2.0 client credentials. GetToken posts grant_type=client_credentials ({value.json.form}) to # /api/oauth2/v2/token with Authorization: Basic {value.json.basic} (base64 of client_id:client_secret; keep it # in the Hub secret SAP_FSM_BASIC_AUTH). Every other event sends Authorization: Bearer {value.json.access_token} # plus X-Account-ID {value.json.account_id} and X-Company-ID {value.json.company_id} (numeric, from Account Admin) # and X-Client-ID custom-coreflux-broker / X-Client-Version 1.0. Tokens expire (expires_in): call GetToken again. # Live (2026-10-06, eu): fake Basic -> 401 {"error":"unauthorized","error_description":"Bad credentials"}; # Data API without a token -> 401 INVALID_LOGIN; fake Bearer -> 401 invalid_token. # Objects: Data API v4 DTO versions pinned to the newest on the SAP Help Portal on 2026-10-06 (ServiceCall.27, # Activity.43, Equipment.24; SAP/fsm-sdk 4.0.1 still ships 26 / 39 / 23). Status, type, origin and priority codes # are company-configurable: read them from an existing service call first. # Payload fields (control fields stay out of bodies; request bodies come from {value.json.body}): # access_token, account_id, company_id, id, external_id, query, page, page_size, force_update, dtos, basic, form, body # query is a URL-encoded Data API condition (lastChanged%3E%3D1759737600000 = lastChanged>=...). id / external_id # go into the path: pass FSM ids or external ids you trust, never raw user input. # CreateServiceCall body = {"subject": "...", "priority": "HIGH", "businessPartner": "", "equipments": [""]} # UpdateServiceCall id = , force_update = "false", body = {"lastChanged": , "priority": "LOW"} # UpsertEquipmentByExternalId external_id = , body = {"udfValues": [{"meta": {"externalId": "RUN_HOURS"}, "value": "1287.5"}]} # Query dtos = "ServiceCall.27", body = {"query": "SELECT sc.id, sc.subject FROM ServiceCall sc"} # Errors: 401 (token), 403 (permission group), 404, 409 / CA-* codes (e.g. stale lastChanged); Data API pages are # limited to 1000 objects and 100,000 objects per pagination. Writes run the company's Business Rules and sync to ERP. # DEFINE CONNECTOR SapFsmApi WITH TYPE REST_API ADD REST_API_CONFIG WITH BASE_ADDRESS "https://YOUR_CLUSTER.fsm.cloud.sap" WITH ENABLE_CLIENT "true" WITH USE_SSL "true" WITH REQUEST_TIMEOUT '30' WITH DEFAULT_CONTENT_TYPE "application/json" # Get an OAuth 2.0 access token (client credentials, HTTP Basic with the FSM OAuth client id:secret). Reply access_token, token_type, expires_in; send access_token on every other event ADD EVENT GetToken WITH SOURCE_TOPIC "cmms/sapfsm/token/create" WITH DESTINATION_TOPIC "cmms/sapfsm/token/create/response" WITH METHOD "POST" WITH ENDPOINT "/api/oauth2/v2/token" WITH HEADERS ADD "Accept" "application/json" ADD "Authorization" "Basic {value.json.basic}" ADD "Content-Type" "application/x-www-form-urlencoded" WITH BODY "{value.json.form}" # Service calls, paged (default 20 per page, keep pageSize at 1000 or less). Reply data[].serviceCall plus currentPage / lastPage ADD EVENT ListServiceCalls WITH SOURCE_TOPIC "cmms/sapfsm/service-call/list" WITH DESTINATION_TOPIC "cmms/sapfsm/service-call/list/response" WITH METHOD "GET" WITH ENDPOINT "/api/data/v4/ServiceCall?dtos=ServiceCall.27&page={value.json.page}&pageSize={value.json.page_size}" WITH HEADERS ADD "Accept" "application/json" ADD "Authorization" "Bearer {value.json.access_token}" ADD "X-Account-ID" "{value.json.account_id}" ADD "X-Company-ID" "{value.json.company_id}" ADD "X-Client-ID" "custom-coreflux-broker" ADD "X-Client-Version" "1.0" # Service calls matching a URL-encoded Data API query condition, e.g. lastChanged>= for incremental polls ADD EVENT FindServiceCalls WITH SOURCE_TOPIC "cmms/sapfsm/service-call/find" WITH DESTINATION_TOPIC "cmms/sapfsm/service-call/find/response" WITH METHOD "GET" WITH ENDPOINT "/api/data/v4/ServiceCall?dtos=ServiceCall.27&query={value.json.query}&pageSize={value.json.page_size}" WITH HEADERS ADD "Accept" "application/json" ADD "Authorization" "Bearer {value.json.access_token}" ADD "X-Account-ID" "{value.json.account_id}" ADD "X-Company-ID" "{value.json.company_id}" ADD "X-Client-ID" "custom-coreflux-broker" ADD "X-Client-Version" "1.0" # One service call by cloud id ADD EVENT GetServiceCall WITH SOURCE_TOPIC "cmms/sapfsm/service-call/get" WITH DESTINATION_TOPIC "cmms/sapfsm/service-call/get/response" WITH METHOD "GET" WITH ENDPOINT "/api/data/v4/ServiceCall/{value.json.id}?dtos=ServiceCall.27" WITH HEADERS ADD "Accept" "application/json" ADD "Authorization" "Bearer {value.json.access_token}" ADD "X-Account-ID" "{value.json.account_id}" ADD "X-Company-ID" "{value.json.company_id}" ADD "X-Client-ID" "custom-coreflux-broker" ADD "X-Client-Version" "1.0" # One service call by your externalId (e.g. the alarm id) ADD EVENT GetServiceCallByExternalId WITH SOURCE_TOPIC "cmms/sapfsm/service-call/get-by-external-id" WITH DESTINATION_TOPIC "cmms/sapfsm/service-call/get-by-external-id/response" WITH METHOD "GET" WITH ENDPOINT "/api/data/v4/ServiceCall/externalId/{value.json.external_id}?dtos=ServiceCall.27" WITH HEADERS ADD "Accept" "application/json" ADD "Authorization" "Bearer {value.json.access_token}" ADD "X-Account-ID" "{value.json.account_id}" ADD "X-Company-ID" "{value.json.company_id}" ADD "X-Client-ID" "custom-coreflux-broker" ADD "X-Client-Version" "1.0" # WRITE: Create a service call from an alarm; reply 201 {data:[{serviceCall:{id, code, lastChanged...}}]} ADD EVENT CreateServiceCall WITH SOURCE_TOPIC "cmms/sapfsm/service-call/create" WITH DESTINATION_TOPIC "cmms/sapfsm/service-call/create/response" WITH METHOD "POST" WITH ENDPOINT "/api/data/v4/ServiceCall?dtos=ServiceCall.27" WITH HEADERS ADD "Accept" "application/json" ADD "Authorization" "Bearer {value.json.access_token}" ADD "X-Account-ID" "{value.json.account_id}" ADD "X-Company-ID" "{value.json.company_id}" ADD "X-Client-ID" "custom-coreflux-broker" ADD "X-Client-Version" "1.0" ADD "Content-Type" "application/json" WITH BODY "{value.json.body}" # WRITE: Patch service call fields; send the current lastChanged (optimistic lock) or force_update true. Reply 201 ADD EVENT UpdateServiceCall WITH SOURCE_TOPIC "cmms/sapfsm/service-call/update" WITH DESTINATION_TOPIC "cmms/sapfsm/service-call/update/response" WITH METHOD "PATCH" WITH ENDPOINT "/api/data/v4/ServiceCall/{value.json.id}?dtos=ServiceCall.27&forceUpdate={value.json.force_update}" WITH HEADERS ADD "Accept" "application/json" ADD "Authorization" "Bearer {value.json.access_token}" ADD "X-Account-ID" "{value.json.account_id}" ADD "X-Company-ID" "{value.json.company_id}" ADD "X-Client-ID" "custom-coreflux-broker" ADD "X-Client-Version" "1.0" ADD "Content-Type" "application/json" WITH BODY "{value.json.body}" # WRITE: Create or update the service call keyed by your externalId (one service call per alarm id; creating needs all mandatory fields) ADD EVENT UpsertServiceCallByExternalId WITH SOURCE_TOPIC "cmms/sapfsm/service-call/upsert" WITH DESTINATION_TOPIC "cmms/sapfsm/service-call/upsert/response" WITH METHOD "PATCH" WITH ENDPOINT "/api/data/v4/ServiceCall/externalId/{value.json.external_id}?dtos=ServiceCall.27" WITH HEADERS ADD "Accept" "application/json" ADD "Authorization" "Bearer {value.json.access_token}" ADD "X-Account-ID" "{value.json.account_id}" ADD "X-Company-ID" "{value.json.company_id}" ADD "X-Client-ID" "custom-coreflux-broker" ADD "X-Client-Version" "1.0" ADD "Content-Type" "application/json" WITH BODY "{value.json.body}" # Activities matching a URL-encoded Data API query condition ADD EVENT FindActivities WITH SOURCE_TOPIC "cmms/sapfsm/activity/find" WITH DESTINATION_TOPIC "cmms/sapfsm/activity/find/response" WITH METHOD "GET" WITH ENDPOINT "/api/data/v4/Activity?dtos=Activity.43&query={value.json.query}&pageSize={value.json.page_size}" WITH HEADERS ADD "Accept" "application/json" ADD "Authorization" "Bearer {value.json.access_token}" ADD "X-Account-ID" "{value.json.account_id}" ADD "X-Company-ID" "{value.json.company_id}" ADD "X-Client-ID" "custom-coreflux-broker" ADD "X-Client-Version" "1.0" # One activity by cloud id (status, executionStage, responsibles, object -> service call) ADD EVENT GetActivity WITH SOURCE_TOPIC "cmms/sapfsm/activity/get" WITH DESTINATION_TOPIC "cmms/sapfsm/activity/get/response" WITH METHOD "GET" WITH ENDPOINT "/api/data/v4/Activity/{value.json.id}?dtos=Activity.43" WITH HEADERS ADD "Accept" "application/json" ADD "Authorization" "Bearer {value.json.access_token}" ADD "X-Account-ID" "{value.json.account_id}" ADD "X-Company-ID" "{value.json.company_id}" ADD "X-Client-ID" "custom-coreflux-broker" ADD "X-Client-Version" "1.0" # WRITE: Create an activity under a service call (object = {objectId, objectType SERVICECALL}) ADD EVENT CreateActivity WITH SOURCE_TOPIC "cmms/sapfsm/activity/create" WITH DESTINATION_TOPIC "cmms/sapfsm/activity/create/response" WITH METHOD "POST" WITH ENDPOINT "/api/data/v4/Activity?dtos=Activity.43" WITH HEADERS ADD "Accept" "application/json" ADD "Authorization" "Bearer {value.json.access_token}" ADD "X-Account-ID" "{value.json.account_id}" ADD "X-Company-ID" "{value.json.company_id}" ADD "X-Client-ID" "custom-coreflux-broker" ADD "X-Client-Version" "1.0" ADD "Content-Type" "application/json" WITH BODY "{value.json.body}" # WRITE: Patch activity fields (send lastChanged or force_update true). Use the Service Management actions to change planning state ADD EVENT UpdateActivity WITH SOURCE_TOPIC "cmms/sapfsm/activity/update" WITH DESTINATION_TOPIC "cmms/sapfsm/activity/update/response" WITH METHOD "PATCH" WITH ENDPOINT "/api/data/v4/Activity/{value.json.id}?dtos=Activity.43&forceUpdate={value.json.force_update}" WITH HEADERS ADD "Accept" "application/json" ADD "Authorization" "Bearer {value.json.access_token}" ADD "X-Account-ID" "{value.json.account_id}" ADD "X-Company-ID" "{value.json.company_id}" ADD "X-Client-ID" "custom-coreflux-broker" ADD "X-Client-Version" "1.0" ADD "Content-Type" "application/json" WITH BODY "{value.json.body}" # Equipment matching a URL-encoded query condition (e.g. serialNumber='...') ADD EVENT FindEquipment WITH SOURCE_TOPIC "cmms/sapfsm/equipment/find" WITH DESTINATION_TOPIC "cmms/sapfsm/equipment/find/response" WITH METHOD "GET" WITH ENDPOINT "/api/data/v4/Equipment?dtos=Equipment.24&query={value.json.query}&pageSize={value.json.page_size}" WITH HEADERS ADD "Accept" "application/json" ADD "Authorization" "Bearer {value.json.access_token}" ADD "X-Account-ID" "{value.json.account_id}" ADD "X-Company-ID" "{value.json.company_id}" ADD "X-Client-ID" "custom-coreflux-broker" ADD "X-Client-Version" "1.0" # One equipment by cloud id (read lastChanged before a PATCH) ADD EVENT GetEquipment WITH SOURCE_TOPIC "cmms/sapfsm/equipment/get" WITH DESTINATION_TOPIC "cmms/sapfsm/equipment/get/response" WITH METHOD "GET" WITH ENDPOINT "/api/data/v4/Equipment/{value.json.id}?dtos=Equipment.24" WITH HEADERS ADD "Accept" "application/json" ADD "Authorization" "Bearer {value.json.access_token}" ADD "X-Account-ID" "{value.json.account_id}" ADD "X-Company-ID" "{value.json.company_id}" ADD "X-Client-ID" "custom-coreflux-broker" ADD "X-Client-Version" "1.0" # One equipment by externalId (usually the S/4HANA or ERP equipment number) ADD EVENT GetEquipmentByExternalId WITH SOURCE_TOPIC "cmms/sapfsm/equipment/get-by-external-id" WITH DESTINATION_TOPIC "cmms/sapfsm/equipment/get-by-external-id/response" WITH METHOD "GET" WITH ENDPOINT "/api/data/v4/Equipment/externalId/{value.json.external_id}?dtos=Equipment.24" WITH HEADERS ADD "Accept" "application/json" ADD "Authorization" "Bearer {value.json.access_token}" ADD "X-Account-ID" "{value.json.account_id}" ADD "X-Company-ID" "{value.json.company_id}" ADD "X-Client-ID" "custom-coreflux-broker" ADD "X-Client-Version" "1.0" # WRITE: Patch equipment with machine data (remarks, status, UDF values). force_update true skips the lastChanged lock ADD EVENT UpdateEquipment WITH SOURCE_TOPIC "cmms/sapfsm/equipment/update" WITH DESTINATION_TOPIC "cmms/sapfsm/equipment/update/response" WITH METHOD "PATCH" WITH ENDPOINT "/api/data/v4/Equipment/{value.json.id}?dtos=Equipment.24&forceUpdate={value.json.force_update}" WITH HEADERS ADD "Accept" "application/json" ADD "Authorization" "Bearer {value.json.access_token}" ADD "X-Account-ID" "{value.json.account_id}" ADD "X-Company-ID" "{value.json.company_id}" ADD "X-Client-ID" "custom-coreflux-broker" ADD "X-Client-Version" "1.0" ADD "Content-Type" "application/json" WITH BODY "{value.json.body}" # WRITE: Patch equipment by externalId, e.g. a run-hours UDF referenced by its UDF meta externalId ADD EVENT UpsertEquipmentByExternalId WITH SOURCE_TOPIC "cmms/sapfsm/equipment/upsert" WITH DESTINATION_TOPIC "cmms/sapfsm/equipment/upsert/response" WITH METHOD "PATCH" WITH ENDPOINT "/api/data/v4/Equipment/externalId/{value.json.external_id}?dtos=Equipment.24" WITH HEADERS ADD "Accept" "application/json" ADD "Authorization" "Bearer {value.json.access_token}" ADD "X-Account-ID" "{value.json.account_id}" ADD "X-Company-ID" "{value.json.company_id}" ADD "X-Client-ID" "custom-coreflux-broker" ADD "X-Client-Version" "1.0" ADD "Content-Type" "application/json" WITH BODY "{value.json.body}" # Read-only CoreSQL query (POST with {"query": ...}); dtos lists every DTO used, separated by ; ADD EVENT Query WITH SOURCE_TOPIC "cmms/sapfsm/query/run" WITH DESTINATION_TOPIC "cmms/sapfsm/query/run/response" WITH METHOD "POST" WITH ENDPOINT "/api/query/v1?dtos={value.json.dtos}&page={value.json.page}&pageSize={value.json.page_size}" WITH HEADERS ADD "Accept" "application/json" ADD "Authorization" "Bearer {value.json.access_token}" ADD "X-Account-ID" "{value.json.account_id}" ADD "X-Company-ID" "{value.json.company_id}" ADD "X-Client-ID" "custom-coreflux-broker" ADD "X-Client-Version" "1.0" ADD "Content-Type" "application/json" WITH BODY "{value.json.body}" # WRITE: Service Management API composite tree: create a service call and its activities in one request ADD EVENT CreateServiceCallWithActivities WITH SOURCE_TOPIC "cmms/sapfsm/service-call-tree/create" WITH DESTINATION_TOPIC "cmms/sapfsm/service-call-tree/create/response" WITH METHOD "POST" WITH ENDPOINT "/service-management/api/v2/composite-tree/service-calls" WITH HEADERS ADD "Accept" "application/json" ADD "Authorization" "Bearer {value.json.access_token}" ADD "X-Account-ID" "{value.json.account_id}" ADD "X-Company-ID" "{value.json.company_id}" ADD "X-Client-ID" "custom-coreflux-broker" ADD "X-Client-Version" "1.0" ADD "Content-Type" "application/json" WITH BODY "{value.json.body}" # WRITE: Business action: cancel a service call (not a delete) ADD EVENT CancelServiceCall WITH SOURCE_TOPIC "cmms/sapfsm/service-call/cancel" WITH DESTINATION_TOPIC "cmms/sapfsm/service-call/cancel/response" WITH METHOD "POST" WITH ENDPOINT "/service-management/api/v2/servicecalls/{value.json.id}/actions/cancel" WITH HEADERS ADD "Accept" "application/json" ADD "Authorization" "Bearer {value.json.access_token}" ADD "X-Account-ID" "{value.json.account_id}" ADD "X-Company-ID" "{value.json.company_id}" ADD "X-Client-ID" "custom-coreflux-broker" ADD "X-Client-Version" "1.0" ADD "Content-Type" "application/json" WITH BODY "{value.json.body}" # WRITE: Business action: mark a service call technically complete ADD EVENT CompleteServiceCall WITH SOURCE_TOPIC "cmms/sapfsm/service-call/technically-complete" WITH DESTINATION_TOPIC "cmms/sapfsm/service-call/technically-complete/response" WITH METHOD "POST" WITH ENDPOINT "/service-management/api/v2/servicecalls/{value.json.id}/actions/technically-complete" WITH HEADERS ADD "Accept" "application/json" ADD "Authorization" "Bearer {value.json.access_token}" ADD "X-Account-ID" "{value.json.account_id}" ADD "X-Company-ID" "{value.json.company_id}" ADD "X-Client-ID" "custom-coreflux-broker" ADD "X-Client-Version" "1.0" ADD "Content-Type" "application/json" WITH BODY "{value.json.body}" # WRITE: Business action: release a planned activity to the technician ADD EVENT ReleaseActivity WITH SOURCE_TOPIC "cmms/sapfsm/activity/release" WITH DESTINATION_TOPIC "cmms/sapfsm/activity/release/response" WITH METHOD "POST" WITH ENDPOINT "/service-management/api/v2/activities/{value.json.id}/actions/release" WITH HEADERS ADD "Accept" "application/json" ADD "Authorization" "Bearer {value.json.access_token}" ADD "X-Account-ID" "{value.json.account_id}" ADD "X-Company-ID" "{value.json.company_id}" ADD "X-Client-ID" "custom-coreflux-broker" ADD "X-Client-Version" "1.0" ADD "Content-Type" "application/json" WITH BODY "{value.json.body}" # WRITE: Business action: close an activity ADD EVENT CloseActivity WITH SOURCE_TOPIC "cmms/sapfsm/activity/close" WITH DESTINATION_TOPIC "cmms/sapfsm/activity/close/response" WITH METHOD "POST" WITH ENDPOINT "/service-management/api/v2/activities/{value.json.id}/actions/close" WITH HEADERS ADD "Accept" "application/json" ADD "Authorization" "Bearer {value.json.access_token}" ADD "X-Account-ID" "{value.json.account_id}" ADD "X-Company-ID" "{value.json.company_id}" ADD "X-Client-ID" "custom-coreflux-broker" ADD "X-Client-Version" "1.0" ADD "Content-Type" "application/json" WITH BODY "{value.json.body}" # WRITE: Business action: cancel an activity (not a delete) ADD EVENT CancelActivity WITH SOURCE_TOPIC "cmms/sapfsm/activity/cancel" WITH DESTINATION_TOPIC "cmms/sapfsm/activity/cancel/response" WITH METHOD "POST" WITH ENDPOINT "/service-management/api/v2/activities/{value.json.id}/actions/cancel" WITH HEADERS ADD "Accept" "application/json" ADD "Authorization" "Bearer {value.json.access_token}" ADD "X-Account-ID" "{value.json.account_id}" ADD "X-Company-ID" "{value.json.company_id}" ADD "X-Client-ID" "custom-coreflux-broker" ADD "X-Client-Version" "1.0" ADD "Content-Type" "application/json" WITH BODY "{value.json.body}" ``` ```lot theme={"theme":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} # SAP FSM Business Rule receiver — REST_API route (server mode, beta, companion of SapFieldServiceManagementRestApi.lot) # Receives the HTTP calls that an SAP Field Service Management Business Rule makes with its 'Webhook' action # (Admin > Company > Business Rules: trigger on create / update of a ServiceCall, Activity, Equipment ..., action # Webhook, method POST, your URL, header + JSON body you define) and publishes the body on MQTT, so changes made # in FSM reach the broker without polling. Opt-in: deploying it opens an HTTP port. # Docs: https://docs.coreflux.org/latest/lot-language/routes/rest-api-apps/sap-field-service-management # # FSM calls out from the SAP cloud: put a reverse proxy or tunnel with a public TLS certificate in front of # SERVER_PORT, allow the SAP FSM cloud egress addresses ('Incoming firewall configuration from SAP Field Service # Cloud') and check a shared secret header there: this route does not authenticate callers. The body is # whatever your Business Rule sends (e.g. ${serviceCall} variables); treat it as JSON you defined. DEFINE CONNECTOR SapFsmBusinessRuleReceiver WITH TYPE REST_API ADD REST_API_CONFIG WITH ENABLE_SERVER "true" WITH SERVER_PORT '9012' WITH DEFAULT_CONTENT_TYPE "application/json" # Business Rule Webhook action (POST https:///sapfsm/notify/service-calls) when a service call is created or changes ADD EVENT NotifyServiceCalls WITH ENDPOINT "/sapfsm/notify/service-calls" WITH METHOD "POST" WITH DESTINATION_TOPIC "cmms/sapfsm/service-call-notification/received" # Same for activities (assignment, release, checkout, close) ADD EVENT NotifyActivities WITH ENDPOINT "/sapfsm/notify/activities" WITH METHOD "POST" WITH DESTINATION_TOPIC "cmms/sapfsm/activity-notification/received" # Any other Business Rule webhook body (equipment, checklists). Filter on the body in a LoT action ADD EVENT NotifyEvents WITH ENDPOINT "/sapfsm/notify/events" WITH METHOD "POST" WITH DESTINATION_TOPIC "cmms/sapfsm/notification/received" ``` ## Example actions ```lot theme={"theme":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} # Example actions for the SapFsmApi route (docs page). Not part of the route template. # Secrets: SAP_FSM_BASIC_AUTH (base64 of :, from Account Admin > Clients), # SAP_FSM_ACCOUNT_ID and SAP_FSM_COMPANY_ID (numeric ids from Account Admin). The access token is kept on the # internal topic internal/sapfsm/access_token. Replace the <...> placeholders (business partner, equipment ids). DEFINE ACTION SapFsmGetToken ON EVERY 30 MINUTES DO SET "basic" WITH GET SECRET "SAP_FSM_BASIC_AUTH" PUBLISH TOPIC "cmms/sapfsm/token/create" WITH { "basic": {basic}, "form": "grant_type=client_credentials" } DEFINE ACTION SapFsmKeepToken ON TOPIC "cmms/sapfsm/token/create/response" DO SET "tok" WITH (GET JSON "access_token" IN PAYLOAD AS STRING) KEEP TOPIC "internal/sapfsm/access_token" WITH {tok} DEFINE ACTION SapFsmServiceCallFromAlarm ON TOPIC "porto/line1/compressor/alarm/raised" DO SET "tok" WITH GET TOPIC "internal/sapfsm/access_token" AS STRING SET "acc" WITH GET SECRET "SAP_FSM_ACCOUNT_ID" SET "comp" WITH GET SECRET "SAP_FSM_COMPANY_ID" SET "alarm" WITH (GET JSON "alarm" IN PAYLOAD AS STRING) SET "value" WITH (GET JSON "value" IN PAYLOAD AS STRING) SET "subj" WITH "Coreflux alarm " + {alarm} + " (value " + {value} + ")" PUBLISH TOPIC "cmms/sapfsm/service-call/create" WITH { "access_token": {tok}, "account_id": {acc}, "company_id": {comp}, "body": { "subject": {subj}, "priority": "HIGH", "businessPartner": "", "equipments": [""] } } DEFINE ACTION SapFsmRunHoursToEquipment ON TOPIC "porto/line1/press01/run-hours" DO SET "tok" WITH GET TOPIC "internal/sapfsm/access_token" AS STRING SET "acc" WITH GET SECRET "SAP_FSM_ACCOUNT_ID" SET "comp" WITH GET SECRET "SAP_FSM_COMPANY_ID" SET "hours" WITH PAYLOAD AS STRING PUBLISH TOPIC "cmms/sapfsm/equipment/upsert" WITH { "access_token": {tok}, "account_id": {acc}, "company_id": {comp}, "external_id": "", "body": { "udfValues": [ { "meta": { "externalId": "RUN_HOURS" }, "value": {hours} } ] } } DEFINE ACTION SapFsmPollChangedServiceCalls ON EVERY 15 MINUTES DO SET "tok" WITH GET TOPIC "internal/sapfsm/access_token" AS STRING SET "acc" WITH GET SECRET "SAP_FSM_ACCOUNT_ID" SET "comp" WITH GET SECRET "SAP_FSM_COMPANY_ID" PUBLISH TOPIC "cmms/sapfsm/query/run" WITH { "access_token": {tok}, "account_id": {acc}, "company_id": {comp}, "dtos": "ServiceCall.27", "page": "1", "page_size": "100", "body": { "query": "SELECT sc.id, sc.code, sc.subject, sc.priority, sc.statusName FROM ServiceCall sc WHERE sc.priority = 'HIGH'" } } ``` ## Notes * Writes run the company's Business Rules and, if connected, sync to SAP S/4HANA or ECC. * Status, type, origin and priority codes and UDFs are company-specific: read an existing service call first and copy its codes. * SAP's own `fsm-sdk` 4.0.1 still pins `ServiceCall.26`, `Activity.39`, `Equipment.23`. If your company rejects the newer versions, change the `dtos=` values in the endpoints. * The Service API pages also list `X-Account-Name` and `X-Company-Name`. The connector sends only the ids, like SAP's SDK; add the name headers if a business action answers `400`. * For Service Management calls the SAP Help pages use `/service-management/api/v2/...`; the SDK calls `/service-management/v2/...`. Both reach the API gateway; the connector uses the documented form. ## Not covered Deletes, full PUT replacement, the Data API batch endpoint, attachments, planning actions that need technician or team ids (plan, replan, reschedule, unassign), bulk actions, approvals and other DTOs (business partners, persons, service assignments, time efforts, materials, checklists). All use the same headers; add events in the same style. ## Related * [REST API connectors](/latest/lot-language/routes/rest-api-routes) * [REST API app templates](/latest/lot-language/routes/rest-api-apps/index) * [SAP FSM Data API v4](https://help.sap.com/docs/SAP_FIELD_SERVICE_MANAGEMENT/fsm_data_api/data-api-introduction.html) * [SAP FSM Service API](https://help.sap.com/docs/SAP_FIELD_SERVICE_MANAGEMENT/fsm_service_api/supported-requests.html) * [SAP/fsm-sdk on GitHub](https://github.com/SAP/fsm-sdk) # SAP S/4HANA Cloud Source: https://docs.coreflux.org/latest/lot-language/routes/rest-api-apps/sap-s4hana-cloud Client-mode REST API connector for a curated S/4HANA Cloud OData slice: production orders, confirmations, goods movements, and maintenance notifications Technical Preview ## Overview A client-mode connector for a **curated manufacturing and plant-maintenance** slice of [SAP S/4HANA Cloud](https://api.sap.com/products/SAPS4HANACloud/apis/packages) OData APIs on the Business Accelerator Hub: | Catalog connector event | OData service | Entity set | | - | - | - | | GET production orders by plant | `API_PRODUCTION_ORDER_2_SRV` | `A_ProductionOrder_2` | | POST order confirmation (yield, scrap, time) | `API_PROD_ORDER_CONFIRMATION_2_SRV` | `ProdnOrdConf2` | | POST goods movement | `API_MATERIAL_DOCUMENT_SRV` | `A_MaterialDocumentHeader` | | POST maintenance notification from alarm | `API_MAINTNOTIFICATION` | `MaintenanceNotification` | `BASE_ADDRESS` is the tenant host root (`https://{tenant}-api.s4hana.cloud.sap`). Endpoints carry `/sap/opu/odata/sap/...`. Topics use `erp/sap-s4hana-cloud//`. > [!NOTE] > > S/4HANA Cloud OData requires a real tenant and either a communication-user (Basic) or OAuth 2.0 client-credentials Bearer token. Mutating calls need an `X-CSRF-Token` (fetch with `Fetch` first). ## Basic Syntax ```lot wrap theme={"theme":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} # Coreflux REST_API template — SAP S/4HANA Cloud (curated Manufacturing / PM slice) # Generated from curated OpenAPI; credentials are placeholders only. # Docs: https://docs.coreflux.org/latest/lot-language/routes/rest-api-routes # Auth: OAuth 2.0 client credentials (Bearer {value.json.access_token}) OR Basic # communication user (WITH USERNAME / WITH PASSWORD GET SECRET). Never commit secrets. # BASE_ADDRESS: https://{tenant}-api.s4hana.cloud.sap — replace YOUR_TENANT. # OData mutating calls need CSRF: run Fetchcsrf first (X-CSRF-Token: Fetch), then pass # {value.json.csrf_token} on POST events. Keep session cookies if the broker exposes them. # Accelerator Hub refs: # API_PRODUCTION_ORDER_2_SRV · API_PROD_ORDER_CONFIRMATION_2_SRV # API_MATERIAL_DOCUMENT_SRV · API_MAINTNOTIFICATION DEFINE CONNECTOR SapS4hanaCloudRestApi WITH TYPE REST_API ADD REST_API_CONFIG WITH BASE_ADDRESS "https://YOUR_TENANT-api.s4hana.cloud.sap" WITH ENABLE_CLIENT "true" WITH USE_SSL "true" WITH REQUEST_TIMEOUT '60' WITH DEFAULT_CONTENT_TYPE "application/json" # Auth option A (OAuth client credentials): obtain access_token upstream, pass as Bearer. # Auth option B (Basic communication user): uncomment and use Hub secrets: # WITH USERNAME "YOUR_COMM_USER" # WITH PASSWORD GET SECRET "SAP_S4_COMM_PASSWORD" # Prefer Hub secrets / GET SECRET — never commit real tokens or passwords. ADD EVENT Getproductionordersbyplant WITH SOURCE_TOPIC "erp/sap-s4hana-cloud/production-order/get" WITH DESTINATION_TOPIC "erp/sap-s4hana-cloud/production-order/get/response" WITH METHOD "GET" WITH ENDPOINT "/sap/opu/odata/sap/API_PRODUCTION_ORDER_2_SRV/A_ProductionOrder_2?$filter=ProductionPlant eq '{value.json.plant}'&$format=json" WITH HEADERS ADD "Accept" "application/json" ADD "Authorization" "Bearer {value.json.access_token}" ADD EVENT Getproductionorder WITH SOURCE_TOPIC "erp/sap-s4hana-cloud/production-order/get-by-id" WITH DESTINATION_TOPIC "erp/sap-s4hana-cloud/production-order/get-by-id/response" WITH METHOD "GET" WITH ENDPOINT "/sap/opu/odata/sap/API_PRODUCTION_ORDER_2_SRV/A_ProductionOrder_2('{value.json.ManufacturingOrder}')?$format=json" WITH HEADERS ADD "Accept" "application/json" ADD "Authorization" "Bearer {value.json.access_token}" ADD EVENT Fetchcsrf WITH SOURCE_TOPIC "erp/sap-s4hana-cloud/csrf/fetch" WITH DESTINATION_TOPIC "erp/sap-s4hana-cloud/csrf/fetch/response" WITH METHOD "GET" WITH ENDPOINT "/sap/opu/odata/sap/API_MAINTNOTIFICATION/MaintenanceNotification?$top=1&$format=json" WITH HEADERS ADD "Accept" "application/json" ADD "X-CSRF-Token" "Fetch" ADD "Authorization" "Bearer {value.json.access_token}" ADD EVENT Createproductionorderconfirmation WITH SOURCE_TOPIC "erp/sap-s4hana-cloud/production-order-confirmation/create" WITH DESTINATION_TOPIC "erp/sap-s4hana-cloud/production-order-confirmation/create/response" WITH METHOD "POST" WITH ENDPOINT "/sap/opu/odata/sap/API_PROD_ORDER_CONFIRMATION_2_SRV/ProdnOrdConf2" WITH HEADERS ADD "Accept" "application/json" ADD "Content-Type" "application/json" ADD "X-CSRF-Token" "{value.json.csrf_token}" ADD "Authorization" "Bearer {value.json.access_token}" WITH BODY "{payload}" ADD EVENT Creatematerialdocument WITH SOURCE_TOPIC "erp/sap-s4hana-cloud/material-document/create" WITH DESTINATION_TOPIC "erp/sap-s4hana-cloud/material-document/create/response" WITH METHOD "POST" WITH ENDPOINT "/sap/opu/odata/sap/API_MATERIAL_DOCUMENT_SRV/A_MaterialDocumentHeader" WITH HEADERS ADD "Accept" "application/json" ADD "Content-Type" "application/json" ADD "X-CSRF-Token" "{value.json.csrf_token}" ADD "Authorization" "Bearer {value.json.access_token}" WITH BODY "{payload}" ADD EVENT Createmaintenancenotification WITH SOURCE_TOPIC "erp/sap-s4hana-cloud/maintenance-notification/create" WITH DESTINATION_TOPIC "erp/sap-s4hana-cloud/maintenance-notification/create/response" WITH METHOD "POST" WITH ENDPOINT "/sap/opu/odata/sap/API_MAINTNOTIFICATION/MaintenanceNotification" WITH HEADERS ADD "Accept" "application/json" ADD "Content-Type" "application/json" ADD "X-CSRF-Token" "{value.json.csrf_token}" ADD "Authorization" "Bearer {value.json.access_token}" WITH BODY "{payload}" ``` ## Events | Event | Method | Endpoint | Request topic | Response topic | | - | - | - | - | - | | Getproductionordersbyplant | GET | `/sap/opu/odata/sap/API_PRODUCTION_ORDER_2_SRV/A_ProductionOrder_2?$filter=ProductionPlant eq '{plant}'` | `erp/sap-s4hana-cloud/production-order/get` | `erp/sap-s4hana-cloud/production-order/get/response` | | Getproductionorder | GET | `/sap/opu/odata/sap/API_PRODUCTION_ORDER_2_SRV/A_ProductionOrder_2('{ManufacturingOrder}')` | `erp/sap-s4hana-cloud/production-order/get-by-id` | `erp/sap-s4hana-cloud/production-order/get-by-id/response` | | Fetchcsrf | GET | `/sap/opu/odata/sap/API_MAINTNOTIFICATION/MaintenanceNotification` | `erp/sap-s4hana-cloud/csrf/fetch` | `erp/sap-s4hana-cloud/csrf/fetch/response` | | Createproductionorderconfirmation | POST | `/sap/opu/odata/sap/API_PROD_ORDER_CONFIRMATION_2_SRV/ProdnOrdConf2` | `erp/sap-s4hana-cloud/production-order-confirmation/create` | `erp/sap-s4hana-cloud/production-order-confirmation/create/response` | | Creatematerialdocument | POST | `/sap/opu/odata/sap/API_MATERIAL_DOCUMENT_SRV/A_MaterialDocumentHeader` | `erp/sap-s4hana-cloud/material-document/create` | `erp/sap-s4hana-cloud/material-document/create/response` | | Createmaintenancenotification | POST | `/sap/opu/odata/sap/API_MAINTNOTIFICATION/MaintenanceNotification` | `erp/sap-s4hana-cloud/maintenance-notification/create` | `erp/sap-s4hana-cloud/maintenance-notification/create/response` | ## Auth OAuth Bearer (client credentials access token): ```lot wrap theme={"theme":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} ADD "Authorization" "Bearer {value.json.access_token}" ``` Or Basic communication user on config (Hub secret): ```lot wrap theme={"theme":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} WITH USERNAME "YOUR_COMM_USER" WITH PASSWORD GET SECRET "SAP_S4_COMM_PASSWORD" ``` CSRF on POST: ```lot wrap theme={"theme":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} ADD "X-CSRF-Token" "{value.json.csrf_token}" ``` **access\_token** `string` *required* OAuth 2.0 access token from the S/4HANA Cloud / BTP token endpoint (client credentials). Prefer a Hub secret. Never commit the token. **plant** `string` *required* Production plant code for Getproductionordersbyplant (`ProductionPlant` OData filter). **ManufacturingOrder** `string` Production order key for Getproductionorder. **csrf\_token** `string` *required* Value from the `X-CSRF-Token` response header after Fetchcsrf. Required on all POST events. High-level setup (no secrets in the template): 1. Create Communication Arrangements for the needed scenarios (e.g. production / confirmation, material document, maintenance `SAP_COM_0397`). 2. Create a communication user or OAuth client; obtain Basic credentials or an access token. 3. Set `BASE_ADDRESS` to `https://{tenant}-api.s4hana.cloud.sap`. 4. Fetch CSRF, then call GET/POST events with placeholders filled. GET production orders by plant: ```json theme={"theme":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} {"access_token": "YOUR_S4_ACCESS_TOKEN", "plant": "1010"} ``` POST maintenance notification (connector body is `{payload}`; fetch CSRF first): ```json theme={"theme":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} { "access_token": "YOUR_S4_ACCESS_TOKEN", "csrf_token": "YOUR_CSRF_TOKEN", "NotificationType": "Y1", "NotificationText": "Alarm from Coreflux edge", "TechnicalObject": "EQUIPMENT_ID", "TechObjIsEquipOrFuncnlLoc": "EAMS_EQUIP", "MaintPriority": "2", "MaintenanceObjectIsDown": true } ``` ## Try it 1. Replace `YOUR_TENANT` in `BASE_ADDRESS` and add the connector on your broker. 2. Publish token + plant JSON to `erp/sap-s4hana-cloud/production-order/get`. 3. Subscribe to `erp/sap-s4hana-cloud/production-order/get/response`. 4. For POSTs: call `erp/sap-s4hana-cloud/csrf/fetch` first, then publish with `csrf_token` and body fields. ## Related * [REST API Connectors](/latest/lot-language/routes/rest-api-routes) * [REST API Apps](/latest/lot-language/routes/rest-api-apps) * [SAP Business Accelerator Hub — S/4HANA Cloud](https://api.sap.com/products/SAPS4HANACloud/apis/packages) # SAP S/4HANA on-premise / ECC (Gateway) Source: https://docs.coreflux.org/latest/lot-language/routes/rest-api-apps/sap-s4hana-onprem Client-mode REST API connector for SAP Gateway OData: production orders, confirmations, goods movements, maintenance notifications, work centers, and PM orders Technical Preview ## Overview A client-mode connector for a **curated manufacturing and plant-maintenance** slice of [SAP S/4HANA on-premise / ECC](https://api.sap.com/products/SAPS4HANA/apis/all) OData APIs exposed through [SAP Gateway](https://help.sap.com/docs/SAP_GATEWAY): | Catalog connector event | OData service | Entity set | | - | - | - | | GET production orders by plant | `API_PRODUCTION_ORDER_2_SRV` | `A_ProductionOrder_2` | | POST order confirmation (yield, scrap, time) | `API_PROD_ORDER_CONFIRMATION_2_SRV` | `ProdnOrdConf2` | | POST goods movement | `API_MATERIAL_DOCUMENT_SRV` | `A_MaterialDocumentHeader` | | POST maintenance notification from alarm | `API_MAINTNOTIFICATION` | `MaintenanceNotification` | | GET work centers | `API_WORK_CENTERS` | `A_WorkCenters` | | GET PM orders | `API_MAINTENANCEORDER` | `MaintenanceOrder` | `BASE_ADDRESS` is the Gateway host root (`https://{host}:{port}`). Endpoints carry `/sap/opu/odata/sap/...`. Topics use `erp/sap-s4hana-onprem//`. > [!NOTE] > > Service naming: Hub/SDK use `API_WORK_CENTERS` (not `API_WORK_CENTER_SRV`) and `API_MAINTENANCEORDER` (not `API_MAINTENANCEORDER_2_SRV`). Activate services in `/IWFND/MAINT_SERVICE` and confirm entity sets with `$metadata` on your host. Older ECC may expose classic PP/PM Gateway names — adjust after metadata check. Mutating OData calls need `X-CSRF-Token`. ## Basic Syntax ```lot wrap theme={"theme":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} # Coreflux REST_API template — SAP S/4HANA on-premise / ECC (SAP Gateway) # Generated from curated OpenAPI; credentials are placeholders only. # Docs: https://docs.coreflux.org/latest/lot-language/routes/rest-api-routes # Auth: Basic (WITH USERNAME / WITH PASSWORD GET SECRET "SAP_S4_ONPREM_PASSWORD") OR # OAuth 2.0 / SAML bearer (Bearer {value.json.access_token}). X.509 at TLS layer. # BASE_ADDRESS: https://YOUR_HOST:YOUR_PORT — Gateway root; endpoints keep /sap/opu/odata/sap/... # OData mutating calls need CSRF: run Fetchcsrf first (X-CSRF-Token: Fetch), then pass # {value.json.csrf_token} on POST events. DEFINE CONNECTOR SapS4hanaOnpremRestApi WITH TYPE REST_API ADD REST_API_CONFIG WITH BASE_ADDRESS "https://YOUR_HOST:YOUR_PORT" WITH ENABLE_CLIENT "true" WITH USE_SSL "true" WITH REQUEST_TIMEOUT '60' WITH DEFAULT_CONTENT_TYPE "application/json" # Auth option A (OAuth / SAML bearer): obtain access_token upstream, pass as Bearer on events. # Auth option B (Basic Gateway user): uncomment and use Hub secrets: # WITH USERNAME "YOUR_GATEWAY_USER" # WITH PASSWORD GET SECRET "SAP_S4_ONPREM_PASSWORD" # Prefer Hub secrets / GET SECRET — never commit real tokens or passwords. # Auth option C (X.509): configure client cert at TLS / broker level (not in LoT headers). ADD EVENT Getproductionordersbyplant WITH SOURCE_TOPIC "erp/sap-s4hana-onprem/production-order/get" WITH DESTINATION_TOPIC "erp/sap-s4hana-onprem/production-order/get/response" WITH METHOD "GET" WITH ENDPOINT "/sap/opu/odata/sap/API_PRODUCTION_ORDER_2_SRV/A_ProductionOrder_2?$filter=ProductionPlant eq '{value.json.plant}'&$format=json" WITH HEADERS ADD "Accept" "application/json" ADD "Authorization" "Bearer {value.json.access_token}" ADD EVENT Getproductionorder WITH SOURCE_TOPIC "erp/sap-s4hana-onprem/production-order/get-by-id" WITH DESTINATION_TOPIC "erp/sap-s4hana-onprem/production-order/get-by-id/response" WITH METHOD "GET" WITH ENDPOINT "/sap/opu/odata/sap/API_PRODUCTION_ORDER_2_SRV/A_ProductionOrder_2('{value.json.ManufacturingOrder}')?$format=json" WITH HEADERS ADD "Accept" "application/json" ADD "Authorization" "Bearer {value.json.access_token}" ADD EVENT Getworkcenters WITH SOURCE_TOPIC "erp/sap-s4hana-onprem/work-center/get" WITH DESTINATION_TOPIC "erp/sap-s4hana-onprem/work-center/get/response" WITH METHOD "GET" WITH ENDPOINT "/sap/opu/odata/sap/API_WORK_CENTERS/A_WorkCenters?$filter=Plant eq '{value.json.plant}'&$format=json" WITH HEADERS ADD "Accept" "application/json" ADD "Authorization" "Bearer {value.json.access_token}" ADD EVENT Getpmorders WITH SOURCE_TOPIC "erp/sap-s4hana-onprem/pm-order/get" WITH DESTINATION_TOPIC "erp/sap-s4hana-onprem/pm-order/get/response" WITH METHOD "GET" WITH ENDPOINT "/sap/opu/odata/sap/API_MAINTENANCEORDER/MaintenanceOrder?$filter=MaintenancePlanningPlant eq '{value.json.plant}'&$format=json" WITH HEADERS ADD "Accept" "application/json" ADD "Authorization" "Bearer {value.json.access_token}" ADD EVENT Fetchcsrf WITH SOURCE_TOPIC "erp/sap-s4hana-onprem/csrf/fetch" WITH DESTINATION_TOPIC "erp/sap-s4hana-onprem/csrf/fetch/response" WITH METHOD "GET" WITH ENDPOINT "/sap/opu/odata/sap/API_MAINTNOTIFICATION/MaintenanceNotification?$top=1&$format=json" WITH HEADERS ADD "Accept" "application/json" ADD "X-CSRF-Token" "Fetch" ADD "Authorization" "Bearer {value.json.access_token}" ADD EVENT Createproductionorderconfirmation WITH SOURCE_TOPIC "erp/sap-s4hana-onprem/production-order-confirmation/create" WITH DESTINATION_TOPIC "erp/sap-s4hana-onprem/production-order-confirmation/create/response" WITH METHOD "POST" WITH ENDPOINT "/sap/opu/odata/sap/API_PROD_ORDER_CONFIRMATION_2_SRV/ProdnOrdConf2" WITH HEADERS ADD "Accept" "application/json" ADD "Content-Type" "application/json" ADD "X-CSRF-Token" "{value.json.csrf_token}" ADD "Authorization" "Bearer {value.json.access_token}" WITH BODY "{payload}" ADD EVENT Creatematerialdocument WITH SOURCE_TOPIC "erp/sap-s4hana-onprem/material-document/create" WITH DESTINATION_TOPIC "erp/sap-s4hana-onprem/material-document/create/response" WITH METHOD "POST" WITH ENDPOINT "/sap/opu/odata/sap/API_MATERIAL_DOCUMENT_SRV/A_MaterialDocumentHeader" WITH HEADERS ADD "Accept" "application/json" ADD "Content-Type" "application/json" ADD "X-CSRF-Token" "{value.json.csrf_token}" ADD "Authorization" "Bearer {value.json.access_token}" WITH BODY "{payload}" ADD EVENT Createmaintenancenotification WITH SOURCE_TOPIC "erp/sap-s4hana-onprem/maintenance-notification/create" WITH DESTINATION_TOPIC "erp/sap-s4hana-onprem/maintenance-notification/create/response" WITH METHOD "POST" WITH ENDPOINT "/sap/opu/odata/sap/API_MAINTNOTIFICATION/MaintenanceNotification" WITH HEADERS ADD "Accept" "application/json" ADD "Content-Type" "application/json" ADD "X-CSRF-Token" "{value.json.csrf_token}" ADD "Authorization" "Bearer {value.json.access_token}" WITH BODY "{payload}" ``` ## Events | Event | Method | Endpoint | Request topic | Response topic | | - | - | - | - | - | | Getproductionordersbyplant | GET | `/sap/opu/odata/sap/API_PRODUCTION_ORDER_2_SRV/A_ProductionOrder_2?$filter=ProductionPlant eq '{plant}'` | `erp/sap-s4hana-onprem/production-order/get` | `erp/sap-s4hana-onprem/production-order/get/response` | | Getproductionorder | GET | `/sap/opu/odata/sap/API_PRODUCTION_ORDER_2_SRV/A_ProductionOrder_2('{ManufacturingOrder}')` | `erp/sap-s4hana-onprem/production-order/get-by-id` | `erp/sap-s4hana-onprem/production-order/get-by-id/response` | | Getworkcenters | GET | `/sap/opu/odata/sap/API_WORK_CENTERS/A_WorkCenters?$filter=Plant eq '{plant}'` | `erp/sap-s4hana-onprem/work-center/get` | `erp/sap-s4hana-onprem/work-center/get/response` | | Getpmorders | GET | `/sap/opu/odata/sap/API_MAINTENANCEORDER/MaintenanceOrder?$filter=MaintenancePlanningPlant eq '{plant}'` | `erp/sap-s4hana-onprem/pm-order/get` | `erp/sap-s4hana-onprem/pm-order/get/response` | | Fetchcsrf | GET | `/sap/opu/odata/sap/API_MAINTNOTIFICATION/MaintenanceNotification` | `erp/sap-s4hana-onprem/csrf/fetch` | `erp/sap-s4hana-onprem/csrf/fetch/response` | | Createproductionorderconfirmation | POST | `/sap/opu/odata/sap/API_PROD_ORDER_CONFIRMATION_2_SRV/ProdnOrdConf2` | `erp/sap-s4hana-onprem/production-order-confirmation/create` | `erp/sap-s4hana-onprem/production-order-confirmation/create/response` | | Creatematerialdocument | POST | `/sap/opu/odata/sap/API_MATERIAL_DOCUMENT_SRV/A_MaterialDocumentHeader` | `erp/sap-s4hana-onprem/material-document/create` | `erp/sap-s4hana-onprem/material-document/create/response` | | Createmaintenancenotification | POST | `/sap/opu/odata/sap/API_MAINTNOTIFICATION/MaintenanceNotification` | `erp/sap-s4hana-onprem/maintenance-notification/create` | `erp/sap-s4hana-onprem/maintenance-notification/create/response` | ## Auth OAuth / SAML Bearer access token: ```lot wrap theme={"theme":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} ADD "Authorization" "Bearer {value.json.access_token}" ``` Or Basic Gateway user on config (Hub secret): ```lot wrap theme={"theme":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} WITH USERNAME "YOUR_GATEWAY_USER" WITH PASSWORD GET SECRET "SAP_S4_ONPREM_PASSWORD" ``` CSRF on POST: ```lot wrap theme={"theme":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} ADD "X-CSRF-Token" "{value.json.csrf_token}" ``` **access\_token** `string` OAuth 2.0 / SAML bearer access token for Gateway. Prefer a Hub secret. Never commit the token. Omit when using Basic on `REST_API_CONFIG`. **plant** `string` *required* Plant code for Getproductionordersbyplant, Getworkcenters, and Getpmorders filters. **ManufacturingOrder** `string` Production order key for Getproductionorder. **csrf\_token** `string` *required* Value from the `X-CSRF-Token` response header after Fetchcsrf. Required on all POST events. High-level setup (no secrets in the template): 1. Activate the OData services in `/IWFND/MAINT_SERVICE` (and confirm with Gateway Client `/IWFND/GW_CLIENT`). 2. Create a Gateway user (Basic), OAuth/SAML bearer client, or X.509 client cert. 3. Set `BASE_ADDRESS` to `https://{host}:{port}`. 4. Fetch CSRF, then call GET/POST events with placeholders filled. GET production orders by plant: ```json theme={"theme":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} {"access_token": "YOUR_GATEWAY_ACCESS_TOKEN", "plant": "1010"} ``` GET work centers / PM orders use the same shape (`plant`). POST maintenance notification (connector body is `{payload}`; fetch CSRF first): ```json theme={"theme":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} { "access_token": "YOUR_GATEWAY_ACCESS_TOKEN", "csrf_token": "YOUR_CSRF_TOKEN", "NotificationType": "Y1", "NotificationText": "Alarm from Coreflux edge", "TechnicalObject": "EQUIPMENT_ID", "TechObjIsEquipOrFuncnlLoc": "EAMS_EQUIP", "MaintPriority": "2", "MaintenanceObjectIsDown": true } ``` ## Try it 1. Replace `YOUR_HOST` / `YOUR_PORT` in `BASE_ADDRESS` and add the connector on your broker. 2. Publish token + plant JSON to `erp/sap-s4hana-onprem/production-order/get` (and optionally `work-center/get` / `pm-order/get`). 3. Subscribe to the matching `/response` topics. 4. For POSTs: call `erp/sap-s4hana-onprem/csrf/fetch` first, then publish with `csrf_token` and body fields. ## Related * [REST API Connectors](/latest/lot-language/routes/rest-api-routes) * [REST API Apps](/latest/lot-language/routes/rest-api-apps) * [SAP S/4HANA Cloud](/latest/lot-language/routes/rest-api-apps/sap-s4hana-cloud) * [SAP Gateway Help](https://help.sap.com/docs/SAP_GATEWAY) * [SAP Business Accelerator Hub — S/4HANA](https://api.sap.com/products/SAPS4HANA/apis/all) # SAP SuccessFactors Source: https://docs.coreflux.org/latest/lot-language/routes/rest-api-apps/sap-successfactors LoT connector for SAP SuccessFactors: send clock-in and clock-out punches, record absences, and read users, jobs, departments and time account balances Technical Preview ## Overview A client-mode connector for [SAP SuccessFactors](https://help.sap.com/docs/successfactors-platform/sap-successfactors-api-reference-guide-odata-v2/about-sap-successfactors-odata-apis-v2) HXM: the OData v2 API (User Management, Employee Central employment and personal information, Foundation Objects, Time Off) and the Time Tracking Clock In Clock Out APIs. Use it to connect the shop floor to HR: * Badge readers and gate terminals publish clock-in / clock-out punches on MQTT, and the connector sends them to Clock In Clock Out. * A kiosk or HMI books or cancels an absence, or shows the remaining vacation balance. * Line planning and andon boards read who is assigned to a plant and department, with standard hours (EmpJob with `asOfDate`). The connector `SapSuccessFactorsRestApi` in `SapSuccessfactorsRestApi.lot` has 28 events (23 GET, 5 POST). 3 are writes: `EmployeeTimeCreate`, `Upsert`, `TimeEventsCreate`. There are no DELETE, PUT or PATCH events. Set `BASE_ADDRESS` to the API server of your data center, with no trailing slash and no `/odata/v2`, for example `https://api55.sapsf.eu` or `https://api4.successfactors.com` (SAP KBA 2215682 lists all API servers). Topics use `finance/sap-successfactors//`; replies land on `/response`. See the base guide: [REST API Connectors](/latest/lot-language/routes/rest-api-routes). ## Setup 1. Create or choose an API user with role-based permissions for the entities the broker uses (for example User, Employee Central job information, Time Off) and **Employee Central API** / **SFAPI** access. Every call runs with this user's permissions. 2. In **Admin Center > Manage OAuth2 Client Applications**, register a client: application name and URL, **Bind to Users** with the API user, and **Generate X.509 Certificate**. Download the certificate (it holds the private key) and save the **API Key** (the `client_id`). 3. For Clock In Clock Out punches, register a second client bound to the technical user `TECHNICAL_USER_CICOTERMINAL_9D3F8AC1`. SuccessFactors accepts punches only with that user's token (SAP KBA 3206348). 4. Note your **company ID**. Keep `SF_COMPANY_ID`, `SF_CLIENT_ID`, the private key or assertions, and the current token in broker secrets and inject them with `GET SECRET`. ## Authentication SuccessFactors uses the OAuth 2.0 SAML 2.0 bearer assertion grant: 1. **Get a SAML assertion** for the API user, signed with the client's private key. Generate it outside the broker (for example with SAP's assertion generator or an integration platform), or use `SamlAssertion` (`POST /oauth/idp` with `client_id`, `user_id`, `token_url`, `private_key`). SAP has deprecated `/oauth/idp` because the private key travels in the call (KBA 3239495); it still answers today, but prefer generating the assertion yourself. 2. **Get a token** with `OAuthToken`: `POST /oauth/token` with `company_id`, `client_id` and `assertion` (the grant type `urn:ietf:params:oauth:grant-type:saml2-bearer` is fixed in the template). The reply has `access_token`, `token_type` `Bearer` and `expires_in`. 3. Every other event sends `Authorization: Bearer ` from the payload. `TokenValidate` checks a token. ```json theme={"theme":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} {"company_id": "", "client_id": "", "assertion": ""} ``` The token events send a form body, so every value must be URL-encoded. Assertions and private keys are Base64 and contain `+`, `/` and `=`: send them as `%2B`, `%2F` and `%3D`, or the server rejects the assertion. > [!WARNING] > > HTTP Basic authentication (`user@companyId:password`) for SuccessFactors APIs reached end of maintenance on 2 June 2025 and is deleted on 13 November 2026 (SAP KBA 3472405), so this template does not use it. If you already use OpenID Connect through SAP Cloud Identity Services, get the token from your Identity Services tenant and send it as `access_token` the same way. Never retain topics that carry an assertion, private key or token, and restrict `finance/sap-successfactors/auth/#` with ACLs. ## Events | Event | Request | Source topic | Notes | | - | - | - | - | | SamlAssertion | `POST /oauth/idp` | `finance/sap-successfactors/auth/assertion` | Generate a SAML 2.0 assertion on the server (deprecated by SAP, KBA 3239495: the X.509 private key travels in the call; prefer signing the assertion yourself). | | OAuthToken | `POST /oauth/token` | `finance/sap-successfactors/auth/token` | Exchange a SAML assertion for an access token (grant\_type urn:ietf:params:oauth:grant-type:saml2-bearer, fixed). | | TokenValidate | `GET /oauth/validate` | `finance/sap-successfactors/auth/validate` | Check that access\_token is still valid (401 when it is malformed or expired). Use before a burst of calls or after a 401. | | Metadata | `GET /odata/v2/{entity_set}/$metadata` | `finance/sap-successfactors/metadata/get` | OData v2 metadata (EDMX XML) of one entity set, for example User, EmpJob or EmployeeTime, including custom fields of your instance. Accept application/xml. | | EntityQuery | `GET /odata/v2/{entity_set}?{query}` | `finance/sap-successfactors/odata/query` | Generic read of any entity set your API user may read (entity\_set e.g. PerEmail, FOCostCenter, WorkSchedule, Holiday). | | UsersList | `GET /odata/v2/User?{query}` | `finance/sap-successfactors/users/list` | List users (catalog event). | | UserGet | `GET /odata/v2/User('{user_id}')?{query}` | `finance/sap-successfactors/users/get` | One user by userId (catalog event), for a badge-to-person lookup. | | PerPersonList | `GET /odata/v2/PerPerson?{query}` | `finance/sap-successfactors/persons/list` | List persons (biographical information). | | PerPersonGet | `GET /odata/v2/PerPerson('{person_id}')?{query}` | `finance/sap-successfactors/persons/get` | One person by personIdExternal. | | EmpEmploymentList | `GET /odata/v2/EmpEmployment?{query}` | `finance/sap-successfactors/employments/list` | Employments (hire, termination, assignment). | | EmpJobList | `GET /odata/v2/EmpJob?{query}` | `finance/sap-successfactors/jobs/list` | Job information (catalog event): department, location, manager, standard hours, time recording variant. Effective dated: add asOfDate=YYYY-MM-DD or fromDate / toDate. | | EmpJobGet | `GET /odata/v2/EmpJob(seqNumber={seq_number}L,startDate=datetime'{start_date}',userId='{user_id}')?{query}` | `finance/sap-successfactors/jobs/get` | One job information record by its key: seq\_number (number), start\_date (YYYY-MM-DDT00:00:00), user\_id. | | FODepartmentList | `GET /odata/v2/FODepartment?{query}` | `finance/sap-successfactors/departments/list` | Departments (Foundation Object, effective dated). | | FOLocationList | `GET /odata/v2/FOLocation?{query}` | `finance/sap-successfactors/locations/list` | Locations, such as plants and sites (Foundation Object, effective dated). | | EmployeeTimeList | `GET /odata/v2/EmployeeTime?{query}` | `finance/sap-successfactors/employee-time/list` | Absences and other employee time records. | | EmployeeTimeGet | `GET /odata/v2/EmployeeTime('{external_code}')?{query}` | `finance/sap-successfactors/employee-time/get` | One EmployeeTime by externalCode, for example to read approvalStatus after a create. | | EmployeeTimeCreate | `POST /odata/v2/EmployeeTime` | `finance/sap-successfactors/employee-time/create` | Write. Create an EmployeeTime record, such as an absence (catalog event POST EmployeeTime). | | Upsert | `POST /odata/v2/upsert?{query}` | `finance/sap-successfactors/odata/upsert` | Write. OData v2 upsert: create or update one record (or an array) of any upsertable entity set, named in \_\_metadata.uri. | | TimeTypeList | `GET /odata/v2/TimeType?{query}` | `finance/sap-successfactors/time-types/list` | Time types (vacation, sickness, shift types) with their codes for EmployeeTime. | | AvailableTimeTypeList | `GET /odata/v2/AvailableTimeType?{query}` | `finance/sap-successfactors/time-types/available` | Time types available to employees through their time profile. | | TimeAccountList | `GET /odata/v2/TimeAccount?{query}` | `finance/sap-successfactors/time-accounts/list` | Time accounts (vacation entitlement, overtime) per user. | | TimeAccountDetailList | `GET /odata/v2/TimeAccountDetail?{query}` | `finance/sap-successfactors/time-accounts/details` | Bookings on time accounts (accruals, absences, payouts). | | EmpTimeAccountBalanceList | `GET /odata/v2/EmpTimeAccountBalance?{query}` | `finance/sap-successfactors/time-accounts/balances` | Current balance per time account, for a kiosk or HMI that shows remaining vacation. | | EmployeeTimeCalendarList | `GET /odata/v2/EmployeeTimeCalendar?{query}` | `finance/sap-successfactors/employee-time/calendar` | Day-by-day calendar entries of employee time records. | | TimeEventsCreate | `POST /rest/timemanagement/timeeventprocessing/v1/TimeEvents` | `finance/sap-successfactors/clock/events/create` | Write. Send clock-in / clock-out punches from a terminal to Clock In Clock Out (catalog clock events). The token must belong to an OAuth client bound to TECHNICAL\_USER\_CICOTERMINAL\_9D3F8AC1. | | TimeEventsExport | `GET /odatav4/timemanagement/timeeventprocessing/clockinclockout/v1/timeevents?{query}` | `finance/sap-successfactors/clock/events/list` | Export clock events (OData v4, up to 1000 per call; needs the export permission, else an empty list). | | ClockGroupsList | `GET /odatav4/timemanagement/timeeventprocessing/ClockInClockOutIntegration.svc/v1/ClockInClockOutGroups?{query}` | `finance/sap-successfactors/clock/groups/list` | Clock In Clock Out groups with their time event types (codes for typeCode). | | ClockGroupGet | `GET /odatav4/timemanagement/timeeventprocessing/ClockInClockOutIntegration.svc/v1/ClockInClockOutGroups('{group_code}')?{query}` | `finance/sap-successfactors/clock/groups/get` | One Clock In Clock Out group by code. | ## Payloads | Field | Used by | Content | | - | - | - | | `company_id`, `client_id`, `assertion` | `OAuthToken` | Company ID, API key of the OAuth client and the SAML assertion, URL-encoded | | `client_id`, `user_id`, `token_url`, `private_key` | `SamlAssertion` | API key, API user (or `TECHNICAL_USER_CICOTERMINAL_9D3F8AC1`), `https%3A%2F%2F%2Foauth%2Ftoken` and the private key, URL-encoded | | `access_token` | every event except the two token events | Token from `OAuthToken` | | `user_id`, `person_id`, `external_code`, `group_code` | `UserGet`, `PerPersonGet`, `EmployeeTimeGet`, `ClockGroupGet` | Key of the record | | `seq_number`, `start_date`, `user_id` | `EmpJobGet` | EmpJob key: sequence number, start date `YYYY-MM-DDT00:00:00`, user ID | | `entity_set` | `EntityQuery`, `Metadata` | Any OData v2 entity set, for example `PerEmail`, `FOCostCenter`, `EmployeeTime` | | `query` | read events, `Upsert` | URL query string without `?`, or `""`, with spaces as `%20`, for example `$filter=userId%20eq%20'cgrant'&$top=50` | | `body` | `EmployeeTimeCreate`, `Upsert`, `TimeEventsCreate` | JSON object (or array for `Upsert`); a JSON array of punches for `TimeEventsCreate` | OData v2 query options: `$filter`, `$select`, `$expand`, `$orderby`, `$top`, `$skip`, `$format`, `$inlinecount`, and the SuccessFactors effective-dating options `asOfDate`, `fromDate` and `toDate` (`YYYY-MM-DD`). Dates in OData v2 JSON bodies use `/Date()/`. ## Basic Syntax An excerpt of the template (the full file has all 28 events and comments): ```lot wrap theme={"theme":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} DEFINE CONNECTOR SapSuccessFactorsRestApi WITH TYPE REST_API ADD REST_API_CONFIG WITH BASE_ADDRESS "https://YOUR_API_SERVER" WITH ENABLE_CLIENT "true" WITH USE_SSL "true" WITH REQUEST_TIMEOUT '120' WITH DEFAULT_CONTENT_TYPE "application/json" ADD EVENT OAuthToken WITH SOURCE_TOPIC "finance/sap-successfactors/auth/token" WITH DESTINATION_TOPIC "finance/sap-successfactors/auth/token/response" WITH METHOD "POST" WITH ENDPOINT "/oauth/token" WITH HEADERS ADD "Accept" "application/json" ADD "User-Agent" "Coreflux-Broker REST_API route (https://www.coreflux.org)" ADD "Content-Type" "application/x-www-form-urlencoded" WITH BODY "company_id={value.json.company_id}&client_id={value.json.client_id}&grant_type=urn%3Aietf%3Aparams%3Aoauth%3Agrant-type%3Asaml2-bearer&assertion={value.json.assertion}" ADD EVENT EmpJobList WITH SOURCE_TOPIC "finance/sap-successfactors/jobs/list" WITH DESTINATION_TOPIC "finance/sap-successfactors/jobs/list/response" WITH METHOD "GET" WITH ENDPOINT "/odata/v2/EmpJob?{value.json.query}" WITH HEADERS ADD "Accept" "application/json" ADD "User-Agent" "Coreflux-Broker REST_API route (https://www.coreflux.org)" ADD "Authorization" "Bearer {value.json.access_token}" ADD EVENT EmployeeTimeCreate WITH SOURCE_TOPIC "finance/sap-successfactors/employee-time/create" WITH DESTINATION_TOPIC "finance/sap-successfactors/employee-time/create/response" WITH METHOD "POST" WITH ENDPOINT "/odata/v2/EmployeeTime" WITH HEADERS ADD "Accept" "application/json" ADD "User-Agent" "Coreflux-Broker REST_API route (https://www.coreflux.org)" ADD "Authorization" "Bearer {value.json.access_token}" ADD "Content-Type" "application/json" WITH BODY "{value.json.body}" ADD EVENT Upsert WITH SOURCE_TOPIC "finance/sap-successfactors/odata/upsert" WITH DESTINATION_TOPIC "finance/sap-successfactors/odata/upsert/response" WITH METHOD "POST" WITH ENDPOINT "/odata/v2/upsert?{value.json.query}" WITH HEADERS ADD "Accept" "application/json" ADD "User-Agent" "Coreflux-Broker REST_API route (https://www.coreflux.org)" ADD "Authorization" "Bearer {value.json.access_token}" ADD "Content-Type" "application/json" WITH BODY "{value.json.body}" ADD EVENT TimeEventsCreate WITH SOURCE_TOPIC "finance/sap-successfactors/clock/events/create" WITH DESTINATION_TOPIC "finance/sap-successfactors/clock/events/create/response" WITH METHOD "POST" WITH ENDPOINT "/rest/timemanagement/timeeventprocessing/v1/TimeEvents" WITH HEADERS ADD "Accept" "application/json" ADD "User-Agent" "Coreflux-Broker REST_API route (https://www.coreflux.org)" ADD "Authorization" "Bearer {value.json.access_token}" ADD "Content-Type" "application/json" WITH BODY "{value.json.body}" ``` ## Examples ### Keep a token for the other actions Publish a fresh, URL-encoded assertion from your signer to `state/sf/assertion` (assertions are short-lived). A `COLLAPSED` model builds the token request, because `PUBLISH TOPIC` does not take inline JSON: ```lot theme={"theme":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} DEFINE MODEL SfTokenRequest COLLAPSED ADD STRING "company_id" ADD STRING "client_id" ADD STRING "assertion" DEFINE ACTION SfToken ON TOPIC "state/sf/assertion" DO SET "company" WITH GET SECRET "SF_COMPANY_ID" SET "client" WITH GET SECRET "SF_CLIENT_ID" SET "assertion" WITH PAYLOAD AS STRING PUBLISH MODEL SfTokenRequest TO "finance/sap-successfactors/auth/token" WITH company_id = {company} client_id = {client} assertion = {assertion} DEFINE ACTION SfKeepToken ON TOPIC "finance/sap-successfactors/auth/token/response" DO SET "token" WITH (GET JSON "access_token" IN PAYLOAD AS STRING) KEEP TOPIC "state/sf/access_token" WITH {token} ``` ### Who works where today Every hour, read the job information valid today for one plant, with department, manager and standard hours. The reply is OData v2 JSON: rows in `d.results`; follow `d.__next` (a `$skiptoken` link) for the next page. ```lot theme={"theme":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} DEFINE MODEL SfQuery COLLAPSED ADD STRING "access_token" ADD STRING "query" DEFINE ACTION SfShiftRoster ON EVERY 60 MINUTES DO SET "token" WITH (GET TOPIC "state/sf/access_token" AS STRING) PUBLISH MODEL SfQuery TO "finance/sap-successfactors/jobs/list" WITH access_token = {token} query = "$filter=location%20eq%20'PORTO'&$select=userId,jobTitle,department,location,standardHours,managerId&$top=200" ``` Without `asOfDate` SuccessFactors returns the record valid today; add `asOfDate=2026-10-05` (or `fromDate` / `toDate`) for another day. ### Send badge punches to Clock In Clock Out The badge gateway publishes the punch payload on `finance/sap-successfactors/clock/events/create` (a LoT model cannot build the JSON array that the API expects). `access_token` must come from the client bound to `TECHNICAL_USER_CICOTERMINAL_9D3F8AC1`. `P10` / `P20` stand for the clock-in and clock-out time event types of your Clock In Clock Out group; `ClockGroupsList` lists them. ```json theme={"theme":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} {"access_token": "", "body": [{"id": "1", "assignmentId": "cgrant", "typeCode": "P10", "timestamp": "2026-10-05T07:58:00+01:00", "terminalId": "PORTO-GATE-1", "timeTypeCode": "WORKING_TIME"}]} ``` SuccessFactors answers `201` with `succeededTimeEvents` (one `UUID` per punch) and `failedTimeEvents` (for example `A time event for this employee and timestamp already exists.`). Up to 1000 punches can go in one call; `id` only has to be unique within the call. Read them back with `TimeEventsExport`: ```json theme={"theme":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} {"access_token": "", "query": "$filter=workAssignmentId%20eq%20'cgrant'%20and%20timestampUTC%20ge%202026-10-05T00:00:00Z&$orderby=timestampUTC&$top=100"} ``` ### Book and cancel an absence Publish to `finance/sap-successfactors/employee-time/create` (the time type must exist and be allowed for the employee; check with `TimeTypeList` / `AvailableTimeTypeList`): ```json theme={"theme":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} {"access_token": "", "body": {"externalCode": "CF-0001", "userId": "cgrant", "timeType": "TT_VAC_REC", "startDate": "/Date(1791763200000)/", "endDate": "/Date(1791763200000)/", "comment": "Booked from the plant kiosk"}} ``` Cancel it later with an upsert on `finance/sap-successfactors/odata/upsert`. The reply has one row per record with `status` `OK` or `ERROR` and a `message`: ```json theme={"theme":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} {"access_token": "", "query": "$format=json", "body": {"__metadata": {"uri": "EmployeeTime('CF-0001')"}, "externalCode": "CF-0001", "approvalStatus": "CANCELLED"}} ``` ## Notes * Effective-dated entities (EmpJob, EmpEmployment, Foundation Objects) return the record valid today unless you pass `asOfDate`, or `fromDate` / `toDate` for history. * `EmpJob` cannot be created with POST; it is upsert only (the metadata marks it `upsertable`, not `creatable`). `Upsert` works for any upsertable entity, but HR master data changes usually belong in SuccessFactors itself; the template only shows time records. * `TimeEventsExport` needs the export permission; without it SuccessFactors returns an empty list with `200`. * Use `Metadata` (`/odata/v2//$metadata`) to see the custom fields of your instance, and `EntityQuery` to read entity sets that have no dedicated event. * Errors: OData v2 returns `401` with `[LGN0007]Invalid OAuth token` for a bad token; the Clock In Clock Out APIs answer `401` `AuthenticationRequired` without a token and `400` `AuthenticationFailed` for a malformed one; `/oauth/token` answers `400` with `errorHttpCode` / `errorMessage`. * Not packaged on purpose: every DELETE, the Clock In Clock Out mass delete (`PATCH` with `deleted: true`), PUT / MERGE updates (use `Upsert`), `$batch`, SFAPI and Compound Employee, the OData v4 Employee Central APIs, and HTTP Basic authentication. # Seeq Source: https://docs.coreflux.org/latest/lot-language/routes/rest-api-apps/seeq LoT connector for Seeq: push MQTT values into signals, store alarms as capsules, read samples and capsules back, search items and run Seeq Formulas Technical Preview ## Overview Seeq is an advanced analytics application for process manufacturing data. It connects to historians and other sources, and it can also **store** signals and conditions itself. The Seeq web application uses the **Seeq Server REST API** for everything, so anything you do in Workbench can be done through `https:///api/`. The **SeeqRestApi** connector covers login, server status, item search, stored signals (create, push samples, replace samples, read samples or one sample), stored conditions (create, add capsules, read capsules), Seeq Formula runs, and archiving. Typical uses: send MQTT readings that are not in any historian into a Seeq signal, store alarms or batch events from MQTT as capsules, and pull Seeq results (for example an aggregate) back onto the broker. Each event publishes the server's reply on `/response`. | Event | Topic | Request | Notes | | - | - | - | - | | Login | `historian/seeq/session/create` | `POST /api/auth/login` | Access key as username + password (Seeq directory); session token comes back in the x-sq-auth response header, body = user | | Logout | `historian/seeq/session/delete` | `DELETE /api/auth/logout` | Invalidate the session token | | GetServerStatus | `historian/seeq/server-status/get` | `GET /api/system/server-status` | Server readiness and version (no token needed) | | Ping | `historian/seeq/ping/get` | `GET /api/system/ping` | Authenticated readiness check; keeps the session alive | | GetCurrentUser | `historian/seeq/user/get` | `GET /api/users/me` | User behind the token (token check) | | SearchItems | `historian/seeq/item/search` | `GET /api/items?{query}` | Find signals / conditions / assets: filters=Name==Temperature\&types=StoredSignal\&limit=50 (URL-encoded) | | GetItem | `historian/seeq/item/get` | `GET /api/items/{id}` | All properties of one item | | CreateSignal | `historian/seeq/signal/create` | `POST /api/signals` | Create a signal stored in Seeq (name required; valueUnitOfMeasure, interpolationMethod, maximumInterpolation) -> id | | GetSignal | `historian/seeq/signal/get` | `GET /api/signals/{id}` | Signal metadata | | ArchiveSignal | `historian/seeq/signal/delete` | `DELETE /api/signals/{id}` | Archive a signal | | AddSamples | `historian/seeq/sample/create` | `POST /api/signals/{id}/samples` | Push samples \{samples: \[\{key: ISO time, value}]} into a stored signal | | ReplaceSamples | `historian/seeq/sample/update` | `PUT /api/signals/{id}/samples` | Replace samples in the same time range (optional interval \{start, end}) | | GetSamples | `historian/seeq/sample/list` | `GET /api/signals/{id}/samples?start={start}&end={end}&limit={limit}` | Samples over an interval (start and end required); continuationToken for the next page | | GetSample | `historian/seeq/sample/get` | `GET /api/signals/{id}/sample/{key}?lookup={lookup}` | One sample at or near a key; lookup AtOrBefore + now = latest value | | CreateCondition | `historian/seeq/condition/create` | `POST /api/conditions` | Create a stored condition (name + maximumDuration, e.g. 1d) -> id | | ArchiveCondition | `historian/seeq/condition/delete` | `DELETE /api/conditions/{id}` | Archive a condition and its capsules | | AddCapsules | `historian/seeq/capsule/create` | `POST /api/conditions/{id}/capsules` | Store alarms / events as capsules \{capsules: \[\{start, end, properties}]} | | GetCapsules | `historian/seeq/capsule/list` | `GET /api/conditions/{id}/capsules?start={start}&end={end}&limit={limit}` | Capsules in a time range | | RunFormula | `historian/seeq/formula/run` | `POST /api/formulas/run` | Run a Seeq Formula \{formula, parameters: \["name=\"], start, end} -> samples / capsules / scalar / table | | RunFormulaQuery | `historian/seeq/formula-query/run` | `GET /api/formulas/run?{query}` | Same as RunFormula with a URL-encoded query (start, end, formula, parameters, limit) | ## Prerequisites 1. A Seeq server the broker host can reach. Seeq SaaS sites look like `https://.seeq.site`. An on-premises server listens on `http://:34216` by default, or on HTTPS behind 443 / 8443. 2. A Seeq **access key** and its password (profile menu → **Access Keys**). Access keys work with every directory, including OAuth 2.0 / OpenID Connect single sign-on. Access key creation is off by default for non-admin users (`Authentication/AccessKey/Creation/Enabled`). An admin can turn it on or create the key for you. A Seeq directory user name and password also work. 3. Write access to the signals and conditions you push to. A signal or condition created through this connector belongs to the access key's user and has global scope unless you set `scopedTo` to a workbook ID. ## Authentication Seeq uses a **session token** in the `x-sq-auth` header: 1. Log in: publish `{ "body": { "username": "", "password": "" } }` to `historian/seeq/session/create`. Leave `authProviderClass` and `authProviderId` out to use the Seeq directory, which access keys require. For a directory user (for example LDAP), set both to the directory's datasource class and ID from `GET /api/auth/providers`. 2. Seeq returns the token in the **`x-sq-auth` response header**. The response body (`historian/seeq/session/create/response`) is the user record, not the token. The connector publishes the body, so get the token another way, for example with a one-off call from the broker host, and store it as a Hub secret: ```bash theme={"theme":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} curl -s -D - -o /dev/null -X POST https:///api/auth/login \ -H "Accept: application/vnd.seeq.v1+json" -H "Content-Type: application/vnd.seeq.v1+json" \ -d '{"username":"","password":""}' | grep -i '^x-sq-auth' ``` 3. Send `"auth": ""` with every other event. The connector puts it in the `x-sq-auth` header. 4. Seeq ends a session after a period of inactivity (the user's `sessionDuration`). A `Ping` every few minutes keeps it alive. A `401` on any response means the token has expired: log in again. Access keys also stop working when their user has not logged in to Seeq within `Authentication/AccessKey/ValidityWithoutUserLogin` (14 days by default). `GetServerStatus` needs no token and is a good connectivity check. > [!WARNING] > > Keep the access key, its password and the session token in Hub secrets and add them with `GET SECRET`. Never put them in the connector file or in retained topics. ## Configuration * Replace `YOUR_SEEQ_SERVER` in `BASE_ADDRESS` with your Seeq host. For an on-premises server without TLS use `http://:34216` and set `USE_SSL` to `"false"`. * Every request sends `Accept: application/vnd.seeq.v1+json`, and every body uses `Content-Type: application/vnd.seeq.v1+json`. Seeq rejects other values: `406` for a different `Accept`, `415` for plain `application/json`. Keep these headers if you add your own events. * Keys (timestamps) are ISO 8601, for example `2026-10-05T07:00:00Z`. Use `Z`. A `+hh:mm` offset in a query string must be URL-encoded (`%2B`). * Signal, condition and item IDs are Seeq GUIDs. `CreateSignal` and `CreateCondition` return them in `id`. You can also find them with `SearchItems` (`filters=Name==Line1 Temperature&types=StoredSignal`) or copy them from Workbench (Item Properties). * `GetSamples` and `GetCapsules` return at most `limit` entries. When more exist, the response carries a `continuationToken`. * Errors are JSON `{ "statusMessage": "..." }`. `403` means the user lacks write access to the item, and `404` means the ID is unknown or archived. * Seeq SaaS sits behind Cloudflare. Requests with the default Python `urllib` User-Agent are refused with Cloudflare error 1010. Requests with no User-Agent or a normal client User-Agent are accepted. ## Examples Create a signal stored in Seeq for an MQTT value: ```json theme={"theme":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} // publish to historian/seeq/signal/create { "auth": "", "body": { "name": "Coreflux Line1 Temperature", "valueUnitOfMeasure": "degC", "interpolationMethod": "Linear", "maximumInterpolation": "1h" } } // historian/seeq/signal/create/response (shortened) { "id": "0EF2C1F0-6E6B-7AF0-9C1A-2B3C4D5E6F70", "name": "Coreflux Line1 Temperature", "type": "StoredSignal", ... } ``` Push samples into the signal: ```json theme={"theme":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} // publish to historian/seeq/sample/create { "auth": "", "id": "", "body": { "samples": [{ "key": "2026-10-05T07:00:00Z", "value": 21.4 }, { "key": "2026-10-05T07:00:10Z", "value": 21.5 }] } } ``` Read the last hour back (`start` and `end` are required): ```json theme={"theme":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} // publish to historian/seeq/sample/list { "auth": "", "id": "", "start": "2026-10-05T06:00:00Z", "end": "2026-10-05T07:00:00Z", "limit": 1000 } // historian/seeq/sample/list/response { "samples": [{ "key": "2026-10-05T06:00:10Z", "value": 20.9 }, ...], "valueUnitOfMeasure": "degC", "limit": 1000 } ``` Store an alarm as a capsule on a stored condition (create it first with `historian/seeq/condition/create` and `{ "name": "Line1 Alarms", "maximumDuration": "1d" }`): ```json theme={"theme":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} // publish to historian/seeq/capsule/create { "auth": "", "id": "", "body": { "capsules": [{ "start": "2026-10-05T06:40:00Z", "end": "2026-10-05T06:55:00Z", "properties": [{ "name": "Alarm", "value": "HighTemp", "unitOfMeasure": "string" }] }] } } ``` Run a Seeq Formula, here hourly averages of a signal (`parameters` binds `$t` to a signal ID): ```json theme={"theme":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} // publish to historian/seeq/formula/run { "auth": "", "body": { "formula": "$t.aggregate(average(), hours(), startKey())", "parameters": ["t="], "start": "2026-10-05T00:00:00Z", "end": "2026-10-05T07:00:00Z" } } ``` Example actions: keep the session alive, remember the IDs of new items, push an MQTT value as a sample, store a closed alarm as a capsule, and poll the latest value: ```lot theme={"theme":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} DEFINE ACTION SeeqKeepAlive ON EVERY 5 MINUTES DO SET "auth" WITH GET SECRET "SEEQ_AUTH_TOKEN" PUBLISH TOPIC "historian/seeq/ping/get" WITH { "auth": {auth} } DEFINE ACTION SeeqRememberSignalId ON TOPIC "historian/seeq/signal/create/response" DO SET "id" WITH (GET JSON "id" IN PAYLOAD AS STRING) KEEP TOPIC "internal/seeq/signal-id/line1-temperature" WITH {id} DEFINE ACTION SeeqRememberConditionId ON TOPIC "historian/seeq/condition/create/response" DO SET "id" WITH (GET JSON "id" IN PAYLOAD AS STRING) KEEP TOPIC "internal/seeq/condition-id/line1-alarms" WITH {id} DEFINE ACTION SeeqPushLine1Temperature ON TOPIC "plant/line1/temperature" DO SET "auth" WITH GET SECRET "SEEQ_AUTH_TOKEN" SET "sid" WITH GET TOPIC "internal/seeq/signal-id/line1-temperature" SET "value" WITH PAYLOAD AS DOUBLE SET "ts" WITH TIMESTAMP "UTC" PUBLISH TOPIC "historian/seeq/sample/create" WITH { "auth": {auth}, "id": {sid}, "body": { "samples": [ { "key": {ts}, "value": {value} } ] } } DEFINE ACTION SeeqStoreAlarmCapsule ON TOPIC "plant/line1/alarm/closed" DO SET "auth" WITH GET SECRET "SEEQ_AUTH_TOKEN" SET "cid" WITH GET TOPIC "internal/seeq/condition-id/line1-alarms" SET "start" WITH (GET JSON "start" IN PAYLOAD AS STRING) SET "end" WITH (GET JSON "end" IN PAYLOAD AS STRING) SET "alarm" WITH (GET JSON "alarm" IN PAYLOAD AS STRING) PUBLISH TOPIC "historian/seeq/capsule/create" WITH { "auth": {auth}, "id": {cid}, "body": { "capsules": [ { "start": {start}, "end": {end}, "properties": [ { "name": "Alarm", "value": {alarm}, "unitOfMeasure": "string" } ] } ] } } DEFINE ACTION SeeqPollLatestTemperature ON EVERY 1 MINUTE DO SET "auth" WITH GET SECRET "SEEQ_AUTH_TOKEN" SET "sid" WITH GET TOPIC "internal/seeq/signal-id/line1-temperature" SET "now" WITH TIMESTAMP "UTC" PUBLISH TOPIC "historian/seeq/sample/get" WITH { "auth": {auth}, "id": {sid}, "key": {now}, "lookup": "AtOrBefore" } ``` `TIMESTAMP "UTC"` must give an ISO 8601 time for Seeq to accept the sample key. Check the format on your broker version before you rely on it. ## Not covered * Getting the session token inside the broker. Seeq returns it in the `x-sq-auth` response header, and the connector publishes only the response body (see Authentication). * Workbooks, worksheets, Organizer topics and reports, assets and asset trees, signal / condition / scalar batch upserts, datasource-ID endpoints (`/signals/{datasourceClass}/{datasourceId}/{dataId}`) and calculated items. Use the same URL pattern and headers with your own event if you need them. * Seeq Data Lab, SPy, Data Lab Functions, Add-ons and the Seeq Connector SDK. * OData export (`/odata.svc`), which authenticates with the access key and its password directly. ## Related * [REST API connectors](/latest/lot-language/routes/rest-api-routes) * [REST API app catalog](/latest/lot-language/routes/rest-api-apps/index) * [Seeq APIs and SDKs](https://support.seeq.com/latest/cloud/seeq-apis-and-sdks) * [Working with Access Keys](https://support.seeq.com/latest/cloud/working-with-access-keys) # SendGrid Source: https://docs.coreflux.org/latest/lot-language/routes/rest-api-apps/sendgrid LoT connector for Twilio SendGrid v3: send email with raw content or dynamic templates, check the user profile, and list or get transactional templates Technical Preview ## Overview A client-mode connector for a **curated messaging** slice of the [Twilio SendGrid API v3](https://www.twilio.com/docs/sendgrid/api-reference/mail-send/mail-send): | Catalog connector event | Entity / path | Notes | | - | - | - | | POST mail/send | `POST /mail/send` | Raw content and/or dynamic `template_id`; **202 Accepted** | | GET user/profile | `GET /user/profile` | API-key credential check | | GET templates | `GET /templates` | `generations` + required `page_size` | | GET templates/\{id} | `GET /templates/{template_id}` | Single transactional template | Default `BASE_ADDRESS` is `https://api.sendgrid.com/v3`. Topics use `messaging/sendgrid//`. > [!NOTE] > > All events send `Authorization: Bearer {api_key}`. Prefer Hub secret `SENDGRID_API_KEY`. Inbound Event Webhook (delivered / bounce / open) → MQTT is out of scope for this client-mode package. EU regional subusers may use `https://api.eu.sendgrid.com/v3`. ## Basic Syntax ```lot wrap theme={"theme":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} # Coreflux REST_API template — Twilio SendGrid API v3 # Auth: Bearer {value.json.api_key} # BASE_ADDRESS: https://api.sendgrid.com/v3 DEFINE CONNECTOR SendGridRestApi WITH TYPE REST_API ADD REST_API_CONFIG WITH BASE_ADDRESS "https://api.sendgrid.com/v3" WITH ENABLE_CLIENT "true" WITH USE_SSL "true" WITH REQUEST_TIMEOUT '60' WITH DEFAULT_CONTENT_TYPE "application/json" ADD EVENT Getuserprofile WITH SOURCE_TOPIC "messaging/sendgrid/user/get-profile" WITH DESTINATION_TOPIC "messaging/sendgrid/user/get-profile/response" WITH METHOD "GET" WITH ENDPOINT "/user/profile" WITH HEADERS ADD "Accept" "application/json" ADD "Authorization" "Bearer {value.json.api_key}" ADD EVENT Sendmail WITH SOURCE_TOPIC "messaging/sendgrid/mail/send" WITH DESTINATION_TOPIC "messaging/sendgrid/mail/send/response" WITH METHOD "POST" WITH ENDPOINT "/mail/send" WITH HEADERS ADD "Accept" "application/json" ADD "Content-Type" "application/json" ADD "Authorization" "Bearer {value.json.api_key}" WITH BODY "{payload}" ADD EVENT Listtemplates WITH SOURCE_TOPIC "messaging/sendgrid/template/list" WITH DESTINATION_TOPIC "messaging/sendgrid/template/list/response" WITH METHOD "GET" WITH ENDPOINT "/templates?generations={value.json.generations}&page_size={value.json.page_size}" WITH HEADERS ADD "Accept" "application/json" ADD "Authorization" "Bearer {value.json.api_key}" ADD EVENT Gettemplate WITH SOURCE_TOPIC "messaging/sendgrid/template/get" WITH DESTINATION_TOPIC "messaging/sendgrid/template/get/response" WITH METHOD "GET" WITH ENDPOINT "/templates/{value.json.template_id}" WITH HEADERS ADD "Accept" "application/json" ADD "Authorization" "Bearer {value.json.api_key}" ``` ## Events | Event | Method | Endpoint | Request topic | Response topic | | - | - | - | - | - | | Getuserprofile | GET | `/user/profile` | `messaging/sendgrid/user/get-profile` | `messaging/sendgrid/user/get-profile/response` | | Sendmail | POST | `/mail/send` | `messaging/sendgrid/mail/send` | `messaging/sendgrid/mail/send/response` | | Listtemplates | GET | `/templates` | `messaging/sendgrid/template/list` | `messaging/sendgrid/template/list/response` | | Gettemplate | GET | `/templates/{template_id}` | `messaging/sendgrid/template/get` | `messaging/sendgrid/template/get/response` | ## Auth 1. Create an API key in the [SendGrid UI](https://app.sendgrid.com/settings/api_keys) with Mail Send (and preferably Template Engine Read) scopes. Use a verified sender identity for `from`. 2. Set connector `BASE_ADDRESS` to `https://api.sendgrid.com/v3` (or `https://api.eu.sendgrid.com/v3` for EU regional). 3. Pass `api_key` on the MQTT payload so `Authorization: Bearer {value.json.api_key}` resolves. 4. Store the key in Hub / `GET SECRET "SENDGRID_API_KEY"`. Never commit real credentials. 5. Successful `GET /user/profile` returns **HTTP 200**. `POST /mail/send` returns **202 Accepted**. `GET /templates` returns **200** with a paged `result` array. ## Example payloads **getUserProfile:** ```json theme={"theme":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} { "api_key": "YOUR_SENDGRID_API_KEY" } ``` **sendMail (raw):** ```json theme={"theme":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} { "api_key": "YOUR_SENDGRID_API_KEY", "personalizations": [ { "to": [{"email": "ops@example.com"}] } ], "from": {"email": "alerts@your-verified-domain.com", "name": "Coreflux"}, "subject": "Coreflux alarm: vibration above threshold — Zone A", "content": [ { "type": "text/plain", "value": "RMS exceeded limit on Zone A" } ] } ``` **sendMail (dynamic template):** ```json theme={"theme":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} { "api_key": "YOUR_SENDGRID_API_KEY", "personalizations": [ { "to": [{"email": "ops@example.com"}], "dynamic_template_data": { "zone": "A", "metric": "vibration_rms", "value": 12.4 } } ], "from": {"email": "alerts@your-verified-domain.com"}, "template_id": "d-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" } ``` **listTemplates:** ```json theme={"theme":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} { "api_key": "YOUR_SENDGRID_API_KEY", "generations": "dynamic", "page_size": "10" } ``` ## Install 1. The template ships with the broker. List it with `-listTemplates` or fetch it from the Hub catalog. 2. Deploy with `-addConnector` from `SendGridRestApi.lot` (not `hub-metadata.lot`). ## See also * [REST API Connectors](/latest/lot-language/routes/rest-api-routes) * [REST API Apps](/latest/lot-language/routes/rest-api-apps) # Sentry Source: https://docs.coreflux.org/latest/lot-language/routes/rest-api-apps/sentry Client-mode REST API connector for a curated Sentry slice: list project issues, get an issue, and resolve it Technical Preview # Sentry Client-mode REST API connector for a curated Sentry slice: list project issues, get an issue, and resolve it. * Hub category: **Monitoring**. * Base: `https://sentry.io` ## Template reference Generated from the shipped broker template `sentry` (`-connectorCode SENTRY`). It lists every event the template defines. ### Auth Auth headers used by the template: `Authorization: Bearer {value.json.api_token}`. ### Template parameters | Placeholder | Type | Description | Required | | - | - | - | - | | `RouteName` | string | Name of the Sentry connector | no | | `BaseUrl` | string | Sentry base URL without trailing slash (e.g. [https://sentry.io](https://sentry.io)) | yes | `BASE_ADDRESS` default: `https://sentry.io`. ### Events | Event | Method | Endpoint | Request topic | Response topic | Payload fields | | - | - | - | - | - | - | | `Listissues` | GET | `/api/0/projects/{value.json.organization}/{value.json.project}/issues/` | `monitoring/sentry/issues/list` | `monitoring/sentry/issues/list/response` | `api_token`, `organization`, `project` | | `Getissue` | GET | `/api/0/issues/{value.json.issue_id}/` | `monitoring/sentry/issues/get` | `monitoring/sentry/issues/get/response` | `api_token`, `issue_id` | | `Resolveissue` | PUT | `/api/0/issues/{value.json.issue_id}/` | `monitoring/sentry/issues/resolve` | `monitoring/sentry/issues/resolve/response` | `api_token`, `body`, `issue_id` | ### Deploy Get the template card (JSON with `template` and `metadata`) from the broker, then deploy the connector: ```bash theme={"theme":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} mosquitto_pub -t '$SYS/Coreflux/Command' -m "-connectorCode SENTRY" ``` The deployable connector from the REST API app, with `BASE_ADDRESS` already filled in: ```lot theme={"theme":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} DEFINE CONNECTOR SentryRestApi WITH TYPE REST_API ADD REST_API_CONFIG WITH BASE_ADDRESS "https://sentry.io" WITH ENABLE_CLIENT "true" WITH USE_SSL "true" WITH REQUEST_TIMEOUT '60' WITH DEFAULT_CONTENT_TYPE "application/json" ADD EVENT Listissues WITH SOURCE_TOPIC "monitoring/sentry/issues/list" WITH DESTINATION_TOPIC "monitoring/sentry/issues/list/response" WITH METHOD "GET" WITH ENDPOINT "/api/0/projects/{value.json.organization}/{value.json.project}/issues/" WITH HEADERS ADD "Accept" "application/json" ADD "Authorization" "Bearer {value.json.api_token}" ADD EVENT Getissue WITH SOURCE_TOPIC "monitoring/sentry/issues/get" WITH DESTINATION_TOPIC "monitoring/sentry/issues/get/response" WITH METHOD "GET" WITH ENDPOINT "/api/0/issues/{value.json.issue_id}/" WITH HEADERS ADD "Accept" "application/json" ADD "Authorization" "Bearer {value.json.api_token}" ADD EVENT Resolveissue WITH SOURCE_TOPIC "monitoring/sentry/issues/resolve" WITH DESTINATION_TOPIC "monitoring/sentry/issues/resolve/response" WITH METHOD "PUT" WITH ENDPOINT "/api/0/issues/{value.json.issue_id}/" WITH HEADERS ADD "Accept" "application/json" ADD "Content-Type" "application/json" ADD "Authorization" "Bearer {value.json.api_token}" WITH BODY "{value.json.body}" ``` Deploy it with `-addConnector` (do not include `ADD METADATA`). ### Usage example Call `Listissues` by publishing a JSON payload to its request topic, and read the reply on the response topic: ```bash theme={"theme":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} mosquitto_sub -t 'monitoring/sentry/issues/list/response' & mosquitto_pub -t 'monitoring/sentry/issues/list' -m '{"api_token": "", "organization": "", "project": ""}' ``` # Sepasoft MES for Ignition Source: https://docs.coreflux.org/latest/lot-language/routes/rest-api-apps/sepasoft-mes-for-ignition LoT connector for Sepasoft MES on Ignition: OEE counts and equipment state, Track and Trace genealogy, work order schedules and gateway health Technical Preview ## Overview One client-mode connector, **SepasoftMES**, for Sepasoft MES modules (OEE Downtime, Track and Trace, Settings and Changeover) running on an Inductive Automation Ignition gateway. It moves MES data between your Unified Namespace and the gateway over HTTP. Sepasoft does not publish a fixed REST API. You author REST endpoints yourself in the Ignition Designer with the Sepasoft **Web Services** module (Web Services, then Providers, then RESTful Endpoints), each one a Python script for `GET`, `POST`, `PUT` or `DELETE`, and the gateway serves them under `/system/ws/rest//`. So the connector has three groups of events: 1. **Gateway health**, no credentials: `/StatusPing` and `/system/gwinfo`. 2. **Generic passthroughs** for any Web Services RESTful endpoint or Ignition WebDev resource you already have. 3. **MES presets** that call a small companion provider folder, `coreflux/v1`. Example scripts for it ship with the connector template (`sepasoft-provider/`), built on documented `system.mes.*` functions. | Event | Request | Notes | | - | - | - | | GatewayStatusPing | `GET /StatusPing` | Returns `{"state":"RUNNING"}` when the gateway is up; no auth | | GatewayInfo | `GET /system/gwinfo` | Version, platform name and redundancy state as `key=value;` text; no auth | | WsRestGet | `GET /system/ws/rest/{resource}?{query}` | Any RESTful endpoint `do_GET` | | WsRestPost | `POST /system/ws/rest/{resource}` | Any RESTful endpoint `do_POST`, JSON body | | WsRestPut | `PUT /system/ws/rest/{resource}` | Any RESTful endpoint `do_PUT`, JSON body | | WsRestDelete | `DELETE /system/ws/rest/{resource}?{query}` | Any RESTful endpoint `do_DELETE` | | WebDevGet | `GET /system/webdev/{project}/{resource}?{query}` | Ignition WebDev resource `doGet` | | WebDevPost | `POST /system/webdev/{project}/{resource}` | Ignition WebDev resource `doPost` | | OeeCountInput | `POST /system/ws/rest/coreflux/v1/oee/countinput` | Writes an infeed, outfeed or reject count to the tag your OEE production model reads | | OeeEquipmentState | `POST /system/ws/rest/coreflux/v1/oee/equipmentstate` | Writes an equipment state code to the OEE state tag | | OeeAnalysis | `GET /system/ws/rest/coreflux/v1/oee/analysis?{query}` | Runs an MES analysis for `eqPath`, `startDate`, `endDate` | | TraceMaterialMove | `POST /system/ws/rest/coreflux/v1/trace/materialmove` | Executes a Track and Trace segment with its materials | | TraceGenealogy | `GET /system/ws/rest/coreflux/v1/trace/genealogy?{query}` | Lot genealogy for `lot` | | ScheduleRuns | `GET /system/ws/rest/coreflux/v1/schedule/runs?{query}` | Scheduled operations requests for `workOrder` between `begin` and `end` | | WorkOrderGet | `GET /system/ws/rest/coreflux/v1/workorder/get?{query}` | Work order quantity and scheduled quantity for `name` | Topics follow `mes/sepasoft//`, and each reply arrives on `/response`. ## Prerequisites * An Ignition gateway (8.x) with the Sepasoft MES modules you use and the Sepasoft **Web Services** module. * For the MES presets, the `coreflux/v1` provider folder (see below). The generic events work with any endpoint you already have. * An Ignition user in the User Source picked on each endpoint, with a role allowed by the endpoint's Required role(s). * Network reach from the broker to the gateway's HTTPS port (8043 by default) or HTTP port (8088). ## Authentication Each RESTful endpoint can require **HTTP Basic** authentication against an Ignition User Source, optionally limited to some roles. Missing credentials return `401` with a `WWW-Authenticate` header and wrong credentials return `403`. An HTTP method with no script returns `405`, and a script syntax error returns `500`. Store the full header value, `Basic `, in a broker secret (suggested `SEPASOFT_BASIC_AUTH`) and map it into the `authorization` payload field with `GET SECRET`. The two gateway health events send no credentials. ## Connector ```lot theme={"theme":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} DEFINE CONNECTOR SepasoftMES WITH TYPE REST_API ADD REST_API_CONFIG WITH BASE_ADDRESS "https://YOUR_IGNITION_GATEWAY:8043" WITH ENABLE_CLIENT "true" WITH USE_SSL "true" WITH REQUEST_TIMEOUT '30' WITH DEFAULT_CONTENT_TYPE "application/json" ADD EVENT GatewayStatusPing WITH SOURCE_TOPIC "mes/sepasoft/gateway-status/get" WITH DESTINATION_TOPIC "mes/sepasoft/gateway-status/get/response" WITH METHOD "GET" WITH ENDPOINT "/StatusPing" WITH HEADERS ADD "Accept" "application/json" ADD EVENT WsRestGet WITH SOURCE_TOPIC "mes/sepasoft/ws-rest/get" WITH DESTINATION_TOPIC "mes/sepasoft/ws-rest/get/response" WITH METHOD "GET" WITH ENDPOINT "/system/ws/rest/{value.json.resource}?{value.json.query}" WITH HEADERS ADD "Accept" "application/json" ADD "Authorization" "{value.json.authorization}" ADD EVENT OeeCountInput WITH SOURCE_TOPIC "mes/sepasoft/oee-count/create" WITH DESTINATION_TOPIC "mes/sepasoft/oee-count/create/response" WITH METHOD "POST" WITH ENDPOINT "/system/ws/rest/coreflux/v1/oee/countinput" WITH HEADERS ADD "Accept" "application/json" ADD "Content-Type" "application/json" ADD "Authorization" "{value.json.authorization}" WITH BODY "{value.json.body}" ``` The full template, `SepasoftMesForIgnitionRestApi.lot`, has all 15 events. Ignition 7.9 mounted providers under `/main/system/ws/rest/...`; on 7.9, change the endpoint prefix. ## Payload fields | Field | Used by | Example | | - | - | - | | `authorization` | every event except the two gateway events | `"Basic Y29yZWZsdXg6Y2hhbmdlLW1l"` (from `GET SECRET`) | | `resource` | `WsRest*`, `WebDev*` | `"v1/line-status"` (folder and resource name under the mount) | | `project` | `WebDev*` | `"MESDemo"` | | `query` | GET and DELETE events | `"eqPath=Enterprise%5CSite%5CLine%201&startDate=2026-10-05T00:00:00&endDate=2026-10-05T08:00:00"`; send `""` when empty | | `body` | POST and PUT events | JSON object; the provider receives it as `request['body']` | ## Companion provider `coreflux/v1` In the Designer, create the folders `coreflux` and `v1` under RESTful Endpoints and four endpoints named `oee`, `trace`, `schedule` and `workorder`. Set Data format to JSON, HTTP Authentication to Basic, choose the User Source and roles, tick Redirect to SSL, and paste the matching example script. The part of the URL after the resource name (for example `/countinput`) reaches the script as `request['path']`. | Script | Sub-paths | Sepasoft and Ignition functions | | - | - | - | | `oee.py` | `POST /countinput`, `POST /equipmentstate`, `GET /analysis` | `system.tag.writeBlocking`, `system.mes.analysis.createMESAnalysisSettings`, `system.mes.analysis.executeAnalysis` | | `trace.py` | `POST /materialmove`, `GET /genealogy` | `system.mes.createSegment`, `setMaterial`, `execute`, `system.mes.trace.getTraceabilityData` | | `schedule.py` | `GET /runs` | `system.mes.workorder.getMESWorkOrder`, `getOperationsRequests(begin, end)` | | `workorder.py` | `GET /get` | `system.mes.workorder.getMESWorkOrder`, `updateQuantities`, `getWorkOrderQuantity`, `getScheduledQuantity` | > [!WARNING] > > The scripts are starting points. Test each one on a development gateway with the Designer's Copy RESTful Endpoint URL before you connect the broker. `OeeCountInput`, `OeeEquipmentState` and `TraceMaterialMove` change production records, so restrict who may publish on those topics with broker rules. ## Examples Check the gateway every minute: ```lot theme={"theme":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} DEFINE ACTION SepasoftGatewayHealth ON EVERY 1 MINUTE DO PUBLISH TOPIC "mes/sepasoft/gateway-status/get" WITH { "query": "" } ``` Forward a line counter to OEE: ```lot theme={"theme":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} DEFINE ACTION SepasoftLineCount ON TOPIC "porto/line1/infeed/count" DO SET "auth" WITH GET SECRET "SEPASOFT_BASIC_AUTH" SET "count" WITH PAYLOAD AS INT PUBLISH TOPIC "mes/sepasoft/oee-count/create" WITH { "authorization": {auth}, "body": { "tag_path": "[default]Line1/Infeed Count", "value": {count} } } ``` Record a material move in Track and Trace (publish to `mes/sepasoft/trace-material-move/create`): ```json theme={"theme":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} { "authorization": "Basic ", "body": { "segment": "Mix Dressing", "equipment_path": "Dressings Inc\\California\\Mixing\\Mixer 1", "materials": [{ "property": "Vinegar", "lot": "LOT-0042", "quantity": 20 }] } } ``` ## Troubleshooting | Symptom | Cause | | - | - | | `401` with `WWW-Authenticate` | `authorization` missing or empty | | `403` | Wrong user or password, or the user lacks a required role | | `404` | Wrong folder or resource name, or a 7.9 gateway that needs `/main/system/ws/rest` | | `405` | The endpoint has no script for that HTTP method | | `500` | Syntax error in the endpoint script, or the script did not return a dict with integer `code` and dict `content` | ## Related * [REST API connectors](/latest/lot-language/routes/rest-api-routes) * [REST API app templates](/latest/lot-language/routes/rest-api-apps/index) * Sepasoft docs: [RESTful Endpoints](https://docs.sepasoft.com/articles/user-manual/restful-endpoints), [Serving up Analysis Data as a Web Service REST Endpoint](https://docs.sepasoft.com/articles/knowledge-base/serving-up-analysis-data-as-a-web-service-rest-endpoint) # Service Titan Api Source: https://docs.coreflux.org/latest/lot-language/routes/rest-api-apps/service-titan-api LoT connector for ServiceTitan: create jobs from equipment faults, add appointments, assign technicians, and register and update installed equipment Technical Preview ## Overview Client-mode REST API connector for ServiceTitan field service: create jobs from connected-equipment faults, patch, note and cancel them, attach installed equipment, add appointments and assign technicians, track technicians, register and update installed equipment, and read locations and lookup lists, with OAuth 2.0 client credentials plus the ST-App-Key header. This is one of the templates in the ServiceTitan REST API app (`servicetitan`). Deploy it alongside the other connectors of that app when they depend on each other (for example a token connector plus an API connector). ## Auth OAuth 2.0 client credentials: GetToken posts grant\_type=client\_credentials\&client\_id\&client\_secret (form) to [https://auth.servicetitan.io/connect/token](https://auth.servicetitan.io/connect/token); tokens last 900 s, no refresh token. API events: Authorization: \ (raw, as in the portal's first-call sample) + ST-App-Key: \ from payload; tenant id in the path. Every event also sends User-Agent: Coreflux-Broker. Auth headers used by the template: `Authorization: {value.json.access_token}`, `ST-App-Key: {value.json.app_key}`. ## Template parameters | Placeholder | Type | Description | Required | | - | - | - | - | | `RouteName` | string | Name of the Service Titan Api connector | no | | `BaseUrl` | string | Service Titan Api base URL without trailing slash (e.g. [https://api.servicetitan.io](https://api.servicetitan.io)) | yes | `BASE_ADDRESS` default: `https://api.servicetitan.io`. ## Events | Event | Method | Endpoint | Request topic | Response topic | Payload fields | | - | - | - | - | - | - | | `ListJobs` | GET | `/jpm/v2/tenant/{value.json.tenant}/jobs?{value.json.query}` | `cmms/servicetitan/job/list` | `cmms/servicetitan/job/list/response` | `access_token`, `app_key`, `query`, `tenant` | | `GetJob` | GET | `/jpm/v2/tenant/{value.json.tenant}/jobs/{value.json.id}` | `cmms/servicetitan/job/get` | `cmms/servicetitan/job/get/response` | `access_token`, `app_key`, `id`, `tenant` | | `CreateJob` | POST | `/jpm/v2/tenant/{value.json.tenant}/jobs` | `cmms/servicetitan/job/create` | `cmms/servicetitan/job/create/response` | `access_token`, `app_key`, `body`, `tenant` | | `UpdateJob` | PATCH | `/jpm/v2/tenant/{value.json.tenant}/jobs/{value.json.id}` | `cmms/servicetitan/job/update` | `cmms/servicetitan/job/update/response` | `access_token`, `app_key`, `body`, `id`, `tenant` | | `CancelJob` | PUT | `/jpm/v2/tenant/{value.json.tenant}/jobs/{value.json.id}/cancel` | `cmms/servicetitan/job/cancel` | `cmms/servicetitan/job/cancel/response` | `access_token`, `app_key`, `body`, `id`, `tenant` | | `ListJobCancelReasons` | GET | `/jpm/v2/tenant/{value.json.tenant}/job-cancel-reasons?{value.json.query}` | `cmms/servicetitan/job-cancel-reason/list` | `cmms/servicetitan/job-cancel-reason/list/response` | `access_token`, `app_key`, `query`, `tenant` | | `ListJobTypes` | GET | `/jpm/v2/tenant/{value.json.tenant}/job-types?{value.json.query}` | `cmms/servicetitan/job-type/list` | `cmms/servicetitan/job-type/list/response` | `access_token`, `app_key`, `query`, `tenant` | | `ListJobNotes` | GET | `/jpm/v2/tenant/{value.json.tenant}/jobs/{value.json.id}/notes?{value.json.query}` | `cmms/servicetitan/job-note/list` | `cmms/servicetitan/job-note/list/response` | `access_token`, `app_key`, `id`, `query`, `tenant` | | `CreateJobNote` | POST | `/jpm/v2/tenant/{value.json.tenant}/jobs/{value.json.id}/notes` | `cmms/servicetitan/job-note/create` | `cmms/servicetitan/job-note/create/response` | `access_token`, `app_key`, `body`, `id`, `tenant` | | `ListJobEquipment` | GET | `/jpm/v2/tenant/{value.json.tenant}/jobs/{value.json.id}/equipment` | `cmms/servicetitan/job-equipment/list` | `cmms/servicetitan/job-equipment/list/response` | `access_token`, `app_key`, `id`, `tenant` | | `AttachJobEquipment` | POST | `/jpm/v2/tenant/{value.json.tenant}/jobs/{value.json.id}/equipment` | `cmms/servicetitan/job-equipment/attach` | `cmms/servicetitan/job-equipment/attach/response` | `access_token`, `app_key`, `body`, `id`, `tenant` | | `ExportJobs` | GET | `/jpm/v2/tenant/{value.json.tenant}/export/jobs?{value.json.query}` | `cmms/servicetitan/job/export` | `cmms/servicetitan/job/export/response` | `access_token`, `app_key`, `query`, `tenant` | | `ListAppointments` | GET | `/jpm/v2/tenant/{value.json.tenant}/appointments?{value.json.query}` | `cmms/servicetitan/appointment/list` | `cmms/servicetitan/appointment/list/response` | `access_token`, `app_key`, `query`, `tenant` | | `AddAppointment` | POST | `/jpm/v2/tenant/{value.json.tenant}/appointments` | `cmms/servicetitan/appointment/create` | `cmms/servicetitan/appointment/create/response` | `access_token`, `app_key`, `body`, `tenant` | | `ListAppointmentAssignments` | GET | `/dispatch/v2/tenant/{value.json.tenant}/appointment-assignments?{value.json.query}` | `cmms/servicetitan/assignment/list` | `cmms/servicetitan/assignment/list/response` | `access_token`, `app_key`, `query`, `tenant` | | `AssignTechnicians` | POST | `/dispatch/v2/tenant/{value.json.tenant}/appointment-assignments/assign-technicians` | `cmms/servicetitan/assignment/assign` | `cmms/servicetitan/assignment/assign/response` | `access_token`, `app_key`, `body`, `tenant` | | `GetTechnicianTracking` | GET | `/dispatch/v2/tenant/{value.json.tenant}/technician-tracking?{value.json.query}` | `cmms/servicetitan/technician-tracking/get` | `cmms/servicetitan/technician-tracking/get/response` | `access_token`, `app_key`, `query`, `tenant` | | `ListTechnicianShifts` | GET | `/dispatch/v2/tenant/{value.json.tenant}/technician-shifts?{value.json.query}` | `cmms/servicetitan/technician-shift/list` | `cmms/servicetitan/technician-shift/list/response` | `access_token`, `app_key`, `query`, `tenant` | | `ListTechnicians` | GET | `/settings/v2/tenant/{value.json.tenant}/technicians?{value.json.query}` | `cmms/servicetitan/technician/list` | `cmms/servicetitan/technician/list/response` | `access_token`, `app_key`, `query`, `tenant` | | `ListBusinessUnits` | GET | `/settings/v2/tenant/{value.json.tenant}/business-units?{value.json.query}` | `cmms/servicetitan/business-unit/list` | `cmms/servicetitan/business-unit/list/response` | `access_token`, `app_key`, `query`, `tenant` | | `ListInstalledEquipment` | GET | `/equipmentsystems/v2/tenant/{value.json.tenant}/installed-equipment?{value.json.query}` | `cmms/servicetitan/equipment/list` | `cmms/servicetitan/equipment/list/response` | `access_token`, `app_key`, `query`, `tenant` | | `GetInstalledEquipment` | GET | `/equipmentsystems/v2/tenant/{value.json.tenant}/installed-equipment/{value.json.id}` | `cmms/servicetitan/equipment/get` | `cmms/servicetitan/equipment/get/response` | `access_token`, `app_key`, `id`, `tenant` | | `CreateInstalledEquipment` | POST | `/equipmentsystems/v2/tenant/{value.json.tenant}/installed-equipment` | `cmms/servicetitan/equipment/create` | `cmms/servicetitan/equipment/create/response` | `access_token`, `app_key`, `body`, `tenant` | | `UpdateInstalledEquipment` | PATCH | `/equipmentsystems/v2/tenant/{value.json.tenant}/installed-equipment/{value.json.id}` | `cmms/servicetitan/equipment/update` | `cmms/servicetitan/equipment/update/response` | `access_token`, `app_key`, `body`, `id`, `tenant` | | `ListEquipmentTypes` | GET | `/equipmentsystems/v2/tenant/{value.json.tenant}/equipment-types?{value.json.query}` | `cmms/servicetitan/equipment-type/list` | `cmms/servicetitan/equipment-type/list/response` | `access_token`, `app_key`, `query`, `tenant` | | `ListEquipmentSystems` | GET | `/equipmentsystems/v2/tenant/{value.json.tenant}/equipment-systems?{value.json.query}` | `cmms/servicetitan/equipment-system/list` | `cmms/servicetitan/equipment-system/list/response` | `access_token`, `app_key`, `query`, `tenant` | | `ListLocations` | GET | `/crm/v2/tenant/{value.json.tenant}/locations?{value.json.query}` | `cmms/servicetitan/location/list` | `cmms/servicetitan/location/list/response` | `access_token`, `app_key`, `query`, `tenant` | | `GetLocation` | GET | `/crm/v2/tenant/{value.json.tenant}/locations/{value.json.id}` | `cmms/servicetitan/location/get` | `cmms/servicetitan/location/get/response` | `access_token`, `app_key`, `id`, `tenant` | | `CreateLocationNote` | POST | `/crm/v2/tenant/{value.json.tenant}/locations/{value.json.id}/notes` | `cmms/servicetitan/location-note/create` | `cmms/servicetitan/location-note/create/response` | `access_token`, `app_key`, `body`, `id`, `tenant` | ## Deploy Get the template card (JSON with `template` and `metadata`) from the broker, then deploy the connector: ```bash theme={"theme":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} mosquitto_pub -t '$SYS/Coreflux/Command' -m "-connectorCode SERVICETITAN" ``` The deployable connector from the REST API app, with `BASE_ADDRESS` already filled in: ```lot theme={"theme":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} DEFINE CONNECTOR ServiceTitanApi WITH TYPE REST_API ADD REST_API_CONFIG WITH BASE_ADDRESS "https://api.servicetitan.io" WITH ENABLE_CLIENT "true" WITH USE_SSL "true" WITH REQUEST_TIMEOUT '30' WITH DEFAULT_CONTENT_TYPE "application/json" ADD EVENT ListJobs WITH SOURCE_TOPIC "cmms/servicetitan/job/list" WITH DESTINATION_TOPIC "cmms/servicetitan/job/list/response" WITH METHOD "GET" WITH ENDPOINT "/jpm/v2/tenant/{value.json.tenant}/jobs?{value.json.query}" WITH HEADERS ADD "Accept" "application/json" ADD "User-Agent" "Coreflux-Broker" ADD "Authorization" "{value.json.access_token}" ADD "ST-App-Key" "{value.json.app_key}" ADD EVENT GetJob WITH SOURCE_TOPIC "cmms/servicetitan/job/get" WITH DESTINATION_TOPIC "cmms/servicetitan/job/get/response" WITH METHOD "GET" WITH ENDPOINT "/jpm/v2/tenant/{value.json.tenant}/jobs/{value.json.id}" WITH HEADERS ADD "Accept" "application/json" ADD "User-Agent" "Coreflux-Broker" ADD "Authorization" "{value.json.access_token}" ADD "ST-App-Key" "{value.json.app_key}" ADD EVENT CreateJob WITH SOURCE_TOPIC "cmms/servicetitan/job/create" WITH DESTINATION_TOPIC "cmms/servicetitan/job/create/response" WITH METHOD "POST" WITH ENDPOINT "/jpm/v2/tenant/{value.json.tenant}/jobs" WITH HEADERS ADD "Accept" "application/json" ADD "User-Agent" "Coreflux-Broker" ADD "Authorization" "{value.json.access_token}" ADD "ST-App-Key" "{value.json.app_key}" ADD "Content-Type" "application/json" WITH BODY "{value.json.body}" ADD EVENT UpdateJob WITH SOURCE_TOPIC "cmms/servicetitan/job/update" WITH DESTINATION_TOPIC "cmms/servicetitan/job/update/response" WITH METHOD "PATCH" WITH ENDPOINT "/jpm/v2/tenant/{value.json.tenant}/jobs/{value.json.id}" WITH HEADERS ADD "Accept" "application/json" ADD "User-Agent" "Coreflux-Broker" ADD "Authorization" "{value.json.access_token}" ADD "ST-App-Key" "{value.json.app_key}" ADD "Content-Type" "application/json" WITH BODY "{value.json.body}" ADD EVENT CancelJob WITH SOURCE_TOPIC "cmms/servicetitan/job/cancel" WITH DESTINATION_TOPIC "cmms/servicetitan/job/cancel/response" WITH METHOD "PUT" WITH ENDPOINT "/jpm/v2/tenant/{value.json.tenant}/jobs/{value.json.id}/cancel" WITH HEADERS ADD "Accept" "application/json" ADD "User-Agent" "Coreflux-Broker" ADD "Authorization" "{value.json.access_token}" ADD "ST-App-Key" "{value.json.app_key}" ADD "Content-Type" "application/json" WITH BODY "{value.json.body}" ADD EVENT ListJobCancelReasons WITH SOURCE_TOPIC "cmms/servicetitan/job-cancel-reason/list" WITH DESTINATION_TOPIC "cmms/servicetitan/job-cancel-reason/list/response" WITH METHOD "GET" WITH ENDPOINT "/jpm/v2/tenant/{value.json.tenant}/job-cancel-reasons?{value.json.query}" WITH HEADERS ADD "Accept" "application/json" ADD "User-Agent" "Coreflux-Broker" ADD "Authorization" "{value.json.access_token}" ADD "ST-App-Key" "{value.json.app_key}" ADD EVENT ListJobTypes WITH SOURCE_TOPIC "cmms/servicetitan/job-type/list" WITH DESTINATION_TOPIC "cmms/servicetitan/job-type/list/response" WITH METHOD "GET" WITH ENDPOINT "/jpm/v2/tenant/{value.json.tenant}/job-types?{value.json.query}" WITH HEADERS ADD "Accept" "application/json" ADD "User-Agent" "Coreflux-Broker" ADD "Authorization" "{value.json.access_token}" ADD "ST-App-Key" "{value.json.app_key}" ADD EVENT ListJobNotes WITH SOURCE_TOPIC "cmms/servicetitan/job-note/list" WITH DESTINATION_TOPIC "cmms/servicetitan/job-note/list/response" WITH METHOD "GET" WITH ENDPOINT "/jpm/v2/tenant/{value.json.tenant}/jobs/{value.json.id}/notes?{value.json.query}" WITH HEADERS ADD "Accept" "application/json" ADD "User-Agent" "Coreflux-Broker" ADD "Authorization" "{value.json.access_token}" ADD "ST-App-Key" "{value.json.app_key}" ADD EVENT CreateJobNote WITH SOURCE_TOPIC "cmms/servicetitan/job-note/create" WITH DESTINATION_TOPIC "cmms/servicetitan/job-note/create/response" WITH METHOD "POST" WITH ENDPOINT "/jpm/v2/tenant/{value.json.tenant}/jobs/{value.json.id}/notes" WITH HEADERS ADD "Accept" "application/json" ADD "User-Agent" "Coreflux-Broker" ADD "Authorization" "{value.json.access_token}" ADD "ST-App-Key" "{value.json.app_key}" ADD "Content-Type" "application/json" WITH BODY "{value.json.body}" ADD EVENT ListJobEquipment WITH SOURCE_TOPIC "cmms/servicetitan/job-equipment/list" WITH DESTINATION_TOPIC "cmms/servicetitan/job-equipment/list/response" WITH METHOD "GET" WITH ENDPOINT "/jpm/v2/tenant/{value.json.tenant}/jobs/{value.json.id}/equipment" WITH HEADERS ADD "Accept" "application/json" ADD "User-Agent" "Coreflux-Broker" ADD "Authorization" "{value.json.access_token}" ADD "ST-App-Key" "{value.json.app_key}" ADD EVENT AttachJobEquipment WITH SOURCE_TOPIC "cmms/servicetitan/job-equipment/attach" WITH DESTINATION_TOPIC "cmms/servicetitan/job-equipment/attach/response" WITH METHOD "POST" WITH ENDPOINT "/jpm/v2/tenant/{value.json.tenant}/jobs/{value.json.id}/equipment" WITH HEADERS ADD "Accept" "application/json" ADD "User-Agent" "Coreflux-Broker" ADD "Authorization" "{value.json.access_token}" ADD "ST-App-Key" "{value.json.app_key}" ADD "Content-Type" "application/json" WITH BODY "{value.json.body}" ADD EVENT ExportJobs WITH SOURCE_TOPIC "cmms/servicetitan/job/export" WITH DESTINATION_TOPIC "cmms/servicetitan/job/export/response" WITH METHOD "GET" WITH ENDPOINT "/jpm/v2/tenant/{value.json.tenant}/export/jobs?{value.json.query}" WITH HEADERS ADD "Accept" "application/json" ADD "User-Agent" "Coreflux-Broker" ADD "Authorization" "{value.json.access_token}" ADD "ST-App-Key" "{value.json.app_key}" ADD EVENT ListAppointments WITH SOURCE_TOPIC "cmms/servicetitan/appointment/list" WITH DESTINATION_TOPIC "cmms/servicetitan/appointment/list/response" WITH METHOD "GET" WITH ENDPOINT "/jpm/v2/tenant/{value.json.tenant}/appointments?{value.json.query}" WITH HEADERS ADD "Accept" "application/json" ADD "User-Agent" "Coreflux-Broker" ADD "Authorization" "{value.json.access_token}" ADD "ST-App-Key" "{value.json.app_key}" ADD EVENT AddAppointment WITH SOURCE_TOPIC "cmms/servicetitan/appointment/create" WITH DESTINATION_TOPIC "cmms/servicetitan/appointment/create/response" WITH METHOD "POST" WITH ENDPOINT "/jpm/v2/tenant/{value.json.tenant}/appointments" WITH HEADERS ADD "Accept" "application/json" ADD "User-Agent" "Coreflux-Broker" ADD "Authorization" "{value.json.access_token}" ADD "ST-App-Key" "{value.json.app_key}" ADD "Content-Type" "application/json" WITH BODY "{value.json.body}" ADD EVENT ListAppointmentAssignments WITH SOURCE_TOPIC "cmms/servicetitan/assignment/list" WITH DESTINATION_TOPIC "cmms/servicetitan/assignment/list/response" WITH METHOD "GET" WITH ENDPOINT "/dispatch/v2/tenant/{value.json.tenant}/appointment-assignments?{value.json.query}" WITH HEADERS ADD "Accept" "application/json" ADD "User-Agent" "Coreflux-Broker" ADD "Authorization" "{value.json.access_token}" ADD "ST-App-Key" "{value.json.app_key}" ADD EVENT AssignTechnicians WITH SOURCE_TOPIC "cmms/servicetitan/assignment/assign" WITH DESTINATION_TOPIC "cmms/servicetitan/assignment/assign/response" WITH METHOD "POST" WITH ENDPOINT "/dispatch/v2/tenant/{value.json.tenant}/appointment-assignments/assign-technicians" WITH HEADERS ADD "Accept" "application/json" ADD "User-Agent" "Coreflux-Broker" ADD "Authorization" "{value.json.access_token}" ADD "ST-App-Key" "{value.json.app_key}" ADD "Content-Type" "application/json" WITH BODY "{value.json.body}" ADD EVENT GetTechnicianTracking WITH SOURCE_TOPIC "cmms/servicetitan/technician-tracking/get" WITH DESTINATION_TOPIC "cmms/servicetitan/technician-tracking/get/response" WITH METHOD "GET" WITH ENDPOINT "/dispatch/v2/tenant/{value.json.tenant}/technician-tracking?{value.json.query}" WITH HEADERS ADD "Accept" "application/json" ADD "User-Agent" "Coreflux-Broker" ADD "Authorization" "{value.json.access_token}" ADD "ST-App-Key" "{value.json.app_key}" ADD EVENT ListTechnicianShifts WITH SOURCE_TOPIC "cmms/servicetitan/technician-shift/list" WITH DESTINATION_TOPIC "cmms/servicetitan/technician-shift/list/response" WITH METHOD "GET" WITH ENDPOINT "/dispatch/v2/tenant/{value.json.tenant}/technician-shifts?{value.json.query}" WITH HEADERS ADD "Accept" "application/json" ADD "User-Agent" "Coreflux-Broker" ADD "Authorization" "{value.json.access_token}" ADD "ST-App-Key" "{value.json.app_key}" ADD EVENT ListTechnicians WITH SOURCE_TOPIC "cmms/servicetitan/technician/list" WITH DESTINATION_TOPIC "cmms/servicetitan/technician/list/response" WITH METHOD "GET" WITH ENDPOINT "/settings/v2/tenant/{value.json.tenant}/technicians?{value.json.query}" WITH HEADERS ADD "Accept" "application/json" ADD "User-Agent" "Coreflux-Broker" ADD "Authorization" "{value.json.access_token}" ADD "ST-App-Key" "{value.json.app_key}" ADD EVENT ListBusinessUnits WITH SOURCE_TOPIC "cmms/servicetitan/business-unit/list" WITH DESTINATION_TOPIC "cmms/servicetitan/business-unit/list/response" WITH METHOD "GET" WITH ENDPOINT "/settings/v2/tenant/{value.json.tenant}/business-units?{value.json.query}" WITH HEADERS ADD "Accept" "application/json" ADD "User-Agent" "Coreflux-Broker" ADD "Authorization" "{value.json.access_token}" ADD "ST-App-Key" "{value.json.app_key}" ADD EVENT ListInstalledEquipment WITH SOURCE_TOPIC "cmms/servicetitan/equipment/list" WITH DESTINATION_TOPIC "cmms/servicetitan/equipment/list/response" WITH METHOD "GET" WITH ENDPOINT "/equipmentsystems/v2/tenant/{value.json.tenant}/installed-equipment?{value.json.query}" WITH HEADERS ADD "Accept" "application/json" ADD "User-Agent" "Coreflux-Broker" ADD "Authorization" "{value.json.access_token}" ADD "ST-App-Key" "{value.json.app_key}" ADD EVENT GetInstalledEquipment WITH SOURCE_TOPIC "cmms/servicetitan/equipment/get" WITH DESTINATION_TOPIC "cmms/servicetitan/equipment/get/response" WITH METHOD "GET" WITH ENDPOINT "/equipmentsystems/v2/tenant/{value.json.tenant}/installed-equipment/{value.json.id}" WITH HEADERS ADD "Accept" "application/json" ADD "User-Agent" "Coreflux-Broker" ADD "Authorization" "{value.json.access_token}" ADD "ST-App-Key" "{value.json.app_key}" ADD EVENT CreateInstalledEquipment WITH SOURCE_TOPIC "cmms/servicetitan/equipment/create" WITH DESTINATION_TOPIC "cmms/servicetitan/equipment/create/response" WITH METHOD "POST" WITH ENDPOINT "/equipmentsystems/v2/tenant/{value.json.tenant}/installed-equipment" WITH HEADERS ADD "Accept" "application/json" ADD "User-Agent" "Coreflux-Broker" ADD "Authorization" "{value.json.access_token}" ADD "ST-App-Key" "{value.json.app_key}" ADD "Content-Type" "application/json" WITH BODY "{value.json.body}" ADD EVENT UpdateInstalledEquipment WITH SOURCE_TOPIC "cmms/servicetitan/equipment/update" WITH DESTINATION_TOPIC "cmms/servicetitan/equipment/update/response" WITH METHOD "PATCH" WITH ENDPOINT "/equipmentsystems/v2/tenant/{value.json.tenant}/installed-equipment/{value.json.id}" WITH HEADERS ADD "Accept" "application/json" ADD "User-Agent" "Coreflux-Broker" ADD "Authorization" "{value.json.access_token}" ADD "ST-App-Key" "{value.json.app_key}" ADD "Content-Type" "application/json" WITH BODY "{value.json.body}" ADD EVENT ListEquipmentTypes WITH SOURCE_TOPIC "cmms/servicetitan/equipment-type/list" WITH DESTINATION_TOPIC "cmms/servicetitan/equipment-type/list/response" WITH METHOD "GET" WITH ENDPOINT "/equipmentsystems/v2/tenant/{value.json.tenant}/equipment-types?{value.json.query}" WITH HEADERS ADD "Accept" "application/json" ADD "User-Agent" "Coreflux-Broker" ADD "Authorization" "{value.json.access_token}" ADD "ST-App-Key" "{value.json.app_key}" ADD EVENT ListEquipmentSystems WITH SOURCE_TOPIC "cmms/servicetitan/equipment-system/list" WITH DESTINATION_TOPIC "cmms/servicetitan/equipment-system/list/response" WITH METHOD "GET" WITH ENDPOINT "/equipmentsystems/v2/tenant/{value.json.tenant}/equipment-systems?{value.json.query}" WITH HEADERS ADD "Accept" "application/json" ADD "User-Agent" "Coreflux-Broker" ADD "Authorization" "{value.json.access_token}" ADD "ST-App-Key" "{value.json.app_key}" ADD EVENT ListLocations WITH SOURCE_TOPIC "cmms/servicetitan/location/list" WITH DESTINATION_TOPIC "cmms/servicetitan/location/list/response" WITH METHOD "GET" WITH ENDPOINT "/crm/v2/tenant/{value.json.tenant}/locations?{value.json.query}" WITH HEADERS ADD "Accept" "application/json" ADD "User-Agent" "Coreflux-Broker" ADD "Authorization" "{value.json.access_token}" ADD "ST-App-Key" "{value.json.app_key}" ADD EVENT GetLocation WITH SOURCE_TOPIC "cmms/servicetitan/location/get" WITH DESTINATION_TOPIC "cmms/servicetitan/location/get/response" WITH METHOD "GET" WITH ENDPOINT "/crm/v2/tenant/{value.json.tenant}/locations/{value.json.id}" WITH HEADERS ADD "Accept" "application/json" ADD "User-Agent" "Coreflux-Broker" ADD "Authorization" "{value.json.access_token}" ADD "ST-App-Key" "{value.json.app_key}" ADD EVENT CreateLocationNote WITH SOURCE_TOPIC "cmms/servicetitan/location-note/create" WITH DESTINATION_TOPIC "cmms/servicetitan/location-note/create/response" WITH METHOD "POST" WITH ENDPOINT "/crm/v2/tenant/{value.json.tenant}/locations/{value.json.id}/notes" WITH HEADERS ADD "Accept" "application/json" ADD "User-Agent" "Coreflux-Broker" ADD "Authorization" "{value.json.access_token}" ADD "ST-App-Key" "{value.json.app_key}" ADD "Content-Type" "application/json" WITH BODY "{value.json.body}" ``` Deploy it with `-addConnector` (do not include `ADD METADATA`). ## Usage example Call `ListJobs` by publishing a JSON payload to its request topic, and read the reply on the response topic: ```bash theme={"theme":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} mosquitto_sub -t 'cmms/servicetitan/job/list/response' & mosquitto_pub -t 'cmms/servicetitan/job/list' -m '{"access_token": "", "app_key": "", "query": "", "tenant": ""}' ``` ## Related * [REST API Connectors](/latest/lot-language/routes/rest-api-routes) * [REST API Apps catalog](/latest/lot-language/routes/rest-api-apps) # ServiceMax (PTC) Source: https://docs.coreflux.org/latest/lot-language/routes/rest-api-apps/servicemax LoT connector for ServiceMax on Salesforce: create and close work orders from alarms, update installed products with telemetry and publish Platform Events Technical Preview ## Overview [ServiceMax](https://www.servicemax.com/) (part of PTC) is field service management for equipment makers and operators. **ServiceMax Core** is a managed package (`SVMXC__` objects) and **Asset 360** (`SVMXA360__`) builds on Salesforce Field Service; both run inside your Salesforce org. This connector lets the broker open a ServiceMax work order when a machine raises an alarm, put run hours and other telemetry on the installed product, and show each technician's open work orders, so service teams see what the machine sees. The template has two connectors: * **ServiceMaxApi** (`ServicemaxRestApi.lot`), a client connector on your org's My Domain with 26 events: 10 writes, 14 reads and 2 token events. It has no `DELETE` events. Events starting with `A360` need Asset 360; the others need ServiceMax Core. * **ServiceMaxNotificationReceiver** (`cmms_servicemax_webhook.lot`), an optional server connector on port `9011` that receives JSON callouts from a Flow or Apex in your org and publishes them on MQTT. > [!NOTE] > > ServiceMax publishes no OpenAPI or Postman file of its own: its objects are ordinary Salesforce sObjects, read and written with the Salesforce REST API. The events were picked by hand from the PTC data dictionaries ([Work Order](https://support.ptc.com/help/servicemaxcore/en/articles/core/work-order1.html), [Installed Product](https://support.ptc.com/help/servicemaxcore/en/articles/core/installed-product-2.html), [Technician/Equipment](https://support.ptc.com/help/servicemaxcore/en/articles/core/technician-equipment.html)) and the [Salesforce REST API Developer Guide](https://developer.salesforce.com/docs/atlas.en-us.api_rest.meta/api_rest/). ## Events | Event | Topic | Request | Kind | What it does | | - | - | - | - | - | | `GetToken` | `cmms/servicemax/token/create` | `POST /services/oauth2/token` | Auth | Get an access token from your My Domain (form body: client\_credentials, or password / JWT bearer / refresh\_token grants). Reply access\_token + instance\_url; send access\_token on every other event | | `RevokeToken` | `cmms/servicemax/token/revoke` | `POST /services/oauth2/revoke` | Auth | Revoke an access or refresh token (form body token=...). Not a data delete | | `ListApiVersions` | `cmms/servicemax/api-version/list` | `GET /services/data/` | Read | API versions the org serves (no auth needed). The connector pins v65.0 | | `GetLimits` | `cmms/servicemax/limit/get` | `GET /services/data/v65.0/limits` | Read | Org limits (DailyApiRequests Max / Remaining and more). Check before polling | | `Query` | `cmms/servicemax/soql/query` | `GET /services/data/v65.0/query?q={q}` | Read | Any SOQL query, URL-encoded (spaces as +). Up to 2000 rows per page; nextRecordsUrl when done is false | | `QueryMore` | `cmms/servicemax/soql/next` | `GET /services/data/v65.0/query/{id}` | Read | Next page of a query (the part of nextRecordsUrl after /query/) | | `DescribeWorkOrder` | `cmms/servicemax/work-order-describe/get` | `GET /services/data/v65.0/sobjects/SVMXC__Service_Order__c/describe` | Read | Field list and the org's picklist values for SVMXC\_\_Order\_Status\_\_c, SVMXC\_\_Priority\_\_c and SVMXC\_\_Order\_Type\_\_c (they are configurable) | | `GetWorkOrder` | `cmms/servicemax/work-order/get` | `GET /services/data/v65.0/sobjects/SVMXC__Service_Order__c/{id}?{query}` | Read | One ServiceMax work order by record Id (query fields=... or "" for all) | | `ListOpenWorkOrdersByTechnician` | `cmms/servicemax/work-order/list-open-by-technician` | `GET /services/data/v65.0/query?q=` | Read | Open work orders assigned to one technician (SVMXC\_\_Group\_Member\_\_c, not closed), next scheduled first, 200 max | | `ListOpenWorkOrdersByInstalledProduct` | `cmms/servicemax/work-order/list-open-by-installed-product` | `GET /services/data/v65.0/query?q=` | Read | Open work orders for one installed product (check before opening a duplicate from a repeating alarm) | | `CreateWorkOrder` | `cmms/servicemax/work-order/create` | `POST /services/data/v65.0/sobjects/SVMXC__Service_Order__c` | Write | Create a ServiceMax work order from an alarm; reply 201 `{id, success, errors}` | | `UpdateWorkOrder` | `cmms/servicemax/work-order/update` | `PATCH /services/data/v65.0/sobjects/SVMXC__Service_Order__c/{id}` | Write | Change work order fields (status, priority, technician SVMXC\_\_Group\_Member\_\_c, ...); reply 204 empty | | `UpsertWorkOrderByExternalId` | `cmms/servicemax/work-order/upsert` | `PATCH /services/data/v65.0/sobjects/SVMXC__Service_Order__c/{external_id_field}/{external_id}` | Write | Create or update a work order keyed by your own External ID field (one work order per alarm id). 201 created / 200 updated | | `DescribeInstalledProduct` | `cmms/servicemax/installed-product-describe/get` | `GET /services/data/v65.0/sobjects/SVMXC__Installed_Product__c/describe` | Read | Installed product fields, including your custom telemetry fields and SVMXC\_\_Status\_\_c values | | `GetInstalledProduct` | `cmms/servicemax/installed-product/get` | `GET /services/data/v65.0/sobjects/SVMXC__Installed_Product__c/{id}?{query}` | Read | One installed product by record Id | | `FindInstalledProductBySerial` | `cmms/servicemax/installed-product/find-by-serial` | `GET /services/data/v65.0/query?q=` | Read | Map a machine serial number from MQTT to its installed product Id | | `UpdateInstalledProduct` | `cmms/servicemax/installed-product/update` | `PATCH /services/data/v65.0/sobjects/SVMXC__Installed_Product__c/{id}` | Write | Telemetry on the installed product: your custom fields (e.g. Run\_Hours\_\_c) plus SVMXC\_\_Status\_\_c / SVMXC\_\_Latitude\_\_c / SVMXC\_\_Longitude\_\_c; reply 204 | | `UpdateRecordsBatch` | `cmms/servicemax/record-batch/update` | `PATCH /services/data/v65.0/composite/sobjects` | Write | Update up to 200 records in one call (sObject Collections), e.g. run hours for a whole line; reply per-record success / errors | | `ListTechnicians` | `cmms/servicemax/technician/list` | `GET /services/data/v65.0/query?q=` | Read | Active technicians / equipment (SVMXC\_\_Service\_Group\_Members\_\_c) with service team and Salesforce user | | `GetTechnician` | `cmms/servicemax/technician/get` | `GET /services/data/v65.0/sobjects/SVMXC__Service_Group_Members__c/{id}?{query}` | Read | One technician / equipment record by Id | | `CreateCase` | `cmms/servicemax/case/create` | `POST /services/data/v65.0/sobjects/Case` | Write | Open a Salesforce Case (the usual ServiceMax entry point before a work order is dispatched) | | `PublishPlatformEvent` | `cmms/servicemax/platform-event/publish` | `POST /services/data/v65.0/sobjects/{event_name}` | Write | Publish a platform event you defined (e.g. Coreflux\_Telemetry\_\_e) for Flows / Apex triggers to act on | | `A360SaveTechnicalAttributes` | `cmms/servicemax/a360-technical-attribute/save` | `POST /services/apexrest/SVMXA360/technicalAttribute/v1/saveTechnicalAttributes/` | Write | Asset 360 only: write telemetry as technical attributes (up to 200 entries, with time of occurrence) on Assets | | `A360CreateWorkOrder` | `cmms/servicemax/a360-work-order/create` | `POST /services/data/v65.0/sobjects/WorkOrder` | Write | Asset 360 only: create a standard WorkOrder on an Asset | | `A360ListOpenWorkOrdersByAsset` | `cmms/servicemax/a360-work-order/list-open-by-asset` | `GET /services/data/v65.0/query?q=` | Read | Asset 360 only: open standard WorkOrders for one Asset | | `A360UpdateAsset` | `cmms/servicemax/a360-asset/update` | `PATCH /services/data/v65.0/sobjects/Asset/{id}` | Write | Asset 360 only: update a standard Asset (status or your custom telemetry fields) | Replies arrive on the same topic with `/response` appended. The SOQL behind the fixed query events: * `ListOpenWorkOrdersByTechnician`: ```sql theme={"theme":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} SELECT Id,Name,SVMXC__Order_Status__c,SVMXC__Priority__c,SVMXC__Order_Type__c,SVMXC__Component__c,SVMXC__Company__c,SVMXC__Scheduled_Date_Time__c,SVMXC__Problem_Description__c FROM SVMXC__Service_Order__c WHERE SVMXC__Group_Member__c='{value.json.technician_id}' AND SVMXC__Closed_On__c=null ORDER BY SVMXC__Scheduled_Date_Time__c LIMIT 200 ``` * `ListOpenWorkOrdersByInstalledProduct`: ```sql theme={"theme":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} SELECT Id,Name,SVMXC__Order_Status__c,SVMXC__Priority__c,SVMXC__Order_Type__c,SVMXC__Component__c,SVMXC__Company__c,SVMXC__Scheduled_Date_Time__c,SVMXC__Problem_Description__c FROM SVMXC__Service_Order__c WHERE SVMXC__Component__c='{value.json.installed_product_id}' AND SVMXC__Closed_On__c=null LIMIT 200 ``` * `FindInstalledProductBySerial`: ```sql theme={"theme":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} SELECT Id,Name,SVMXC__Serial_Lot_Number__c,SVMXC__Status__c,SVMXC__Product__c,SVMXC__Company__c,SVMXC__Site__c,SVMXC__Parent__c FROM SVMXC__Installed_Product__c WHERE SVMXC__Serial_Lot_Number__c='{value.json.serial}' LIMIT 5 ``` * `ListTechnicians`: ```sql theme={"theme":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} SELECT Id,Name,SVMXC__Service_Group__c,SVMXC__Salesforce_User__c,SVMXC__Email__c,SVMXC__Active__c FROM SVMXC__Service_Group_Members__c WHERE SVMXC__Active__c=true LIMIT 200 ``` * `A360ListOpenWorkOrdersByAsset`: ```sql theme={"theme":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} SELECT Id,WorkOrderNumber,Status,Priority,Subject,AssetId,OwnerId FROM WorkOrder WHERE AssetId='{value.json.asset_id}' AND IsClosed=false LIMIT 200 ``` ## Prerequisites * A Salesforce org with ServiceMax Core installed (and Asset 360 for the `A360` events). * An **External Client App** (or Connected App) with OAuth enabled. For server-to-server use enable the **client credentials flow** and set a **Run As** integration user that has the ServiceMax permission sets and access to the objects and fields you write. * Your org's **My Domain** URL, for example `https://acme.my.salesforce.com` (sandbox `https://acme--uat.sandbox.my.salesforce.com`). Change `BASE_ADDRESS` to it. `login.salesforce.com` and `test.salesforce.com` only issue tokens; REST calls on them fail with `URL_NOT_RESET`. * Outbound HTTPS from the broker to that host. * For notifications only: a public HTTPS URL that forwards to the broker's port `9011`. ## Authentication > [!WARNING] > > The consumer secret and every access token act as the Run As user. Store the consumer key and secret in the Hub secrets `SALESFORCE_CLIENT_ID` and `SALESFORCE_CLIENT_SECRET`, read them with `GET SECRET`, and never put them or a token in a connector file or a retained topic. `GetToken` posts a form body to `/services/oauth2/token` on your My Domain. With the client credentials flow: ```json theme={"theme":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} { "form": "grant_type=client_credentials&client_id=&client_secret=" } ``` The reply carries `access_token` and `instance_url`. Every other event sends `Authorization: Bearer {value.json.access_token}`, so each payload carries the token: ```json theme={"theme":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} { "access_token": "" } ``` The same event works for the JWT bearer flow (`grant_type=urn:ietf:params:oauth:grant-type:jwt-bearer&assertion=`) and refresh tokens. Tokens expire with the org's session timeout; on `401 INVALID_SESSION_ID` call `GetToken` again. The example actions refresh it every hour. Answers seen from the real Salesforce hosts without valid credentials: a fake client returns `400 {"error":"invalid_client_id"}`, a fake JWT `400 invalid_grant`, and any REST call with a fake token `401 [{"errorCode":"INVALID_SESSION_ID"}]`. ## Payload conventions * `access_token`: the token from `GetToken`. * `id`: an 18-character Salesforce record Id (work order `a2x...`, installed product, technician, Asset), or the query locator for `QueryMore`. * `query`: `fields=Id,Name,...` for the get events, or `""` for all fields. * `q`: a whole SOQL statement, URL-encoded (spaces as `+`). * `technician_id`, `installed_product_id`, `serial`, `asset_id`: values the fixed queries put inside SOQL quotes. * `body`: the JSON record, sent unchanged on `POST` and `PATCH` events. Creates answer `201` with `{"id": "...", "success": true, "errors": []}`; updates answer an empty `204`. Errors are JSON arrays such as `[{"errorCode": "REQUIRED_FIELD_MISSING", "message": "...", "fields": [...]}]`. > [!WARNING] > > `technician_id`, `installed_product_id`, `serial` and `asset_id` are inserted into SOQL as they are. Pass Salesforce Ids or serial numbers that come from your own system, never raw user text with quotes in it. Create a work order from an alarm. `SVMXC__Component__c` is the installed product, `SVMXC__Company__c` the account: ```json theme={"theme":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} { "access_token": "", "body": { "SVMXC__Company__c": "", "SVMXC__Component__c": "", "SVMXC__Order_Type__c": "Field Service", "SVMXC__Priority__c": "High", "SVMXC__Problem_Description__c": "Alarm TT-101 = 92.4 C at 2026-10-06T06:15:00Z (Coreflux)" } } ``` Close it. `SVMXC__Order_Status__c`, `SVMXC__Priority__c` and `SVMXC__Order_Type__c` are picklists that each org configures, so read the allowed values with `DescribeWorkOrder` first: ```json theme={"theme":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} { "access_token": "", "id": "", "body": { "SVMXC__Order_Status__c": "Closed" } } ``` To avoid one work order per repeated alarm, add a custom External ID field (for example `Coreflux_Alarm_Id__c`) to the work order and use `UpsertWorkOrderByExternalId`, or check `ListOpenWorkOrdersByInstalledProduct` first. ## Telemetry ServiceMax Core has no standard telemetry field on `SVMXC__Installed_Product__c`. Add the custom fields you need (for example `Run_Hours__c` and `Last_Telemetry_At__c`) and write them with `UpdateInstalledProduct`, or for many machines at once with `UpdateRecordsBatch` (up to 200 records per call). Map a machine's serial number to its record with `FindInstalledProductBySerial`. ```json theme={"theme":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} { "access_token": "", "id": "", "body": { "Run_Hours__c": 1287.5, "Last_Telemetry_At__c": "2026-10-06T06:30:00Z", "SVMXC__Latitude__c": 41.1579, "SVMXC__Longitude__c": -8.6291 } } ``` On **Asset 360**, send readings as technical attributes instead. `A360SaveTechnicalAttributes` creates missing attributes, records the time of each reading and finds the Asset by Id, name or an identifier field (200 entries per call): ```json theme={"theme":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} { "access_token": "", "body": { "source": "External System", "objectName": "Asset", "assetIdentifierFieldAPIName": "SerialNumber", "attributeList": [ { "value": "10800", "unit": "Watt", "attributeKey": "EnergyConsumption", "dataType": "Number", "timeOfOccurence": "2026-10-06 06:30:00", "assetIdentifiers": [ { "identifierName": "Serial Number", "value": "SN-PRESS-0042" } ] } ] } } ``` ## Notification receiver Deploy `cmms_servicemax_webhook.lot` only if you want the org to push changes to the broker. It opens HTTP port `9011`. | Event | Request | Published on | What it receives | | - | - | - | - | | `NotifyWorkOrders` | `POST /servicemax/notify/work-orders` | `cmms/servicemax/work-order-notification/received` | Flow HTTP Callout / Apex callout with JSON when a work order is created or changes (POST https\://``/servicemax/notify/work-orders) | | `NotifyInstalledProducts` | `POST /servicemax/notify/installed-products` | `cmms/servicemax/installed-product-notification/received` | Same for installed product / Asset changes | | `NotifyEvents` | `POST /servicemax/notify/events` | `cmms/servicemax/notification/received` | Any other JSON callout (cases, Asset 360 objects). Filter on the body in a LoT action | Salesforce has no built-in JSON webhook. Build a record-triggered Flow with an **HTTP Callout** action (or an Apex callout) on `SVMXC__Service_Order__c` or `SVMXC__Installed_Product__c`, point a Named Credential at the public URL of the matching path, and send the fields you need as JSON. > [!WARNING] > > The receiver connector does not authenticate callers. Put a reverse proxy with a public TLS certificate in front of port `9011` and check a shared secret header that the Named Credential sends. Outbound Messages are SOAP and expect an `true` reply, which this connector does not send, so they are not supported. ## Connector ```lot theme={"theme":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} # Coreflux REST_API template — ServiceMax (PTC) on the Salesforce REST API v65.0 (beta): get an OAuth token, create # ServiceMax Core work orders (SVMXC__Service_Order__c) from alarms, upsert them by your own alarm id, list open work # orders by technician (SVMXC__Group_Member__c) or installed product, close them, find installed products # (SVMXC__Installed_Product__c) by serial number and PATCH them with telemetry (single or 200-record batch), read # technicians (SVMXC__Service_Group_Members__c), open Cases, publish Platform Events, and for Asset 360 orgs write # technical attributes, create standard WorkOrders and update Assets. # Postman file). Credentials are placeholders only. # Docs: https://docs.coreflux.org/latest/lot-language/routes/rest-api-apps/servicemax # # Host: your Salesforce org's My Domain, https://YOUR_DOMAIN.my.salesforce.com (sandbox: # https://YOUR_DOMAIN--SANDBOX.sandbox.my.salesforce.com). Use the instance_url from the token reply. REST calls on # login.salesforce.com / test.salesforce.com answer 400 URL_NOT_RESET; those hosts only issue tokens. # Auth: Salesforce OAuth 2.0 through an External Client App / Connected App. GetToken posts a form body # ({value.json.form}) to /services/oauth2/token on the My Domain: client credentials # (grant_type=client_credentials&client_id=...&client_secret=..., needs a Run As user), or the JWT bearer / password # / refresh_token grants. Every other event sends Authorization: Bearer {value.json.access_token}. Keep the # consumer key / secret in Hub secrets (SALESFORCE_CLIENT_ID, SALESFORCE_CLIENT_SECRET) and the token on an # internal topic. Tokens expire per the org session timeout (401 INVALID_SESSION_ID): call GetToken again. # Live (2026-10-06): no or fake token -> 401 [{"errorCode":"INVALID_SESSION_ID"}]; fake client -> 400 invalid_client_id. # Objects: ServiceMax Core (managed package SVMXC__) work orders, installed products and technicians. Asset 360 # (SVMXA360__) orgs use the standard WorkOrder and Asset objects plus the technical attributes Apex REST service; # the A360* events need Asset 360, the others need ServiceMax Core. Picklist values (SVMXC__Order_Status__c, # SVMXC__Priority__c, SVMXC__Order_Type__c) are org-configurable: check them with DescribeWorkOrder. # Payload fields (control fields stay out of bodies; request bodies come from {value.json.body}): # access_token, id, query, q, technician_id, installed_product_id, serial, asset_id, external_id_field, # external_id, event_name, form, body # q / query are URL-encoded (spaces as +). technician_id, installed_product_id, serial and asset_id are inserted # inside SOQL quotes: pass only Salesforce Ids or serials you trust (no quotes), never raw user input. # CreateWorkOrder body = {"SVMXC__Component__c": "", "SVMXC__Problem_Description__c": "..."} # UpdateWorkOrder id = , body = {"SVMXC__Order_Status__c": "Closed"} # UpdateInstalledProduct id = , body = {"Run_Hours__c": 1287.5} (custom telemetry fields) # A360SaveTechnicalAttributes body = {"source": "External System", "objectName": "Asset", "attributeList": [...]} # Errors: JSON arrays [{"message","errorCode","fields"}]: 400 MALFORMED_QUERY / INVALID_FIELD / # REQUIRED_FIELD_MISSING / FIELD_CUSTOM_VALIDATION_EXCEPTION, 401 INVALID_SESSION_ID, 403 REQUEST_LIMIT_EXCEEDED or # no object access, 404 NOT_FOUND. Writes fire the org's ServiceMax triggers, flows, SLAs and dispatch rules. # DEFINE CONNECTOR ServiceMaxApi WITH TYPE REST_API ADD REST_API_CONFIG WITH BASE_ADDRESS "https://YOUR_DOMAIN.my.salesforce.com" WITH ENABLE_CLIENT "true" WITH USE_SSL "true" WITH REQUEST_TIMEOUT '30' WITH DEFAULT_CONTENT_TYPE "application/json" # Get an access token from your My Domain (form body: client_credentials, or password / JWT bearer / refresh_token grants). Reply access_token + instance_url; send access_token on every other event ADD EVENT GetToken WITH SOURCE_TOPIC "cmms/servicemax/token/create" WITH DESTINATION_TOPIC "cmms/servicemax/token/create/response" WITH METHOD "POST" WITH ENDPOINT "/services/oauth2/token" WITH HEADERS ADD "Accept" "application/json" ADD "Content-Type" "application/x-www-form-urlencoded" WITH BODY "{value.json.form}" # Revoke an access or refresh token (form body token=...). Not a data delete ADD EVENT RevokeToken WITH SOURCE_TOPIC "cmms/servicemax/token/revoke" WITH DESTINATION_TOPIC "cmms/servicemax/token/revoke/response" WITH METHOD "POST" WITH ENDPOINT "/services/oauth2/revoke" WITH HEADERS ADD "Accept" "application/json" ADD "Content-Type" "application/x-www-form-urlencoded" WITH BODY "{value.json.form}" # API versions the org serves (no auth needed). The route pins v65.0 ADD EVENT ListApiVersions WITH SOURCE_TOPIC "cmms/servicemax/api-version/list" WITH DESTINATION_TOPIC "cmms/servicemax/api-version/list/response" WITH METHOD "GET" WITH ENDPOINT "/services/data/" WITH HEADERS ADD "Accept" "application/json" # Org limits (DailyApiRequests Max / Remaining and more). Check before polling ADD EVENT GetLimits WITH SOURCE_TOPIC "cmms/servicemax/limit/get" WITH DESTINATION_TOPIC "cmms/servicemax/limit/get/response" WITH METHOD "GET" WITH ENDPOINT "/services/data/v65.0/limits" WITH HEADERS ADD "Accept" "application/json" ADD "Authorization" "Bearer {value.json.access_token}" # Any SOQL query, URL-encoded (spaces as +). Up to 2000 rows per page; nextRecordsUrl when done is false ADD EVENT Query WITH SOURCE_TOPIC "cmms/servicemax/soql/query" WITH DESTINATION_TOPIC "cmms/servicemax/soql/query/response" WITH METHOD "GET" WITH ENDPOINT "/services/data/v65.0/query?q={value.json.q}" WITH HEADERS ADD "Accept" "application/json" ADD "Authorization" "Bearer {value.json.access_token}" # Next page of a query (the part of nextRecordsUrl after /query/) ADD EVENT QueryMore WITH SOURCE_TOPIC "cmms/servicemax/soql/next" WITH DESTINATION_TOPIC "cmms/servicemax/soql/next/response" WITH METHOD "GET" WITH ENDPOINT "/services/data/v65.0/query/{value.json.id}" WITH HEADERS ADD "Accept" "application/json" ADD "Authorization" "Bearer {value.json.access_token}" # Field list and the org's picklist values for SVMXC__Order_Status__c, SVMXC__Priority__c and SVMXC__Order_Type__c (they are configurable) ADD EVENT DescribeWorkOrder WITH SOURCE_TOPIC "cmms/servicemax/work-order-describe/get" WITH DESTINATION_TOPIC "cmms/servicemax/work-order-describe/get/response" WITH METHOD "GET" WITH ENDPOINT "/services/data/v65.0/sobjects/SVMXC__Service_Order__c/describe" WITH HEADERS ADD "Accept" "application/json" ADD "Authorization" "Bearer {value.json.access_token}" # One ServiceMax work order by record Id (query fields=... or "" for all) ADD EVENT GetWorkOrder WITH SOURCE_TOPIC "cmms/servicemax/work-order/get" WITH DESTINATION_TOPIC "cmms/servicemax/work-order/get/response" WITH METHOD "GET" WITH ENDPOINT "/services/data/v65.0/sobjects/SVMXC__Service_Order__c/{value.json.id}?{value.json.query}" WITH HEADERS ADD "Accept" "application/json" ADD "Authorization" "Bearer {value.json.access_token}" # Open work orders assigned to one technician (SVMXC__Group_Member__c, not closed), next scheduled first, 200 max ADD EVENT ListOpenWorkOrdersByTechnician WITH SOURCE_TOPIC "cmms/servicemax/work-order/list-open-by-technician" WITH DESTINATION_TOPIC "cmms/servicemax/work-order/list-open-by-technician/response" WITH METHOD "GET" WITH ENDPOINT "/services/data/v65.0/query?q=SELECT+Id,Name,SVMXC__Order_Status__c,SVMXC__Priority__c,SVMXC__Order_Type__c,SVMXC__Component__c,SVMXC__Company__c,SVMXC__Scheduled_Date_Time__c,SVMXC__Problem_Description__c+FROM+SVMXC__Service_Order__c+WHERE+SVMXC__Group_Member__c='{value.json.technician_id}'+AND+SVMXC__Closed_On__c=null+ORDER+BY+SVMXC__Scheduled_Date_Time__c+LIMIT+200" WITH HEADERS ADD "Accept" "application/json" ADD "Authorization" "Bearer {value.json.access_token}" # Open work orders for one installed product (check before opening a duplicate from a repeating alarm) ADD EVENT ListOpenWorkOrdersByInstalledProduct WITH SOURCE_TOPIC "cmms/servicemax/work-order/list-open-by-installed-product" WITH DESTINATION_TOPIC "cmms/servicemax/work-order/list-open-by-installed-product/response" WITH METHOD "GET" WITH ENDPOINT "/services/data/v65.0/query?q=SELECT+Id,Name,SVMXC__Order_Status__c,SVMXC__Priority__c,SVMXC__Order_Type__c,SVMXC__Component__c,SVMXC__Company__c,SVMXC__Scheduled_Date_Time__c,SVMXC__Problem_Description__c+FROM+SVMXC__Service_Order__c+WHERE+SVMXC__Component__c='{value.json.installed_product_id}'+AND+SVMXC__Closed_On__c=null+LIMIT+200" WITH HEADERS ADD "Accept" "application/json" ADD "Authorization" "Bearer {value.json.access_token}" # WRITE: Create a ServiceMax work order from an alarm; reply 201 {id, success, errors} ADD EVENT CreateWorkOrder WITH SOURCE_TOPIC "cmms/servicemax/work-order/create" WITH DESTINATION_TOPIC "cmms/servicemax/work-order/create/response" WITH METHOD "POST" WITH ENDPOINT "/services/data/v65.0/sobjects/SVMXC__Service_Order__c" WITH HEADERS ADD "Accept" "application/json" ADD "Authorization" "Bearer {value.json.access_token}" ADD "Content-Type" "application/json" WITH BODY "{value.json.body}" # WRITE: Change work order fields (status, priority, technician SVMXC__Group_Member__c, ...); reply 204 empty ADD EVENT UpdateWorkOrder WITH SOURCE_TOPIC "cmms/servicemax/work-order/update" WITH DESTINATION_TOPIC "cmms/servicemax/work-order/update/response" WITH METHOD "PATCH" WITH ENDPOINT "/services/data/v65.0/sobjects/SVMXC__Service_Order__c/{value.json.id}" WITH HEADERS ADD "Accept" "application/json" ADD "Authorization" "Bearer {value.json.access_token}" ADD "Content-Type" "application/json" WITH BODY "{value.json.body}" # WRITE: Create or update a work order keyed by your own External ID field (one work order per alarm id). 201 created / 200 updated ADD EVENT UpsertWorkOrderByExternalId WITH SOURCE_TOPIC "cmms/servicemax/work-order/upsert" WITH DESTINATION_TOPIC "cmms/servicemax/work-order/upsert/response" WITH METHOD "PATCH" WITH ENDPOINT "/services/data/v65.0/sobjects/SVMXC__Service_Order__c/{value.json.external_id_field}/{value.json.external_id}" WITH HEADERS ADD "Accept" "application/json" ADD "Authorization" "Bearer {value.json.access_token}" ADD "Content-Type" "application/json" WITH BODY "{value.json.body}" # Installed product fields, including your custom telemetry fields and SVMXC__Status__c values ADD EVENT DescribeInstalledProduct WITH SOURCE_TOPIC "cmms/servicemax/installed-product-describe/get" WITH DESTINATION_TOPIC "cmms/servicemax/installed-product-describe/get/response" WITH METHOD "GET" WITH ENDPOINT "/services/data/v65.0/sobjects/SVMXC__Installed_Product__c/describe" WITH HEADERS ADD "Accept" "application/json" ADD "Authorization" "Bearer {value.json.access_token}" # One installed product by record Id ADD EVENT GetInstalledProduct WITH SOURCE_TOPIC "cmms/servicemax/installed-product/get" WITH DESTINATION_TOPIC "cmms/servicemax/installed-product/get/response" WITH METHOD "GET" WITH ENDPOINT "/services/data/v65.0/sobjects/SVMXC__Installed_Product__c/{value.json.id}?{value.json.query}" WITH HEADERS ADD "Accept" "application/json" ADD "Authorization" "Bearer {value.json.access_token}" # Map a machine serial number from MQTT to its installed product Id ADD EVENT FindInstalledProductBySerial WITH SOURCE_TOPIC "cmms/servicemax/installed-product/find-by-serial" WITH DESTINATION_TOPIC "cmms/servicemax/installed-product/find-by-serial/response" WITH METHOD "GET" WITH ENDPOINT "/services/data/v65.0/query?q=SELECT+Id,Name,SVMXC__Serial_Lot_Number__c,SVMXC__Status__c,SVMXC__Product__c,SVMXC__Company__c,SVMXC__Site__c,SVMXC__Parent__c+FROM+SVMXC__Installed_Product__c+WHERE+SVMXC__Serial_Lot_Number__c='{value.json.serial}'+LIMIT+5" WITH HEADERS ADD "Accept" "application/json" ADD "Authorization" "Bearer {value.json.access_token}" # WRITE: Telemetry on the installed product: your custom fields (e.g. Run_Hours__c) plus SVMXC__Status__c / SVMXC__Latitude__c / SVMXC__Longitude__c; reply 204 ADD EVENT UpdateInstalledProduct WITH SOURCE_TOPIC "cmms/servicemax/installed-product/update" WITH DESTINATION_TOPIC "cmms/servicemax/installed-product/update/response" WITH METHOD "PATCH" WITH ENDPOINT "/services/data/v65.0/sobjects/SVMXC__Installed_Product__c/{value.json.id}" WITH HEADERS ADD "Accept" "application/json" ADD "Authorization" "Bearer {value.json.access_token}" ADD "Content-Type" "application/json" WITH BODY "{value.json.body}" # WRITE: Update up to 200 records in one call (sObject Collections), e.g. run hours for a whole line; reply per-record success / errors ADD EVENT UpdateRecordsBatch WITH SOURCE_TOPIC "cmms/servicemax/record-batch/update" WITH DESTINATION_TOPIC "cmms/servicemax/record-batch/update/response" WITH METHOD "PATCH" WITH ENDPOINT "/services/data/v65.0/composite/sobjects" WITH HEADERS ADD "Accept" "application/json" ADD "Authorization" "Bearer {value.json.access_token}" ADD "Content-Type" "application/json" WITH BODY "{value.json.body}" # Active technicians / equipment (SVMXC__Service_Group_Members__c) with service team and Salesforce user ADD EVENT ListTechnicians WITH SOURCE_TOPIC "cmms/servicemax/technician/list" WITH DESTINATION_TOPIC "cmms/servicemax/technician/list/response" WITH METHOD "GET" WITH ENDPOINT "/services/data/v65.0/query?q=SELECT+Id,Name,SVMXC__Service_Group__c,SVMXC__Salesforce_User__c,SVMXC__Email__c,SVMXC__Active__c+FROM+SVMXC__Service_Group_Members__c+WHERE+SVMXC__Active__c=true+LIMIT+200" WITH HEADERS ADD "Accept" "application/json" ADD "Authorization" "Bearer {value.json.access_token}" # One technician / equipment record by Id ADD EVENT GetTechnician WITH SOURCE_TOPIC "cmms/servicemax/technician/get" WITH DESTINATION_TOPIC "cmms/servicemax/technician/get/response" WITH METHOD "GET" WITH ENDPOINT "/services/data/v65.0/sobjects/SVMXC__Service_Group_Members__c/{value.json.id}?{value.json.query}" WITH HEADERS ADD "Accept" "application/json" ADD "Authorization" "Bearer {value.json.access_token}" # WRITE: Open a Salesforce Case (the usual ServiceMax entry point before a work order is dispatched) ADD EVENT CreateCase WITH SOURCE_TOPIC "cmms/servicemax/case/create" WITH DESTINATION_TOPIC "cmms/servicemax/case/create/response" WITH METHOD "POST" WITH ENDPOINT "/services/data/v65.0/sobjects/Case" WITH HEADERS ADD "Accept" "application/json" ADD "Authorization" "Bearer {value.json.access_token}" ADD "Content-Type" "application/json" WITH BODY "{value.json.body}" # WRITE: Publish a platform event you defined (e.g. Coreflux_Telemetry__e) for Flows / Apex triggers to act on ADD EVENT PublishPlatformEvent WITH SOURCE_TOPIC "cmms/servicemax/platform-event/publish" WITH DESTINATION_TOPIC "cmms/servicemax/platform-event/publish/response" WITH METHOD "POST" WITH ENDPOINT "/services/data/v65.0/sobjects/{value.json.event_name}" WITH HEADERS ADD "Accept" "application/json" ADD "Authorization" "Bearer {value.json.access_token}" ADD "Content-Type" "application/json" WITH BODY "{value.json.body}" # WRITE: Asset 360 only: write telemetry as technical attributes (up to 200 entries, with time of occurrence) on Assets ADD EVENT A360SaveTechnicalAttributes WITH SOURCE_TOPIC "cmms/servicemax/a360-technical-attribute/save" WITH DESTINATION_TOPIC "cmms/servicemax/a360-technical-attribute/save/response" WITH METHOD "POST" WITH ENDPOINT "/services/apexrest/SVMXA360/technicalAttribute/v1/saveTechnicalAttributes/" WITH HEADERS ADD "Accept" "application/json" ADD "Authorization" "Bearer {value.json.access_token}" ADD "Content-Type" "application/json" WITH BODY "{value.json.body}" # WRITE: Asset 360 only: create a standard WorkOrder on an Asset ADD EVENT A360CreateWorkOrder WITH SOURCE_TOPIC "cmms/servicemax/a360-work-order/create" WITH DESTINATION_TOPIC "cmms/servicemax/a360-work-order/create/response" WITH METHOD "POST" WITH ENDPOINT "/services/data/v65.0/sobjects/WorkOrder" WITH HEADERS ADD "Accept" "application/json" ADD "Authorization" "Bearer {value.json.access_token}" ADD "Content-Type" "application/json" WITH BODY "{value.json.body}" # Asset 360 only: open standard WorkOrders for one Asset ADD EVENT A360ListOpenWorkOrdersByAsset WITH SOURCE_TOPIC "cmms/servicemax/a360-work-order/list-open-by-asset" WITH DESTINATION_TOPIC "cmms/servicemax/a360-work-order/list-open-by-asset/response" WITH METHOD "GET" WITH ENDPOINT "/services/data/v65.0/query?q=SELECT+Id,WorkOrderNumber,Status,Priority,Subject,AssetId,OwnerId+FROM+WorkOrder+WHERE+AssetId='{value.json.asset_id}'+AND+IsClosed=false+LIMIT+200" WITH HEADERS ADD "Accept" "application/json" ADD "Authorization" "Bearer {value.json.access_token}" # WRITE: Asset 360 only: update a standard Asset (status or your custom telemetry fields) ADD EVENT A360UpdateAsset WITH SOURCE_TOPIC "cmms/servicemax/a360-asset/update" WITH DESTINATION_TOPIC "cmms/servicemax/a360-asset/update/response" WITH METHOD "PATCH" WITH ENDPOINT "/services/data/v65.0/sobjects/Asset/{value.json.id}" WITH HEADERS ADD "Accept" "application/json" ADD "Authorization" "Bearer {value.json.access_token}" ADD "Content-Type" "application/json" WITH BODY "{value.json.body}" ``` Notification receiver (optional): ```lot theme={"theme":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} # ServiceMax notification receiver — REST_API route (server mode, beta, companion of ServicemaxRestApi.lot) # Receives JSON callouts that your Salesforce org sends when ServiceMax records change (a record-triggered Flow # with an HTTP Callout action, or an Apex trigger / queueable callout) and publishes the body on MQTT, so work # order and installed product changes reach the broker without polling. Opt-in: deploying it opens an HTTP port. # Docs: https://docs.coreflux.org/latest/lot-language/routes/rest-api-apps/servicemax # # Salesforce needs a public HTTPS URL registered as a Named Credential / External Credential (or Remote Site). # Put a reverse proxy or tunnel with a public TLS certificate in front of SERVER_PORT and check a shared # secret header there: this route does not authenticate callers. Salesforce Outbound Messages are SOAP and need # a SOAP true reply, which this route does not send, so use Flow HTTP Callouts / Apex (JSON) instead. # The body is whatever your Flow or Apex sends; treat it as JSON you defined. DEFINE CONNECTOR ServiceMaxNotificationReceiver WITH TYPE REST_API ADD REST_API_CONFIG WITH ENABLE_SERVER "true" WITH SERVER_PORT '9011' WITH DEFAULT_CONTENT_TYPE "application/json" # Flow HTTP Callout / Apex callout with JSON when a work order is created or changes (POST https:///servicemax/notify/work-orders) ADD EVENT NotifyWorkOrders WITH ENDPOINT "/servicemax/notify/work-orders" WITH METHOD "POST" WITH DESTINATION_TOPIC "cmms/servicemax/work-order-notification/received" # Same for installed product / Asset changes ADD EVENT NotifyInstalledProducts WITH ENDPOINT "/servicemax/notify/installed-products" WITH METHOD "POST" WITH DESTINATION_TOPIC "cmms/servicemax/installed-product-notification/received" # Any other JSON callout (cases, Asset 360 objects). Filter on the body in a LoT action ADD EVENT NotifyEvents WITH ENDPOINT "/servicemax/notify/events" WITH METHOD "POST" WITH DESTINATION_TOPIC "cmms/servicemax/notification/received" ``` ## Example actions These actions refresh the token every hour, open a work order when a compressor alarm is raised, write run hours to the installed product, and poll one technician's open work orders. ```lot theme={"theme":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} # Example actions for the ServiceMaxApi route (docs page). Not part of the route template. # Secrets: SALESFORCE_CLIENT_ID, SALESFORCE_CLIENT_SECRET (External Client App / Connected App with the client # credentials flow enabled and a Run As integration user that has the ServiceMax permission sets). The access # token is kept on the internal topic internal/servicemax/access_token. IDs (installed product, technician, account) # come from installed-product/find-by-serial and technician/list; replace the <...> placeholders below. DEFINE ACTION ServiceMaxGetToken ON EVERY 1 HOUR DO SET "cid" WITH GET SECRET "SALESFORCE_CLIENT_ID" SET "cs" WITH GET SECRET "SALESFORCE_CLIENT_SECRET" SET "form" WITH "grant_type=client_credentials&client_id=" + {cid} + "&client_secret=" + {cs} PUBLISH TOPIC "cmms/servicemax/token/create" WITH { "form": {form} } DEFINE ACTION ServiceMaxKeepToken ON TOPIC "cmms/servicemax/token/create/response" DO SET "tok" WITH (GET JSON "access_token" IN PAYLOAD AS STRING) KEEP TOPIC "internal/servicemax/access_token" WITH {tok} DEFINE ACTION ServiceMaxWorkOrderFromAlarm ON TOPIC "porto/line1/compressor/alarm/raised" DO SET "tok" WITH GET TOPIC "internal/servicemax/access_token" AS STRING SET "alarm" WITH (GET JSON "alarm" IN PAYLOAD AS STRING) SET "value" WITH (GET JSON "value" IN PAYLOAD AS STRING) SET "desc" WITH "Coreflux alarm " + {alarm} + " (value " + {value} + ")" PUBLISH TOPIC "cmms/servicemax/work-order/create" WITH { "access_token": {tok}, "body": { "SVMXC__Company__c": "", "SVMXC__Component__c": "", "SVMXC__Order_Type__c": "Field Service", "SVMXC__Priority__c": "High", "SVMXC__Problem_Description__c": {desc} } } DEFINE ACTION ServiceMaxRunHoursTelemetry ON TOPIC "porto/line1/press01/run-hours" DO SET "tok" WITH GET TOPIC "internal/servicemax/access_token" AS STRING SET "hours" WITH PAYLOAD AS DOUBLE PUBLISH TOPIC "cmms/servicemax/installed-product/update" WITH { "access_token": {tok}, "id": "", "body": { "Run_Hours__c": {hours} } } DEFINE ACTION ServiceMaxPollTechnicianWorkOrders ON EVERY 15 MINUTES DO SET "tok" WITH GET TOPIC "internal/servicemax/access_token" AS STRING PUBLISH TOPIC "cmms/servicemax/work-order/list-open-by-technician" WITH { "access_token": {tok}, "technician_id": "" } ``` ## Notes * The connector pins API `v65.0`. Change the version in the endpoints to move up; `ListApiVersions` shows what your org serves. * "Open" in the fixed queries means `SVMXC__Closed_On__c` is empty. If your org cancels work orders without a close date, use `Query` with your own status filter. * Every call counts against the org's rolling 24-hour API allocation (`GetLimits` shows `DailyApiRequests`). Over it, Salesforce answers `403 REQUEST_LIMIT_EXCEEDED`. Poll every few minutes, not per message. * Queries return up to 2000 rows per page; when `done` is false, pass the end of `nextRecordsUrl` to `QueryMore`. * Writes fire the org's ServiceMax triggers, flows, SLA clocks and dispatch rules. * `PublishPlatformEvent` needs a platform event you define (for example `Coreflux_Telemetry__e`) that Flows or Apex triggers subscribe to. ## Not covered Deletes, work detail lines and parts, ServiceMax SFM and dispatch services, Outbound Messages, Pub/Sub API and streaming subscriptions, Bulk API, files, and Asset 360 maintenance plans, warranties and service contracts. All are reachable through the Salesforce APIs and can be added to a fork. ## Related * [REST API connectors](/latest/lot-language/routes/rest-api-routes) * [REST API app templates](/latest/lot-language/routes/rest-api-apps/index) * [Salesforce](/latest/lot-language/routes/rest-api-apps/salesforce) * [ServiceMax Core: Work Order object](https://support.ptc.com/help/servicemaxcore/en/articles/core/work-order1.html) * [Asset 360: technical attributes REST API](https://support.ptc.com/help/servicemax_asset360/en/articles/asset-360/creating-technical-attributes-through-rest-apis.html) * [Salesforce REST API Developer Guide](https://developer.salesforce.com/docs/atlas.en-us.api_rest.meta/api_rest/) # ServiceNow Source: https://docs.coreflux.org/latest/lot-language/routes/rest-api-apps/servicenow LoT REST API connector for ServiceNow: open incidents from alarms, send Event Management events, patch CMDB CIs and create catalog tasks Technical Preview ## Overview A client-mode connector for a **curated ITSM / OT** slice of the [ServiceNow Table API](https://www.servicenow.com/docs/r/api-reference/rest-apis/c_TableAPI.html) and Event Management Connectors under `https://YOUR_INSTANCE.service-now.com`: | Catalog connector event | Entity / path | Notes | | - | - | - | | (discovery) | `GET /api/now/table/incident` | List incidents (`sysparm_limit` in template) | | POST table/incident from alarm | `POST /api/now/table/incident` | Create incident (flat JSON fields) | | POST em/inbound\_event (Event Management) | `POST /api/sn_em_connector/em/inbound_event?source=` | **Verified EM Connectors path** — not `/api/now/em/inbound_event` | | PATCH table/cmdb\_ci/\{sys\_id} | `PATCH /api/now/table/cmdb_ci/{sys_id}` | Generic CI table; fork for specific CI classes if needed | | GET table/sc\_task?sysparm\_query= | `GET /api/now/table/sc_task?sysparm_query=` | Catalog / fulfillment tasks | `BASE_ADDRESS` is `https://{instance}.service-now.com` (no trailing slash; endpoints already include `/api/...`). Topics use `itsm/servicenow//`. > [!NOTE] > > Catalog wording places `em/inbound_event` under `/api/now/`. Public Event Management Connectors documentation uses `/api/sn_em_connector/em/inbound_event?source={sourceName}` (for example `genericJson`). Configure a push connector / event rule on the instance before posting events. > [!NOTE] > > Prefer Hub secrets for tokens and passwords. ## Basic Syntax ```lot wrap theme={"theme":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} # Coreflux REST_API template — ServiceNow (Table API + EM curated slice) # Auth: Bearer {value.json.access_token} (OAuth) or Basic via config # BASE_ADDRESS: https://YOUR_INSTANCE.service-now.com DEFINE CONNECTOR ServiceNowRestApi WITH TYPE REST_API ADD REST_API_CONFIG WITH BASE_ADDRESS "https://YOUR_INSTANCE.service-now.com" WITH ENABLE_CLIENT "true" WITH USE_SSL "true" WITH REQUEST_TIMEOUT '60' WITH DEFAULT_CONTENT_TYPE "application/json" ADD EVENT Listincidents WITH SOURCE_TOPIC "itsm/servicenow/incident/list" WITH DESTINATION_TOPIC "itsm/servicenow/incident/list/response" WITH METHOD "GET" WITH ENDPOINT "/api/now/table/incident?sysparm_limit={value.json.sysparm_limit}" WITH HEADERS ADD "Accept" "application/json" ADD "Authorization" "Bearer {value.json.access_token}" ADD EVENT Createincident WITH SOURCE_TOPIC "itsm/servicenow/incident/create" WITH DESTINATION_TOPIC "itsm/servicenow/incident/create/response" WITH METHOD "POST" WITH ENDPOINT "/api/now/table/incident" WITH HEADERS ADD "Accept" "application/json" ADD "Content-Type" "application/json" ADD "Authorization" "Bearer {value.json.access_token}" WITH BODY "{payload}" ADD EVENT Postinboundevent WITH SOURCE_TOPIC "itsm/servicenow/em/inbound_event" WITH DESTINATION_TOPIC "itsm/servicenow/em/inbound_event/response" WITH METHOD "POST" WITH ENDPOINT "/api/sn_em_connector/em/inbound_event?source={value.json.source}" WITH HEADERS ADD "Accept" "application/json" ADD "Content-Type" "application/json" ADD "Authorization" "Bearer {value.json.access_token}" WITH BODY "{payload}" ADD EVENT Patchcmdbci WITH SOURCE_TOPIC "itsm/servicenow/cmdb_ci/patch" WITH DESTINATION_TOPIC "itsm/servicenow/cmdb_ci/patch/response" WITH METHOD "PATCH" WITH ENDPOINT "/api/now/table/cmdb_ci/{value.json.sys_id}" WITH HEADERS ADD "Accept" "application/json" ADD "Content-Type" "application/json" ADD "Authorization" "Bearer {value.json.access_token}" WITH BODY "{payload}" ADD EVENT Listsctasks WITH SOURCE_TOPIC "itsm/servicenow/sc_task/list" WITH DESTINATION_TOPIC "itsm/servicenow/sc_task/list/response" WITH METHOD "GET" WITH ENDPOINT "/api/now/table/sc_task?sysparm_query={value.json.sysparm_query}" WITH HEADERS ADD "Accept" "application/json" ADD "Authorization" "Bearer {value.json.access_token}" ``` ## Events | Event | Method | Endpoint | Request topic | Response topic | | - | - | - | - | - | | Listincidents | GET | `/api/now/table/incident?sysparm_limit=` | `itsm/servicenow/incident/list` | `itsm/servicenow/incident/list/response` | | Createincident | POST | `/api/now/table/incident` | `itsm/servicenow/incident/create` | `itsm/servicenow/incident/create/response` | | Postinboundevent | POST | `/api/sn_em_connector/em/inbound_event?source=` | `itsm/servicenow/em/inbound_event` | `itsm/servicenow/em/inbound_event/response` | | Patchcmdbci | PATCH | `/api/now/table/cmdb_ci/{sys_id}` | `itsm/servicenow/cmdb_ci/patch` | `itsm/servicenow/cmdb_ci/patch/response` | | Listsctasks | GET | `/api/now/table/sc_task?sysparm_query=` | `itsm/servicenow/sc_task/list` | `itsm/servicenow/sc_task/list/response` | ## Auth **OAuth 2.0 Bearer — default in the template:** 1. Register an OAuth application on the ServiceNow instance (System OAuth → Application Registry). 2. Obtain an `access_token`. 3. Set connector `BASE_ADDRESS` to `https://{instance}.service-now.com`. 4. Pass `access_token` on the MQTT request payload so headers resolve: * `Authorization: Bearer {value.json.access_token}` 5. Store the token in Hub secrets / `GET SECRET`. Never commit real credentials. **Basic (username / password):** 1. Create an integration user with roles for Table API / Event Management as needed (e.g. `itil`, `rest_service`, EM connector roles). 2. Prefer `WITH USERNAME "integration.user"` and `WITH PASSWORD GET SECRET "SERVICENOW_PASSWORD"` on `REST_API_CONFIG` (remove Bearer header lines if using config Basic). ## Example payloads **Create incident from alarm:** ```json theme={"theme":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} { "access_token": "YOUR_ACCESS_TOKEN", "short_description": "Line sensor fault — Zone A", "description": "Vibration above threshold at 2026-10-04T00:00:00Z", "urgency": "2", "impact": "2", "category": "Hardware" } ``` **EM inbound event:** ```json theme={"theme":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} { "access_token": "YOUR_ACCESS_TOKEN", "source": "genericJson", "event_class": "IIoTAlarm", "node": "plc-line-a", "type": "High Vibration", "resource": "ZoneA/Sensor01", "severity": "3", "description": "Vibration above threshold" } ``` **PATCH CMDB CI:** ```json theme={"theme":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} { "access_token": "YOUR_ACCESS_TOKEN", "sys_id": "a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6", "operational_status": "2", "short_description": "Updated from Coreflux telemetry" } ``` **List sc\_task:** ```json theme={"theme":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} { "access_token": "YOUR_ACCESS_TOKEN", "sysparm_query": "state=1^ORDERBYnumber" } ``` ## Deploy notes * Deploy **`ServiceNowRestApi.lot` only** (`-addConnector`). Do **not** deploy `hub-metadata.lot` as a connector — it exists for Hub / `-listTemplates` (`ADD METADATA` + `WITH IMAGE` base64). * Hub category: **REST API Connectors**. * Doc link: [https://docs.coreflux.org/latest/lot-language/routes/rest-api-apps/servicenow](https://docs.coreflux.org/latest/lot-language/routes/rest-api-apps/servicenow) ## Related * [REST API Connectors](/latest/lot-language/routes/rest-api-routes) * [REST API Apps catalog](/latest/lot-language/routes/rest-api-apps) # SharePoint and OneDrive (Microsoft Graph) Source: https://docs.coreflux.org/latest/lot-language/routes/rest-api-apps/sharepoint-and-onedrive LoT REST API connector for SharePoint and OneDrive via Microsoft Graph: upload files, create list items, list drive children and run large-file uploads Technical Preview ## Overview A client-mode connector for a **curated storage** slice of [Microsoft Graph OneDrive / SharePoint](https://learn.microsoft.com/en-us/graph/api/resources/onedrive) under `https://graph.microsoft.com/v1.0`: | Catalog connector event | Entity / path | Notes | | - | - | - | | PUT drives/\{id}/root:/\{path}:/content | `PUT /drives/{drive-id}/root:/{item-path}:/content` | Small file upload/replace (≤ \~250 MB) — Bearer Entra token | | POST sites/\{id}/lists/\{id}/items | `POST /sites/{site-id}/lists/{list-id}/items` | Create list item (device register) — Bearer | | GET drives/\{id}/root/children | `GET /drives/{drive-id}/root/children` | List children under drive root — Bearer | | (supporting) createUploadSession | `POST /drives/{drive-id}/root:/{item-path}:/createUploadSession` | Large files — returns `uploadUrl` on another host | | (supporting) GET by path | `GET /drives/{drive-id}/root:/{item-path}:` | driveItem metadata | | (supporting) PUT upload chunk | `PUT {upload_path}` | **Host:** uploadUrl origin — no Authorization | Default `BASE_ADDRESS` is `https://graph.microsoft.com/v1.0` (no trailing slash). Topics use `storage/sharepoint-and-onedrive//`. See the base guide: [REST API Connectors](/latest/lot-language/routes/rest-api-routes). > [!NOTE] > > **Path encoding:** `item_path` is the path under the drive root **without** a leading slash (e.g. `FolderA/telemetry.csv`). URL-encode path segments in the publisher if they contain spaces or reserved characters. LoT ENDPOINT uses `/drives/{value.json.drive_id}/root:/{value.json.item_path}:/content`. > [!NOTE] > > **Large upload sessions:** `createUploadSession` returns an `uploadUrl` that is **not** on `graph.microsoft.com`. When calling `Putuploadchunk`, set connector `BASE_ADDRESS` to that URL's origin (scheme + host + port) and put path + query in `upload_path`. Do not send a Graph `Authorization` header — the session URL embeds auth. Same pattern as Teams Workflows webhooks and S3 presigned PUT. > [!NOTE] > > Prefer Hub secrets for Entra access tokens (`GET SECRET "GRAPH_ACCESS_TOKEN"` or `"SHAREPOINT_GRAPH_TOKEN"`). Typical scopes: `Files.ReadWrite.All`, `Sites.ReadWrite.All`, and `offline_access` for refresh. ## Basic Syntax ```lot wrap theme={"theme":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} # Coreflux REST_API template — SharePoint and OneDrive (Microsoft Graph) # Auth: Bearer {value.json.access_token} # BASE_ADDRESS: https://graph.microsoft.com/v1.0 # Putuploadchunk: set BASE_ADDRESS to uploadUrl origin DEFINE CONNECTOR SharepointAndOnedriveRestApi WITH TYPE REST_API ADD REST_API_CONFIG WITH BASE_ADDRESS "https://graph.microsoft.com/v1.0" WITH ENABLE_CLIENT "true" WITH USE_SSL "true" WITH REQUEST_TIMEOUT '60' WITH DEFAULT_CONTENT_TYPE "application/json" ADD EVENT Putdriveitemcontent WITH SOURCE_TOPIC "storage/sharepoint-and-onedrive/drive-item/put-content" WITH DESTINATION_TOPIC "storage/sharepoint-and-onedrive/drive-item/put-content/response" WITH METHOD "PUT" WITH ENDPOINT "/drives/{value.json.drive_id}/root:/{value.json.item_path}:/content" WITH HEADERS ADD "Accept" "application/json" ADD "Content-Type" "{value.json.content_type}" ADD "Authorization" "Bearer {value.json.access_token}" WITH BODY "{payload}" ADD EVENT Postlistitem WITH SOURCE_TOPIC "storage/sharepoint-and-onedrive/list-item/create" WITH DESTINATION_TOPIC "storage/sharepoint-and-onedrive/list-item/create/response" WITH METHOD "POST" WITH ENDPOINT "/sites/{value.json.site_id}/lists/{value.json.list_id}/items" WITH HEADERS ADD "Accept" "application/json" ADD "Content-Type" "application/json" ADD "Authorization" "Bearer {value.json.access_token}" WITH BODY "{payload}" ADD EVENT Listdrivechildren WITH SOURCE_TOPIC "storage/sharepoint-and-onedrive/drive-children/list" WITH DESTINATION_TOPIC "storage/sharepoint-and-onedrive/drive-children/list/response" WITH METHOD "GET" WITH ENDPOINT "/drives/{value.json.drive_id}/root/children" WITH HEADERS ADD "Accept" "application/json" ADD "Authorization" "Bearer {value.json.access_token}" ADD EVENT Createuploadsession WITH SOURCE_TOPIC "storage/sharepoint-and-onedrive/drive-item/create-upload-session" WITH DESTINATION_TOPIC "storage/sharepoint-and-onedrive/drive-item/create-upload-session/response" WITH METHOD "POST" WITH ENDPOINT "/drives/{value.json.drive_id}/root:/{value.json.item_path}:/createUploadSession" WITH HEADERS ADD "Accept" "application/json" ADD "Content-Type" "application/json" ADD "Authorization" "Bearer {value.json.access_token}" WITH BODY "{payload}" ADD EVENT Getdriveitembypath WITH SOURCE_TOPIC "storage/sharepoint-and-onedrive/drive-item/get-by-path" WITH DESTINATION_TOPIC "storage/sharepoint-and-onedrive/drive-item/get-by-path/response" WITH METHOD "GET" WITH ENDPOINT "/drives/{value.json.drive_id}/root:/{value.json.item_path}:" WITH HEADERS ADD "Accept" "application/json" ADD "Authorization" "Bearer {value.json.access_token}" ADD EVENT Putuploadchunk WITH SOURCE_TOPIC "storage/sharepoint-and-onedrive/upload-session/put-chunk" WITH DESTINATION_TOPIC "storage/sharepoint-and-onedrive/upload-session/put-chunk/response" WITH METHOD "PUT" WITH ENDPOINT "{value.json.upload_path}" WITH HEADERS ADD "Content-Type" "application/octet-stream" ADD "Content-Length" "{value.json.content_length}" ADD "Content-Range" "{value.json.content_range}" WITH BODY "{payload}" ``` ## Auth 1. **Graph events:** pass Entra ID `access_token` on each MQTT payload → `Authorization: Bearer {value.json.access_token}`. Prefer Hub secrets / `GET SECRET "GRAPH_ACCESS_TOKEN"` or `"SHAREPOINT_GRAPH_TOKEN"`. Never commit real tokens. 2. **Scopes:** `Files.ReadWrite.All` (drive content), `Sites.ReadWrite.All` (list items), `offline_access` (refresh). Application (client credentials) or delegated depending on your Entra app registration. 3. **Upload chunk:** no Authorization header. Split `uploadUrl` into origin → `BASE_ADDRESS` and path+query → `upload_path`. Send `Content-Range` / `Content-Length` as placeholders. ## MQTT topics | Event | Source topic | Response topic | | - | - | - | | Putdriveitemcontent | `storage/sharepoint-and-onedrive/drive-item/put-content` | `…/response` | | Postlistitem | `storage/sharepoint-and-onedrive/list-item/create` | `…/response` | | Listdrivechildren | `storage/sharepoint-and-onedrive/drive-children/list` | `…/response` | | Createuploadsession | `storage/sharepoint-and-onedrive/drive-item/create-upload-session` | `…/response` | | Getdriveitembypath | `storage/sharepoint-and-onedrive/drive-item/get-by-path` | `…/response` | | Putuploadchunk | `storage/sharepoint-and-onedrive/upload-session/put-chunk` | `…/response` | ## Example payloads **List drive children** (`BASE_ADDRESS` = `https://graph.microsoft.com/v1.0`): ```json theme={"theme":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} { "access_token": "YOUR_ENTRA_ACCESS_TOKEN", "drive_id": "b!xxxxxxxx" } ``` **Upload small file content:** ```json theme={"theme":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} { "access_token": "YOUR_ENTRA_ACCESS_TOKEN", "drive_id": "b!xxxxxxxx", "item_path": "IIoT/snapshots/line-1.csv", "content_type": "text/csv" } ``` Publish the file bytes (or text) as the MQTT payload body that `{payload}` forwards. Ensure `content_type` matches the body. Path/header placeholders come from `value.json.*`. **Create list item (device register):** ```json theme={"theme":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} { "access_token": "YOUR_ENTRA_ACCESS_TOKEN", "site_id": "contoso.sharepoint.com,guid,guid", "list_id": "LIST_GUID", "fields": { "Title": "device-42", "Status": "online" } } ``` **Create upload session (large file):** ```json theme={"theme":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} { "access_token": "YOUR_ENTRA_ACCESS_TOKEN", "drive_id": "b!xxxxxxxx", "item_path": "IIoT/archives/big.bin", "item": { "@microsoft.graph.conflictBehavior": "replace" } } ``` Then `Putuploadchunk` with `BASE_ADDRESS` = uploadUrl origin: ```json theme={"theme":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} { "upload_path": "/upn/…?guid=…", "content_length": "1048576", "content_range": "bytes 0-1048575/5242880" } ``` ## Sources * [https://learn.microsoft.com/en-us/graph/api/driveitem-put-content](https://learn.microsoft.com/en-us/graph/api/driveitem-put-content) * [https://learn.microsoft.com/en-us/graph/api/driveitem-createuploadsession](https://learn.microsoft.com/en-us/graph/api/driveitem-createuploadsession) * [https://learn.microsoft.com/en-us/graph/api/driveitem-list-children](https://learn.microsoft.com/en-us/graph/api/driveitem-list-children) * [https://learn.microsoft.com/en-us/graph/api/listitem-create](https://learn.microsoft.com/en-us/graph/api/listitem-create) * [https://learn.microsoft.com/en-us/graph/api/driveitem-get](https://learn.microsoft.com/en-us/graph/api/driveitem-get) * [https://learn.microsoft.com/en-us/graph/api/resources/onedrive](https://learn.microsoft.com/en-us/graph/api/resources/onedrive) * [https://learn.microsoft.com/en-us/graph/api/resources/sharepoint](https://learn.microsoft.com/en-us/graph/api/resources/sharepoint) * [https://learn.microsoft.com/en-us/connectors/sharepointonline/](https://learn.microsoft.com/en-us/connectors/sharepointonline/) ## Related * [REST API Connectors](/latest/lot-language/routes/rest-api-routes) * [Amazon S3](/latest/lot-language/routes/rest-api-apps/amazon-s3) * [Azure Blob Storage and Data Lake Gen2](/latest/lot-language/routes/rest-api-apps/azure-blob-storage-and-data-lake-gen2) * [MinIO](/latest/lot-language/routes/rest-api-apps/minio) # Shelly Cloud Control Source: https://docs.coreflux.org/latest/lot-language/routes/rest-api-apps/shelly-cloud-control LoT REST API connector for the Shelly Cloud Control API v2: list devices and set switches, covers, lights and groups from MQTT with a cloud auth key Technical Preview ## Overview Client-mode REST API connectors for Shelly Gen2+ devices (local RPC over HTTP) and the Shelly Cloud Control API v2: switch, cover, light, power and 3-phase energy meter status, energy history, webhooks, relay and cover control. This is one of the templates in the Shelly (Gen2+ local RPC and Shelly Cloud) REST API app (`shelly`). Deploy it alongside the other connectors of that app when they depend on each other (for example a token connector plus an API connector). ## Auth * **schemes**: \["none (local, device auth off)", "auth\_key query parameter (cloud)"] * **secret**: SHELLY\_CLOUD\_AUTH\_KEY * **note**: Local HTTP RPC uses SHA-256 Digest (RFC 7616) when auth is enabled; the REST\_API connector cannot answer the per-request nonce challenge, so protected devices return 401 except Shelly.GetDeviceInfo and /shelly. Use the device's native MQTT (\/rpc) or the cloud connector for protected devices. ## Template parameters | Placeholder | Type | Description | Required | | - | - | - | - | | `RouteName` | string | Name of the Shelly Cloud Control connector | no | | `BaseUrl` | string | Shelly Cloud Control base URL without trailing slash (e.g. [https://shelly-49-eu.shelly.cloud](https://shelly-49-eu.shelly.cloud)) | yes | `BASE_ADDRESS` default: `https://shelly-49-eu.shelly.cloud`. ## Events | Event | Method | Endpoint | Request topic | Response topic | Payload fields | | - | - | - | - | - | - | | `CloudGetDevices` | POST | `/v2/devices/api/get?auth_key={value.json.auth_key}` | `energy/shelly/cloud/devices/get` | `energy/shelly/cloud/devices/get/response` | `auth_key`, `body` | | `CloudSetSwitch` | POST | `/v2/devices/api/set/switch?auth_key={value.json.auth_key}` | `energy/shelly/cloud/switch/set` | `energy/shelly/cloud/switch/set/response` | `auth_key`, `body` | | `CloudSetCover` | POST | `/v2/devices/api/set/cover?auth_key={value.json.auth_key}` | `energy/shelly/cloud/cover/set` | `energy/shelly/cloud/cover/set/response` | `auth_key`, `body` | | `CloudSetLight` | POST | `/v2/devices/api/set/light?auth_key={value.json.auth_key}` | `energy/shelly/cloud/light/set` | `energy/shelly/cloud/light/set/response` | `auth_key`, `body` | | `CloudSetGroups` | POST | `/v2/devices/api/set/groups?auth_key={value.json.auth_key}` | `energy/shelly/cloud/groups/set` | `energy/shelly/cloud/groups/set/response` | `auth_key`, `body` | ## Deploy Get the template card (JSON with `template` and `metadata`) from the broker, then deploy the connector: ```bash theme={"theme":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} mosquitto_pub -t '$SYS/Coreflux/Command' -m "-connectorCode SHELLY_CLOUD_CONTROL" ``` The deployable connector from the REST API app, with `BASE_ADDRESS` already filled in: ```lot theme={"theme":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} DEFINE CONNECTOR ShellyCloudControl WITH TYPE REST_API ADD REST_API_CONFIG WITH BASE_ADDRESS "https://shelly-49-eu.shelly.cloud" WITH ENABLE_CLIENT "true" WITH USE_SSL "true" WITH REQUEST_TIMEOUT '10' WITH DEFAULT_CONTENT_TYPE "application/json" ADD EVENT CloudGetDevices WITH SOURCE_TOPIC "energy/shelly/cloud/devices/get" WITH DESTINATION_TOPIC "energy/shelly/cloud/devices/get/response" WITH METHOD "POST" WITH ENDPOINT "/v2/devices/api/get?auth_key={value.json.auth_key}" WITH HEADERS ADD "Accept" "application/json" ADD "User-Agent" "Coreflux-Broker REST_API route (https://www.coreflux.org)" ADD "Content-Type" "application/json" WITH BODY "{value.json.body}" ADD EVENT CloudSetSwitch WITH SOURCE_TOPIC "energy/shelly/cloud/switch/set" WITH DESTINATION_TOPIC "energy/shelly/cloud/switch/set/response" WITH METHOD "POST" WITH ENDPOINT "/v2/devices/api/set/switch?auth_key={value.json.auth_key}" WITH HEADERS ADD "Accept" "application/json" ADD "User-Agent" "Coreflux-Broker REST_API route (https://www.coreflux.org)" ADD "Content-Type" "application/json" WITH BODY "{value.json.body}" ADD EVENT CloudSetCover WITH SOURCE_TOPIC "energy/shelly/cloud/cover/set" WITH DESTINATION_TOPIC "energy/shelly/cloud/cover/set/response" WITH METHOD "POST" WITH ENDPOINT "/v2/devices/api/set/cover?auth_key={value.json.auth_key}" WITH HEADERS ADD "Accept" "application/json" ADD "User-Agent" "Coreflux-Broker REST_API route (https://www.coreflux.org)" ADD "Content-Type" "application/json" WITH BODY "{value.json.body}" ADD EVENT CloudSetLight WITH SOURCE_TOPIC "energy/shelly/cloud/light/set" WITH DESTINATION_TOPIC "energy/shelly/cloud/light/set/response" WITH METHOD "POST" WITH ENDPOINT "/v2/devices/api/set/light?auth_key={value.json.auth_key}" WITH HEADERS ADD "Accept" "application/json" ADD "User-Agent" "Coreflux-Broker REST_API route (https://www.coreflux.org)" ADD "Content-Type" "application/json" WITH BODY "{value.json.body}" ADD EVENT CloudSetGroups WITH SOURCE_TOPIC "energy/shelly/cloud/groups/set" WITH DESTINATION_TOPIC "energy/shelly/cloud/groups/set/response" WITH METHOD "POST" WITH ENDPOINT "/v2/devices/api/set/groups?auth_key={value.json.auth_key}" WITH HEADERS ADD "Accept" "application/json" ADD "User-Agent" "Coreflux-Broker REST_API route (https://www.coreflux.org)" ADD "Content-Type" "application/json" WITH BODY "{value.json.body}" ``` Deploy it with `-addConnector` (do not include `ADD METADATA`). ## Usage example Call `CloudGetDevices` by publishing a JSON payload to its request topic, and read the reply on the response topic: ```bash theme={"theme":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} mosquitto_sub -t 'energy/shelly/cloud/devices/get/response' & mosquitto_pub -t 'energy/shelly/cloud/devices/get' -m '{"auth_key": "", "body": {}}' ``` ## Related * [REST API Connectors](/latest/lot-language/routes/rest-api-routes) * [REST API Apps catalog](/latest/lot-language/routes/rest-api-apps) # Shelly Gen2 Rpc Source: https://docs.coreflux.org/latest/lot-language/routes/rest-api-apps/shelly-gen2-rpc LoT REST API connector for Shelly Gen2+ local RPC over HTTP: read device, switch, cover, light and energy meter status, control relays and manage webhooks Technical Preview ## Overview Client-mode REST API connectors for Shelly Gen2+ devices (local RPC over HTTP) and the Shelly Cloud Control API v2: switch, cover, light, power and 3-phase energy meter status, energy history, webhooks, relay and cover control. This is one of the templates in the Shelly (Gen2+ local RPC and Shelly Cloud) REST API app (`shelly`). Deploy it alongside the other connectors of that app when they depend on each other (for example a token connector plus an API connector). ## Auth * **schemes**: \["none (local, device auth off)", "auth\_key query parameter (cloud)"] * **secret**: SHELLY\_CLOUD\_AUTH\_KEY * **note**: Local HTTP RPC uses SHA-256 Digest (RFC 7616) when auth is enabled; the REST\_API connector cannot answer the per-request nonce challenge, so protected devices return 401 except Shelly.GetDeviceInfo and /shelly. Use the device's native MQTT (\/rpc) or the cloud connector for protected devices. ## Template parameters | Placeholder | Type | Description | Required | | - | - | - | - | | `RouteName` | string | Name of the Shelly Gen2 Rpc connector | no | | `BaseUrl` | string | Shelly Gen2 Rpc base URL without trailing slash (e.g. [http://192.168.33.1](http://192.168.33.1)) | yes | `BASE_ADDRESS` default: `http://192.168.33.1`. ## Events | Event | Method | Endpoint | Request topic | Response topic | Payload fields | | - | - | - | - | - | - | | `GetShellyIdentity` | GET | `/shelly` | `energy/shelly/identity/get` | `energy/shelly/identity/get/response` | none | | `GetDeviceInfo` | GET | `/rpc/Shelly.GetDeviceInfo` | `energy/shelly/device-info/get` | `energy/shelly/device-info/get/response` | none | | `GetStatus` | GET | `/rpc/Shelly.GetStatus` | `energy/shelly/status/get` | `energy/shelly/status/get/response` | none | | `GetConfig` | GET | `/rpc/Shelly.GetConfig` | `energy/shelly/config/get` | `energy/shelly/config/get/response` | none | | `ListMethods` | GET | `/rpc/Shelly.ListMethods` | `energy/shelly/methods/get` | `energy/shelly/methods/get/response` | none | | `CheckForUpdate` | GET | `/rpc/Shelly.CheckForUpdate` | `energy/shelly/update/check` | `energy/shelly/update/check/response` | none | | `SysGetStatus` | GET | `/rpc/Sys.GetStatus` | `energy/shelly/sys/status/get` | `energy/shelly/sys/status/get/response` | none | | `SwitchGetStatus` | GET | `/rpc/Switch.GetStatus?id={value.json.id}` | `energy/shelly/switch/status/get` | `energy/shelly/switch/status/get/response` | `id` | | `SwitchGetConfig` | GET | `/rpc/Switch.GetConfig?id={value.json.id}` | `energy/shelly/switch/config/get` | `energy/shelly/switch/config/get/response` | `id` | | `InputGetStatus` | GET | `/rpc/Input.GetStatus?id={value.json.id}` | `energy/shelly/input/status/get` | `energy/shelly/input/status/get/response` | `id` | | `CoverGetStatus` | GET | `/rpc/Cover.GetStatus?id={value.json.id}` | `energy/shelly/cover/status/get` | `energy/shelly/cover/status/get/response` | `id` | | `LightGetStatus` | GET | `/rpc/Light.GetStatus?id={value.json.id}` | `energy/shelly/light/status/get` | `energy/shelly/light/status/get/response` | `id` | | `Pm1GetStatus` | GET | `/rpc/PM1.GetStatus?id={value.json.id}` | `energy/shelly/pm1/status/get` | `energy/shelly/pm1/status/get/response` | `id` | | `EmGetStatus` | GET | `/rpc/EM.GetStatus?id={value.json.id}` | `energy/shelly/em/status/get` | `energy/shelly/em/status/get/response` | `id` | | `EmDataGetStatus` | GET | `/rpc/EMData.GetStatus?id={value.json.id}` | `energy/shelly/emdata/status/get` | `energy/shelly/emdata/status/get/response` | `id` | | `EmDataGetData` | GET | `/rpc/EMData.GetData?id={value.json.id}&ts={value.json.ts}&end_ts={value.json.end_ts}` | `energy/shelly/emdata/data/get` | `energy/shelly/emdata/data/get/response` | `end_ts`, `id`, `ts` | | `TemperatureGetStatus` | GET | `/rpc/Temperature.GetStatus?id={value.json.id}` | `energy/shelly/temperature/status/get` | `energy/shelly/temperature/status/get/response` | `id` | | `WebhookList` | GET | `/rpc/Webhook.List` | `energy/shelly/webhook/list` | `energy/shelly/webhook/list/response` | none | | `SwitchSet` | POST | `/rpc/Switch.Set` | `energy/shelly/switch/set` | `energy/shelly/switch/set/response` | `body` | | `SwitchToggle` | POST | `/rpc/Switch.Toggle` | `energy/shelly/switch/toggle` | `energy/shelly/switch/toggle/response` | `body` | | `CoverOpen` | POST | `/rpc/Cover.Open` | `energy/shelly/cover/open` | `energy/shelly/cover/open/response` | `body` | | `CoverClose` | POST | `/rpc/Cover.Close` | `energy/shelly/cover/close` | `energy/shelly/cover/close/response` | `body` | | `CoverStop` | POST | `/rpc/Cover.Stop` | `energy/shelly/cover/stop` | `energy/shelly/cover/stop/response` | `body` | | `CoverGoToPosition` | POST | `/rpc/Cover.GoToPosition` | `energy/shelly/cover/position/set` | `energy/shelly/cover/position/set/response` | `body` | | `LightSet` | POST | `/rpc/Light.Set` | `energy/shelly/light/set` | `energy/shelly/light/set/response` | `body` | | `WebhookCreate` | POST | `/rpc/Webhook.Create` | `energy/shelly/webhook/create` | `energy/shelly/webhook/create/response` | `body` | | `WebhookDelete` | POST | `/rpc/Webhook.Delete` | `energy/shelly/webhook/delete` | `energy/shelly/webhook/delete/response` | `body` | | `Reboot` | POST | `/rpc/Shelly.Reboot` | `energy/shelly/reboot` | `energy/shelly/reboot/response` | `body` | | `Rpc` | POST | `/rpc` | `energy/shelly/rpc` | `energy/shelly/rpc/response` | `body` | ## Deploy Get the template card (JSON with `template` and `metadata`) from the broker, then deploy the connector: ```bash theme={"theme":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} mosquitto_pub -t '$SYS/Coreflux/Command' -m "-connectorCode SHELLY_GEN2_RPC" ``` The deployable connector from the REST API app, with `BASE_ADDRESS` already filled in: ```lot theme={"theme":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} DEFINE CONNECTOR ShellyGen2Rpc WITH TYPE REST_API ADD REST_API_CONFIG WITH BASE_ADDRESS "http://192.168.33.1" WITH ENABLE_CLIENT "true" WITH USE_SSL "false" WITH REQUEST_TIMEOUT '10' WITH DEFAULT_CONTENT_TYPE "application/json" ADD EVENT GetShellyIdentity WITH SOURCE_TOPIC "energy/shelly/identity/get" WITH DESTINATION_TOPIC "energy/shelly/identity/get/response" WITH METHOD "GET" WITH ENDPOINT "/shelly" WITH HEADERS ADD "Accept" "application/json" ADD "User-Agent" "Coreflux-Broker REST_API route (https://www.coreflux.org)" ADD EVENT GetDeviceInfo WITH SOURCE_TOPIC "energy/shelly/device-info/get" WITH DESTINATION_TOPIC "energy/shelly/device-info/get/response" WITH METHOD "GET" WITH ENDPOINT "/rpc/Shelly.GetDeviceInfo" WITH HEADERS ADD "Accept" "application/json" ADD "User-Agent" "Coreflux-Broker REST_API route (https://www.coreflux.org)" ADD EVENT GetStatus WITH SOURCE_TOPIC "energy/shelly/status/get" WITH DESTINATION_TOPIC "energy/shelly/status/get/response" WITH METHOD "GET" WITH ENDPOINT "/rpc/Shelly.GetStatus" WITH HEADERS ADD "Accept" "application/json" ADD "User-Agent" "Coreflux-Broker REST_API route (https://www.coreflux.org)" ADD EVENT GetConfig WITH SOURCE_TOPIC "energy/shelly/config/get" WITH DESTINATION_TOPIC "energy/shelly/config/get/response" WITH METHOD "GET" WITH ENDPOINT "/rpc/Shelly.GetConfig" WITH HEADERS ADD "Accept" "application/json" ADD "User-Agent" "Coreflux-Broker REST_API route (https://www.coreflux.org)" ADD EVENT ListMethods WITH SOURCE_TOPIC "energy/shelly/methods/get" WITH DESTINATION_TOPIC "energy/shelly/methods/get/response" WITH METHOD "GET" WITH ENDPOINT "/rpc/Shelly.ListMethods" WITH HEADERS ADD "Accept" "application/json" ADD "User-Agent" "Coreflux-Broker REST_API route (https://www.coreflux.org)" ADD EVENT CheckForUpdate WITH SOURCE_TOPIC "energy/shelly/update/check" WITH DESTINATION_TOPIC "energy/shelly/update/check/response" WITH METHOD "GET" WITH ENDPOINT "/rpc/Shelly.CheckForUpdate" WITH HEADERS ADD "Accept" "application/json" ADD "User-Agent" "Coreflux-Broker REST_API route (https://www.coreflux.org)" ADD EVENT SysGetStatus WITH SOURCE_TOPIC "energy/shelly/sys/status/get" WITH DESTINATION_TOPIC "energy/shelly/sys/status/get/response" WITH METHOD "GET" WITH ENDPOINT "/rpc/Sys.GetStatus" WITH HEADERS ADD "Accept" "application/json" ADD "User-Agent" "Coreflux-Broker REST_API route (https://www.coreflux.org)" ADD EVENT SwitchGetStatus WITH SOURCE_TOPIC "energy/shelly/switch/status/get" WITH DESTINATION_TOPIC "energy/shelly/switch/status/get/response" WITH METHOD "GET" WITH ENDPOINT "/rpc/Switch.GetStatus?id={value.json.id}" WITH HEADERS ADD "Accept" "application/json" ADD "User-Agent" "Coreflux-Broker REST_API route (https://www.coreflux.org)" ADD EVENT SwitchGetConfig WITH SOURCE_TOPIC "energy/shelly/switch/config/get" WITH DESTINATION_TOPIC "energy/shelly/switch/config/get/response" WITH METHOD "GET" WITH ENDPOINT "/rpc/Switch.GetConfig?id={value.json.id}" WITH HEADERS ADD "Accept" "application/json" ADD "User-Agent" "Coreflux-Broker REST_API route (https://www.coreflux.org)" ADD EVENT InputGetStatus WITH SOURCE_TOPIC "energy/shelly/input/status/get" WITH DESTINATION_TOPIC "energy/shelly/input/status/get/response" WITH METHOD "GET" WITH ENDPOINT "/rpc/Input.GetStatus?id={value.json.id}" WITH HEADERS ADD "Accept" "application/json" ADD "User-Agent" "Coreflux-Broker REST_API route (https://www.coreflux.org)" ADD EVENT CoverGetStatus WITH SOURCE_TOPIC "energy/shelly/cover/status/get" WITH DESTINATION_TOPIC "energy/shelly/cover/status/get/response" WITH METHOD "GET" WITH ENDPOINT "/rpc/Cover.GetStatus?id={value.json.id}" WITH HEADERS ADD "Accept" "application/json" ADD "User-Agent" "Coreflux-Broker REST_API route (https://www.coreflux.org)" ADD EVENT LightGetStatus WITH SOURCE_TOPIC "energy/shelly/light/status/get" WITH DESTINATION_TOPIC "energy/shelly/light/status/get/response" WITH METHOD "GET" WITH ENDPOINT "/rpc/Light.GetStatus?id={value.json.id}" WITH HEADERS ADD "Accept" "application/json" ADD "User-Agent" "Coreflux-Broker REST_API route (https://www.coreflux.org)" ADD EVENT Pm1GetStatus WITH SOURCE_TOPIC "energy/shelly/pm1/status/get" WITH DESTINATION_TOPIC "energy/shelly/pm1/status/get/response" WITH METHOD "GET" WITH ENDPOINT "/rpc/PM1.GetStatus?id={value.json.id}" WITH HEADERS ADD "Accept" "application/json" ADD "User-Agent" "Coreflux-Broker REST_API route (https://www.coreflux.org)" ADD EVENT EmGetStatus WITH SOURCE_TOPIC "energy/shelly/em/status/get" WITH DESTINATION_TOPIC "energy/shelly/em/status/get/response" WITH METHOD "GET" WITH ENDPOINT "/rpc/EM.GetStatus?id={value.json.id}" WITH HEADERS ADD "Accept" "application/json" ADD "User-Agent" "Coreflux-Broker REST_API route (https://www.coreflux.org)" ADD EVENT EmDataGetStatus WITH SOURCE_TOPIC "energy/shelly/emdata/status/get" WITH DESTINATION_TOPIC "energy/shelly/emdata/status/get/response" WITH METHOD "GET" WITH ENDPOINT "/rpc/EMData.GetStatus?id={value.json.id}" WITH HEADERS ADD "Accept" "application/json" ADD "User-Agent" "Coreflux-Broker REST_API route (https://www.coreflux.org)" ADD EVENT EmDataGetData WITH SOURCE_TOPIC "energy/shelly/emdata/data/get" WITH DESTINATION_TOPIC "energy/shelly/emdata/data/get/response" WITH METHOD "GET" WITH ENDPOINT "/rpc/EMData.GetData?id={value.json.id}&ts={value.json.ts}&end_ts={value.json.end_ts}" WITH HEADERS ADD "Accept" "application/json" ADD "User-Agent" "Coreflux-Broker REST_API route (https://www.coreflux.org)" ADD EVENT TemperatureGetStatus WITH SOURCE_TOPIC "energy/shelly/temperature/status/get" WITH DESTINATION_TOPIC "energy/shelly/temperature/status/get/response" WITH METHOD "GET" WITH ENDPOINT "/rpc/Temperature.GetStatus?id={value.json.id}" WITH HEADERS ADD "Accept" "application/json" ADD "User-Agent" "Coreflux-Broker REST_API route (https://www.coreflux.org)" ADD EVENT WebhookList WITH SOURCE_TOPIC "energy/shelly/webhook/list" WITH DESTINATION_TOPIC "energy/shelly/webhook/list/response" WITH METHOD "GET" WITH ENDPOINT "/rpc/Webhook.List" WITH HEADERS ADD "Accept" "application/json" ADD "User-Agent" "Coreflux-Broker REST_API route (https://www.coreflux.org)" ADD EVENT SwitchSet WITH SOURCE_TOPIC "energy/shelly/switch/set" WITH DESTINATION_TOPIC "energy/shelly/switch/set/response" WITH METHOD "POST" WITH ENDPOINT "/rpc/Switch.Set" WITH HEADERS ADD "Accept" "application/json" ADD "User-Agent" "Coreflux-Broker REST_API route (https://www.coreflux.org)" ADD "Content-Type" "application/json" WITH BODY "{value.json.body}" ADD EVENT SwitchToggle WITH SOURCE_TOPIC "energy/shelly/switch/toggle" WITH DESTINATION_TOPIC "energy/shelly/switch/toggle/response" WITH METHOD "POST" WITH ENDPOINT "/rpc/Switch.Toggle" WITH HEADERS ADD "Accept" "application/json" ADD "User-Agent" "Coreflux-Broker REST_API route (https://www.coreflux.org)" ADD "Content-Type" "application/json" WITH BODY "{value.json.body}" ADD EVENT CoverOpen WITH SOURCE_TOPIC "energy/shelly/cover/open" WITH DESTINATION_TOPIC "energy/shelly/cover/open/response" WITH METHOD "POST" WITH ENDPOINT "/rpc/Cover.Open" WITH HEADERS ADD "Accept" "application/json" ADD "User-Agent" "Coreflux-Broker REST_API route (https://www.coreflux.org)" ADD "Content-Type" "application/json" WITH BODY "{value.json.body}" ADD EVENT CoverClose WITH SOURCE_TOPIC "energy/shelly/cover/close" WITH DESTINATION_TOPIC "energy/shelly/cover/close/response" WITH METHOD "POST" WITH ENDPOINT "/rpc/Cover.Close" WITH HEADERS ADD "Accept" "application/json" ADD "User-Agent" "Coreflux-Broker REST_API route (https://www.coreflux.org)" ADD "Content-Type" "application/json" WITH BODY "{value.json.body}" ADD EVENT CoverStop WITH SOURCE_TOPIC "energy/shelly/cover/stop" WITH DESTINATION_TOPIC "energy/shelly/cover/stop/response" WITH METHOD "POST" WITH ENDPOINT "/rpc/Cover.Stop" WITH HEADERS ADD "Accept" "application/json" ADD "User-Agent" "Coreflux-Broker REST_API route (https://www.coreflux.org)" ADD "Content-Type" "application/json" WITH BODY "{value.json.body}" ADD EVENT CoverGoToPosition WITH SOURCE_TOPIC "energy/shelly/cover/position/set" WITH DESTINATION_TOPIC "energy/shelly/cover/position/set/response" WITH METHOD "POST" WITH ENDPOINT "/rpc/Cover.GoToPosition" WITH HEADERS ADD "Accept" "application/json" ADD "User-Agent" "Coreflux-Broker REST_API route (https://www.coreflux.org)" ADD "Content-Type" "application/json" WITH BODY "{value.json.body}" ADD EVENT LightSet WITH SOURCE_TOPIC "energy/shelly/light/set" WITH DESTINATION_TOPIC "energy/shelly/light/set/response" WITH METHOD "POST" WITH ENDPOINT "/rpc/Light.Set" WITH HEADERS ADD "Accept" "application/json" ADD "User-Agent" "Coreflux-Broker REST_API route (https://www.coreflux.org)" ADD "Content-Type" "application/json" WITH BODY "{value.json.body}" ADD EVENT WebhookCreate WITH SOURCE_TOPIC "energy/shelly/webhook/create" WITH DESTINATION_TOPIC "energy/shelly/webhook/create/response" WITH METHOD "POST" WITH ENDPOINT "/rpc/Webhook.Create" WITH HEADERS ADD "Accept" "application/json" ADD "User-Agent" "Coreflux-Broker REST_API route (https://www.coreflux.org)" ADD "Content-Type" "application/json" WITH BODY "{value.json.body}" ADD EVENT WebhookDelete WITH SOURCE_TOPIC "energy/shelly/webhook/delete" WITH DESTINATION_TOPIC "energy/shelly/webhook/delete/response" WITH METHOD "POST" WITH ENDPOINT "/rpc/Webhook.Delete" WITH HEADERS ADD "Accept" "application/json" ADD "User-Agent" "Coreflux-Broker REST_API route (https://www.coreflux.org)" ADD "Content-Type" "application/json" WITH BODY "{value.json.body}" ADD EVENT Reboot WITH SOURCE_TOPIC "energy/shelly/reboot" WITH DESTINATION_TOPIC "energy/shelly/reboot/response" WITH METHOD "POST" WITH ENDPOINT "/rpc/Shelly.Reboot" WITH HEADERS ADD "Accept" "application/json" ADD "User-Agent" "Coreflux-Broker REST_API route (https://www.coreflux.org)" ADD "Content-Type" "application/json" WITH BODY "{value.json.body}" ADD EVENT Rpc WITH SOURCE_TOPIC "energy/shelly/rpc" WITH DESTINATION_TOPIC "energy/shelly/rpc/response" WITH METHOD "POST" WITH ENDPOINT "/rpc" WITH HEADERS ADD "Accept" "application/json" ADD "User-Agent" "Coreflux-Broker REST_API route (https://www.coreflux.org)" ADD "Content-Type" "application/json" WITH BODY "{value.json.body}" ``` Deploy it with `-addConnector` (do not include `ADD METADATA`). ## Usage example Call `GetShellyIdentity` by publishing a JSON payload to its request topic, and read the reply on the response topic: ```bash theme={"theme":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} mosquitto_sub -t 'energy/shelly/identity/get/response' & mosquitto_pub -t 'energy/shelly/identity/get' -m '{}' ``` ## Related * [REST API Connectors](/latest/lot-language/routes/rest-api-routes) * [REST API Apps catalog](/latest/lot-language/routes/rest-api-apps) # Shopify Source: https://docs.coreflux.org/latest/lot-language/routes/rest-api-apps/shopify LoT REST API connector for the Shopify GraphQL Admin API: adjust inventory from sensor counts, publish pick lists to MQTT, look up SKUs and list locations Technical Preview ## Overview A client-mode connector for the [Shopify GraphQL Admin API](https://shopify.dev/docs/api/admin-graphql) (version `2026-10`). Use it to connect the shop floor to your online store: when a counter or scale reports finished goods, adjust the sellable stock in Shopify, and push the list of open, unfulfilled orders to MQTT so a packing line or pick-to-light system can work through it. Every GraphQL event is a `POST` to `/admin/api/2026-10/graphql.json`. The event only decides the MQTT topics; the GraphQL document and its variables travel in the payload as `body`. Canonical bodies for each event are below. | Event | Request | Notes | | - | - | - | | GetShop | `POST /admin/api/2026-10/graphql.json` (`shop`) | Smoke and store check | | GetLocations | `POST …/graphql.json` (`locations`) | Location ids (`gid://shopify/Location/…`) for inventory changes | | GetInventoryItems | `POST …/graphql.json` (`inventoryItems`) | Find an inventory item by SKU, with `available` and `on_hand` per location | | AdjustInventoryQuantities | `POST …/graphql.json` (`inventoryAdjustQuantities`) | Add or remove stock by a delta, with an idempotency key | | GetOrders | `POST …/graphql.json` (`orders`) | Open, unfulfilled orders with line items (pick list), paged by cursor | | ClientCredentialsToken | `POST /admin/oauth/access_token` | Exchanges client id and secret for a 24-hour access token | Topics use `finance/shopify//`; replies land on `/response`. Set `BASE_ADDRESS` to your store's permanent `https://.myshopify.com` domain (not a custom storefront domain). See the base guide: [REST API Connectors](/latest/lot-language/routes/rest-api-routes). > [!NOTE] > > **Auth:** every GraphQL call sends the `X-Shopify-Access-Token` header with `access_token` from the payload. Get a token in one of these ways: > > * **Dev Dashboard app, client credentials.** Create an app in the [Dev Dashboard](https://shopify.dev/docs/apps/build/dev-dashboard/get-api-access-tokens) in the same Shopify organisation as the store, install it with the scopes below, and publish its client id and secret to ClientCredentialsToken. The token lasts 24 hours (`expires_in` 86399). This only works for stores in your own organisation; other stores answer `shop_not_permitted`. > * **Existing admin-created custom app.** Its `shpat_…` Admin API access token does not expire. New admin-created custom apps can no longer be made. > * **Public or custom-distribution app.** Run the authorization code grant outside the broker and pass the token. > > Scopes: `read_orders`, `read_inventory`, `write_inventory`, `read_locations` and `read_products`. Keep credentials in Hub secrets (suggested names `SHOPIFY_STORE`, `SHOPIFY_CLIENT_ID`, `SHOPIFY_CLIENT_SECRET`, `SHOPIFY_ACCESS_TOKEN`). Never commit them into the `.lot`. > [!WARNING] > > **Idempotency is required.** From API version `2026-04`, `inventoryAdjustQuantities` must carry the `@idempotent(key: …)` directive. Use one key per physical count event (for example `line3-counter7-000123`) and send the same key when you retry, so a retried message never counts stock twice. Shopify remembers keys for 24 hours. Each change must also set `changeFromQuantity`: the quantity you expect before the change (Shopify rejects the change with `CHANGE_FROM_QUANTITY_STALE` if it moved), or `null` to skip that check. > [!TIP] > > **GraphQL errors come back with HTTP 200.** Check the top-level `errors` array (syntax, `THROTTLED`, access denied) and the mutation's `userErrors` array (validation) in every reply. Only a bad token (**401**), a wrong store (**404**) and a few store states (**402**, **423**) change the HTTP status. ## Basic Syntax ```lot wrap theme={"theme":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} DEFINE CONNECTOR ShopifyAdminGraphqlApi WITH TYPE REST_API ADD REST_API_CONFIG WITH BASE_ADDRESS "https://YOUR_STORE.myshopify.com" WITH ENABLE_CLIENT "true" WITH USE_SSL "true" WITH REQUEST_TIMEOUT '60' WITH DEFAULT_CONTENT_TYPE "application/json" # Auth: X-Shopify-Access-Token {value.json.access_token}; replace YOUR_STORE. Never commit tokens. ADD EVENT GetShop WITH SOURCE_TOPIC "finance/shopify/shop/get" WITH DESTINATION_TOPIC "finance/shopify/shop/get/response" WITH METHOD "POST" WITH ENDPOINT "/admin/api/2026-10/graphql.json" WITH HEADERS ADD "Accept" "application/json" ADD "Content-Type" "application/json" ADD "X-Shopify-Access-Token" "{value.json.access_token}" WITH BODY "{value.json.body}" ADD EVENT GetLocations WITH SOURCE_TOPIC "finance/shopify/location/list" WITH DESTINATION_TOPIC "finance/shopify/location/list/response" WITH METHOD "POST" WITH ENDPOINT "/admin/api/2026-10/graphql.json" WITH HEADERS ADD "Accept" "application/json" ADD "Content-Type" "application/json" ADD "X-Shopify-Access-Token" "{value.json.access_token}" WITH BODY "{value.json.body}" ADD EVENT GetInventoryItems WITH SOURCE_TOPIC "finance/shopify/inventory-item/list" WITH DESTINATION_TOPIC "finance/shopify/inventory-item/list/response" WITH METHOD "POST" WITH ENDPOINT "/admin/api/2026-10/graphql.json" WITH HEADERS ADD "Accept" "application/json" ADD "Content-Type" "application/json" ADD "X-Shopify-Access-Token" "{value.json.access_token}" WITH BODY "{value.json.body}" ADD EVENT AdjustInventoryQuantities WITH SOURCE_TOPIC "finance/shopify/inventory/adjust" WITH DESTINATION_TOPIC "finance/shopify/inventory/adjust/response" WITH METHOD "POST" WITH ENDPOINT "/admin/api/2026-10/graphql.json" WITH HEADERS ADD "Accept" "application/json" ADD "Content-Type" "application/json" ADD "X-Shopify-Access-Token" "{value.json.access_token}" WITH BODY "{value.json.body}" ADD EVENT GetOrders WITH SOURCE_TOPIC "finance/shopify/order/list" WITH DESTINATION_TOPIC "finance/shopify/order/list/response" WITH METHOD "POST" WITH ENDPOINT "/admin/api/2026-10/graphql.json" WITH HEADERS ADD "Accept" "application/json" ADD "Content-Type" "application/json" ADD "X-Shopify-Access-Token" "{value.json.access_token}" WITH BODY "{value.json.body}" ADD EVENT ClientCredentialsToken WITH SOURCE_TOPIC "finance/shopify/oauth/token" WITH DESTINATION_TOPIC "finance/shopify/oauth/token/response" WITH METHOD "POST" WITH ENDPOINT "/admin/oauth/access_token" WITH HEADERS ADD "Accept" "application/json" ADD "Content-Type" "application/x-www-form-urlencoded" WITH BODY "grant_type=client_credentials&client_id={value.json.client_id}&client_secret={value.json.client_secret}" ``` ## MQTT examples ### Get a token (client credentials) Publish to `finance/shopify/oauth/token`: ```json theme={"theme":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} { "client_id": "", "client_secret": "" } ``` Reply on `finance/shopify/oauth/token/response`: `{"access_token":"…","scope":"read_inventory,write_inventory,…","expires_in":86399}`. ### Find the inventory item for a SKU Publish to `finance/shopify/inventory-item/list`: ```json theme={"theme":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} { "access_token": "", "body": { "query": "query InventoryBySku($q: String!) { inventoryItems(first: 5, query: $q) { nodes { id sku tracked inventoryLevels(first: 10) { nodes { location { id name } quantities(names: [\"available\", \"on_hand\"]) { name quantity } } } } } }", "variables": { "q": "sku:CF-SEAL-01" } } } ``` The reply gives the inventory item id, and for each location its id and the `available` and `on_hand` quantities. Use these ids in the adjustment. `GetLocations` lists all locations (`{"query":"query Locations($first: Int!) { locations(first: $first) { nodes { id name isActive } } }","variables":{"first":20}}`). ### Adjust stock from a sensor count Publish to `finance/shopify/inventory/adjust` (here: 5 finished units counted on line 3): ```json theme={"theme":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} { "access_token": "", "body": { "query": "mutation AdjustFromSensor($input: InventoryAdjustQuantitiesInput!, $idempotencyKey: String!) { inventoryAdjustQuantities(input: $input) @idempotent(key: $idempotencyKey) { inventoryAdjustmentGroup { id createdAt reason referenceDocumentUri changes { name delta quantityAfterChange } } userErrors { field message code } } }", "variables": { "idempotencyKey": "line3-counter7-000123", "input": { "reason": "correction", "name": "available", "referenceDocumentUri": "gid://coreflux/SensorCount/line3-counter7-000123", "changes": [ { "delta": 5, "inventoryItemId": "gid://shopify/InventoryItem/30322695", "locationId": "gid://shopify/Location/124656943", "changeFromQuantity": null } ] } } } } ``` `delta` is the change, not the new total. Use a negative delta to remove stock. `reason` must be one of Shopify's reasons, for example `correction`, `cycle_count_available` or `received`. `referenceDocumentUri` shows up in the store's inventory history, so point it at your own system (`gid://coreflux/…`). A good reply has `"userErrors": []` and `quantityAfterChange` for each change. ### Publish the pick list Publish to `finance/shopify/order/list`: ```json theme={"theme":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} { "access_token": "", "body": { "query": "query PickList($q: String, $after: String) { orders(first: 25, after: $after, query: $q, sortKey: CREATED_AT) { nodes { id name createdAt displayFulfillmentStatus lineItems(first: 50) { nodes { sku name quantity } } } pageInfo { hasNextPage endCursor } } }", "variables": { "q": "status:open fulfillment_status:unfulfilled", "after": null } } } ``` The reply lists open, unfulfilled orders with SKU, name and quantity for each line. When `pageInfo.hasNextPage` is `true`, publish again with `"after"` set to `pageInfo.endCursor`. `read_orders` only reaches recent orders; older ones need the `read_all_orders` scope. ### Smoke test Publish to `finance/shopify/shop/get`: ```json theme={"theme":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} { "access_token": "", "body": { "query": "query Shop { shop { name myshopifyDomain currencyCode } }", "variables": {} } } ``` ## Keeping the token Client-credentials tokens last 24 hours. These two actions fetch a new one from Hub secrets twice a day and store it: ```lot theme={"theme":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} DEFINE ACTION ShopifyTokenFromSecrets ON EVERY 12 HOURS DO SET "cid" WITH GET SECRET "SHOPIFY_CLIENT_ID" SET "csecret" WITH GET SECRET "SHOPIFY_CLIENT_SECRET" PUBLISH TOPIC "finance/shopify/oauth/token" WITH { "client_id": {cid}, "client_secret": {csecret} } DEFINE ACTION ShopifyKeepToken ON TOPIC "finance/shopify/oauth/token/response" DO SET "access" WITH GET JSON "access_token" IN PAYLOAD AS STRING IF {access} != "null" THEN KEEP SECRET "SHOPIFY_ACCESS_TOKEN" WITH {access} ``` Restrict who can read the `finance/shopify/#` topics with a rule, because these payloads carry tokens and client credentials. ## Webhooks (companion, not packaged) Shopify can `POST` an `orders/create` [webhook](https://shopify.dev/docs/apps/build/webhooks/subscribe/https) to an HTTPS URL. Each delivery carries `X-Shopify-Hmac-SHA256`, a base64 HMAC-SHA256 of the raw body made with the app's client secret, and should be rejected if it does not match. A REST API connector in server mode cannot check that signature, so the receiver is not part of this template. Options: put a verifying proxy in front of a separate `ENABLE_SERVER` connector, use Shopify's Amazon EventBridge or Google Cloud Pub/Sub delivery, or poll GetOrders on a timer (for example with `created_at:>…` in `q`). ## Notes * API versions are released every quarter and each is supported for at least 12 months. `2026-10` is accessible until October 2027. Change the version in each `ENDPOINT` when you upgrade; a retired version is answered by the oldest supported one. * Rate limits use a calculated query cost in a leaky bucket per app and store; the size and restore rate depend on the store's plan. Each reply carries `extensions.cost`. On `THROTTLED`, wait and retry with the same idempotency key. * Common errors: **401** `[API] Invalid API key or access token` (bad or expired token), `ACCESS_DENIED` in `errors` (missing scope), `userErrors` such as `CHANGE_FROM_QUANTITY_STALE` or an invalid reason, **404** (wrong store domain). * Not covered: the REST Admin API (legacy), bulk operations, product and fulfillment mutations, `inventorySetQuantities` (sets an absolute quantity). ## Related * [REST API Connectors](/latest/lot-language/routes/rest-api-routes) * [Shopify: inventoryAdjustQuantities](https://shopify.dev/docs/api/admin-graphql/latest/mutations/inventoryAdjustQuantities) * [Shopify: orders query](https://shopify.dev/docs/api/admin-graphql/latest/queries/orders) * [Shopify: implementing idempotency](https://shopify.dev/docs/apps/build/apis/graphql-admin/implementing-idempotency) * [Shopify: client credentials tokens](https://shopify.dev/docs/apps/build/dev-dashboard/get-api-access-tokens) * [Shopify API versioning](https://shopify.dev/docs/api/usage/versioning) # Siemens Industrial Edge Management Source: https://docs.coreflux.org/latest/lot-language/routes/rest-api-apps/siemens-industrial-edge-management LoT REST API connector for Siemens Industrial Edge Management: list Edge Devices, browse the app catalog, deploy and manage apps, and follow install jobs Technical Preview ## Overview One client-mode connector, **SiemensIEM**, for the Portal Public API of a self-hosted Siemens Industrial Edge Management (IEM-OS, or IEM Pro on Kubernetes). IEM is the central place where you onboard Industrial Edge Devices (IEDs) and deploy Industrial Edge apps to them. With this connector the broker can publish your edge fleet into the Unified Namespace (device status, statistics, installed apps) and drive app rollouts from MQTT: deploy an app, restart or stop it, push a new app configuration, then follow the batch until every device job is `COMPLETED`. The connector talks to the IEM management API only. Data on the devices themselves (Databus MQTT, connectors, IIH) is not part of this connector. | Event | Topic | Request | Notes | | - | - | - | - | | Login | `mes/siemens-iem/token/create` | `POST /portal/api/v1/login/direct` | Username/password (or technical user) -> data.access\_token, valid 24 h | | VerifyToken | `mes/siemens-iem/token/verify` | `GET /portal/api/v1/auth-tokens/verify` | Returns the auth details of the token | | Logout | `mes/siemens-iem/token/delete` | `GET /portal/api/v1/logout` | Invalidates the token (IEM-OS only; not on IEM on Kubernetes) | | ListDevices | `mes/siemens-iem/device/list` | `GET /portal/api/v1/devices` | query: page=0\&size=50 (send "" for defaults) | | GetDevice | `mes/siemens-iem/device/get` | `GET /portal/api/v1/devices/{id}` | data.deviceStatus, deviceVersion, deviceTypeId, ... | | GetDeviceStatistics | `mes/siemens-iem/device-statistics/get` | `GET /portal/api/v1/devices/{id}/statistics` | Last statistics the device sent (app counts, network, resources) | | ListInstalledApps | `mes/siemens-iem/device-app/list` | `GET /portal/api/v1/devices/installed-apps` | query: deviceid=\ (send "" for all devices) | | ListCatalogApps | `mes/siemens-iem/application/list` | `GET /portal/api/v1/applications` | Catalog applications with their 32-char application ids | | GetApplication | `mes/siemens-iem/application/get` | `GET /portal/api/v1/applications/{id}` | Application details and versions | | ListAppConfigs | `mes/siemens-iem/application-config/list` | `GET /portal/api/v1/applications/{id}/configs` | Config ids, template ids and version ids for updateAppConfig | | DeployApplication | `mes/siemens-iem/application/deploy` | `POST /portal/api/v1/batches` | Install a catalog app on devices; form = multipart infoMap \{"devices":\[...]} -> data = batch id | | ApplicationCommand | `mes/siemens-iem/application/command` | `POST /portal/api/v1/batches` | operation: installApplication/updateApplication/uninstallApplication/startApplication/restartApplication/stopApplication/updateAppConfig/deleteAppConfig; query: versionId=...\&schedule=... or "" | | GetBatch | `mes/siemens-iem/batch/get` | `GET /portal/api/v1/batches/{batchId}` | data = READY / PROCESSED | | ListBatchJobs | `mes/siemens-iem/batch-job/list` | `GET /portal/api/v1/batches/{id}/jobs` | Per-device install jobs of a batch | | GetInstallJob | `mes/siemens-iem/job/get` | `GET /portal/api/v1/installjobs/{id}` | data = COMPLETED / PENDING / QUEUED / FAILED / EXECUTING / DELETED / DELETING | Every reply is published on `/response`. ## Authentication The Portal API uses a token in the `Authorization` header, sent **as is** (no `Bearer` prefix): 1. Publish to `mes/siemens-iem/token/create` with `{"body": {"username": "...", "password": "..."}}`. Use an IEM user or a technical user with the rights you need. The reply has `data.access_token`, valid for 24 hours. 2. Put that token in `"authorization"` on every other event. Keep the user name and password in Hub secrets (for example `SIEMENS_IEM_USERNAME` and `SIEMENS_IEM_PASSWORD`) and read them with `GET SECRET`. Never paste them into the connector. A missing or expired token returns `401`. > [!WARNING] > > Many IEM installations use a self-signed certificate (the Siemens examples call `curl --insecure`). The broker host must trust the IEM certificate authority, or every call fails during the TLS handshake. ## Connector Replace `YOUR_IEM_HOST` with your IEM address (no path). Every endpoint starts with `/portal/api/v1`. ```lot theme={"theme":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} DEFINE CONNECTOR SiemensIEM WITH TYPE REST_API ADD REST_API_CONFIG WITH BASE_ADDRESS "https://YOUR_IEM_HOST" WITH ENABLE_CLIENT "true" WITH USE_SSL "true" WITH REQUEST_TIMEOUT '30' WITH DEFAULT_CONTENT_TYPE "application/json" ADD EVENT Login WITH SOURCE_TOPIC "mes/siemens-iem/token/create" WITH DESTINATION_TOPIC "mes/siemens-iem/token/create/response" WITH METHOD "POST" WITH ENDPOINT "/portal/api/v1/login/direct" WITH HEADERS ADD "Accept" "application/json" ADD "Content-Type" "application/json" WITH BODY "{value.json.body}" ADD EVENT VerifyToken WITH SOURCE_TOPIC "mes/siemens-iem/token/verify" WITH DESTINATION_TOPIC "mes/siemens-iem/token/verify/response" WITH METHOD "GET" WITH ENDPOINT "/portal/api/v1/auth-tokens/verify" WITH HEADERS ADD "Accept" "application/json" ADD "Authorization" "{value.json.authorization}" ADD EVENT Logout WITH SOURCE_TOPIC "mes/siemens-iem/token/delete" WITH DESTINATION_TOPIC "mes/siemens-iem/token/delete/response" WITH METHOD "GET" WITH ENDPOINT "/portal/api/v1/logout" WITH HEADERS ADD "Accept" "application/json" ADD "Authorization" "{value.json.authorization}" ADD EVENT ListDevices WITH SOURCE_TOPIC "mes/siemens-iem/device/list" WITH DESTINATION_TOPIC "mes/siemens-iem/device/list/response" WITH METHOD "GET" WITH ENDPOINT "/portal/api/v1/devices?{value.json.query}" WITH HEADERS ADD "Accept" "application/json" ADD "Authorization" "{value.json.authorization}" ADD EVENT GetDevice WITH SOURCE_TOPIC "mes/siemens-iem/device/get" WITH DESTINATION_TOPIC "mes/siemens-iem/device/get/response" WITH METHOD "GET" WITH ENDPOINT "/portal/api/v1/devices/{value.json.device_id}" WITH HEADERS ADD "Accept" "application/json" ADD "Authorization" "{value.json.authorization}" ADD EVENT GetDeviceStatistics WITH SOURCE_TOPIC "mes/siemens-iem/device-statistics/get" WITH DESTINATION_TOPIC "mes/siemens-iem/device-statistics/get/response" WITH METHOD "GET" WITH ENDPOINT "/portal/api/v1/devices/{value.json.device_id}/statistics" WITH HEADERS ADD "Accept" "application/json" ADD "Authorization" "{value.json.authorization}" ADD EVENT ListInstalledApps WITH SOURCE_TOPIC "mes/siemens-iem/device-app/list" WITH DESTINATION_TOPIC "mes/siemens-iem/device-app/list/response" WITH METHOD "GET" WITH ENDPOINT "/portal/api/v1/devices/installed-apps?{value.json.query}" WITH HEADERS ADD "Accept" "application/json" ADD "Authorization" "{value.json.authorization}" ADD EVENT ListCatalogApps WITH SOURCE_TOPIC "mes/siemens-iem/application/list" WITH DESTINATION_TOPIC "mes/siemens-iem/application/list/response" WITH METHOD "GET" WITH ENDPOINT "/portal/api/v1/applications" WITH HEADERS ADD "Accept" "application/json" ADD "Authorization" "{value.json.authorization}" ADD EVENT GetApplication WITH SOURCE_TOPIC "mes/siemens-iem/application/get" WITH DESTINATION_TOPIC "mes/siemens-iem/application/get/response" WITH METHOD "GET" WITH ENDPOINT "/portal/api/v1/applications/{value.json.app_id}" WITH HEADERS ADD "Accept" "application/json" ADD "Authorization" "{value.json.authorization}" ADD EVENT ListAppConfigs WITH SOURCE_TOPIC "mes/siemens-iem/application-config/list" WITH DESTINATION_TOPIC "mes/siemens-iem/application-config/list/response" WITH METHOD "GET" WITH ENDPOINT "/portal/api/v1/applications/{value.json.app_id}/configs" WITH HEADERS ADD "Accept" "application/json" ADD "Authorization" "{value.json.authorization}" ADD EVENT DeployApplication WITH SOURCE_TOPIC "mes/siemens-iem/application/deploy" WITH DESTINATION_TOPIC "mes/siemens-iem/application/deploy/response" WITH METHOD "POST" WITH ENDPOINT "/portal/api/v1/batches?appid={value.json.app_id}&operation=installApplication" WITH HEADERS ADD "Accept" "application/json" ADD "Content-Type" "multipart/form-data; boundary=CorefluxIemBoundary" ADD "Authorization" "{value.json.authorization}" WITH BODY "{value.json.form}" ADD EVENT ApplicationCommand WITH SOURCE_TOPIC "mes/siemens-iem/application/command" WITH DESTINATION_TOPIC "mes/siemens-iem/application/command/response" WITH METHOD "POST" WITH ENDPOINT "/portal/api/v1/batches?appid={value.json.app_id}&operation={value.json.operation}&{value.json.query}" WITH HEADERS ADD "Accept" "application/json" ADD "Content-Type" "multipart/form-data; boundary=CorefluxIemBoundary" ADD "Authorization" "{value.json.authorization}" WITH BODY "{value.json.form}" ADD EVENT GetBatch WITH SOURCE_TOPIC "mes/siemens-iem/batch/get" WITH DESTINATION_TOPIC "mes/siemens-iem/batch/get/response" WITH METHOD "GET" WITH ENDPOINT "/portal/api/v1/batches/{value.json.batch_id}" WITH HEADERS ADD "Accept" "application/json" ADD "Authorization" "{value.json.authorization}" ADD EVENT ListBatchJobs WITH SOURCE_TOPIC "mes/siemens-iem/batch-job/list" WITH DESTINATION_TOPIC "mes/siemens-iem/batch-job/list/response" WITH METHOD "GET" WITH ENDPOINT "/portal/api/v1/batches/{value.json.batch_id}/jobs" WITH HEADERS ADD "Accept" "application/json" ADD "Authorization" "{value.json.authorization}" ADD EVENT GetInstallJob WITH SOURCE_TOPIC "mes/siemens-iem/job/get" WITH DESTINATION_TOPIC "mes/siemens-iem/job/get/response" WITH METHOD "GET" WITH ENDPOINT "/portal/api/v1/installjobs/{value.json.job_id}" WITH HEADERS ADD "Accept" "application/json" ADD "Authorization" "{value.json.authorization}" ``` ## Deploying an app `POST /portal/api/v1/batches` takes `multipart/form-data` with one field, `infoMap` (JSON). The connector sends the fixed boundary `CorefluxIemBoundary`, so `form` carries the complete multipart body with CRLF line breaks: ```json theme={"theme":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} { "authorization": "", "app_id": "46d2ac22e41a4b65af55a434c887578e", "form": "--CorefluxIemBoundary\r\nContent-Disposition: form-data; name=\"infoMap\"\r\n\r\n{\"devices\":[\"06cdac6c607e447dad16eef9f767a56e\"]}\r\n--CorefluxIemBoundary--\r\n" } ``` Publish that to `mes/siemens-iem/application/deploy`. The reply is `{"data": ""}`. Then: 1. `mes/siemens-iem/batch/get` with `batch_id`: `READY` (batch accepted) or `PROCESSED` (device jobs created). 2. `mes/siemens-iem/batch-job/list` with `batch_id`: one job per device. 3. `mes/siemens-iem/job/get` with `job_id`: `QUEUED`, `EXECUTING`, `COMPLETED`, `FAILED`, ... `infoMap` can also carry `delayTime` (1 to 60 minutes between devices) and `configs` (for example `[{"configId": "...", "templateId": "..."}]`, ids from `ListAppConfigs`). For other operations use `mes/siemens-iem/application/command` with `operation` set to `updateApplication`, `uninstallApplication`, `startApplication`, `restartApplication`, `stopApplication`, `updateAppConfig` or `deleteAppConfig`, and `query` set to `""` or `versionId=` (developer apps) and `schedule=`. > [!WARNING] > > Deploy and command events change what runs on production edge devices. Restrict who may publish to `mes/siemens-iem/application/#` with a broker rule. ## Example actions Log in twice a day, keep the token in broker state, and poll the device list every five minutes: ```lot theme={"theme":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} DEFINE ACTION SiemensIemLogin ON EVERY 12 HOURS DO SET "user" WITH GET SECRET "SIEMENS_IEM_USERNAME" SET "pass" WITH GET SECRET "SIEMENS_IEM_PASSWORD" PUBLISH TOPIC "mes/siemens-iem/token/create" WITH { "body": { "username": {user}, "password": {pass} } } DEFINE ACTION SiemensIemKeepToken ON TOPIC "mes/siemens-iem/token/create/response" DO SET "token" WITH GET JSON "data.access_token" IN PAYLOAD AS STRING KEEP TOPIC "state/siemens-iem/token" WITH {token} DEFINE ACTION SiemensIemPollDevices ON EVERY 5 MINUTES DO SET "token" WITH GET TOPIC "state/siemens-iem/token" AS STRING PUBLISH TOPIC "mes/siemens-iem/device/list" WITH { "authorization": {token}, "query": "page=0&size=100" } ``` ## Notes * `ListDevices` takes `query` such as `page=0&size=50`. `ListInstalledApps` takes `deviceid=`, or `""` for all devices. * Device status is `data.deviceStatus` from `GetDevice`. There is no separate device status endpoint. `GetDeviceStatistics` returns the last statistics the device sent, keyed by timestamp. * `Logout` works on IEM-OS only (not on IEM on Kubernetes). * Not included: application import from file and file-based app configs (multipart file uploads), device creation and deletion, device reboot or shutdown jobs, log downloads, projects and companies, and the separate IEM Admin API. ## References * [IEM Portal API 1.9.0 (OpenAPI)](https://developer.siemens.com/industrial-edge/api/iem/api-portal/v1.9.0/api-portal-1-9-0.html) * [Portal Public API reference](https://docs.industrial-operations-x.siemens.cloud/r/en-us/portal-public-api) * [Industrial Edge APIs and SDKs](https://docs.industrial-operations-x.siemens.cloud/p/industrial-edge-apis-sdks) * [REST API connectors](/latest/lot-language/routes/rest-api-routes) # Siemens Insights Hub Source: https://docs.coreflux.org/latest/lot-language/routes/rest-api-apps/siemens-insights-hub LoT REST API connector for Siemens Insights Hub (MindSphere): write and read IoT time series, create and list events, and list assets from MQTT Technical Preview ## Overview A client-mode connector for a **curated cloud-IoT** slice of [Siemens Insights Hub](https://developer.siemens.com/insights-hub/docs/apis/index.html) (formerly MindSphere) covering these connector events: | Catalog connector event | Entity / path | Notes | | - | - | - | | PUT iottimeseries/v3/timeseries/\{asset}/\{aspect} | `PUT /iottimeseries/v3/timeseries/{assetId}/{aspectName}` | Ingest near-real-time time series | | GET time series | `GET /iottimeseries/v3/timeseries/{assetId}/{aspectName}` | Query with `from` / `to` / `limit` | | POST eventmanagement/v3/events | `POST /eventmanagement/v3/events` | Create standard or custom events | | GET assetmanagement/v3/assets | `GET /assetmanagement/v3/assets` | List assets (`page` / `size`) | | (list) | `GET /eventmanagement/v3/events` | Optional paged event list | Default `BASE_ADDRESS` is `https://gateway.eu1.mindsphere.io/api` (no trailing slash; substitute `{region}`). Topics use `cloud-iot/siemens-insights-hub//`. See the base guide: [REST API Connectors](/latest/lot-language/routes/rest-api-routes). > [!NOTE] > > **Auth header:** Pass the full `Authorization` value via MQTT JSON field `authorization` as `Bearer {access_token}` (OAuth 2.0 technical user client credentials, or application / Token Management credentials). Prefer Hub secrets (`SIEMENS_INSIGHTS_HUB_AUTH`). Never commit real credentials. See [Accessing Industrial IoT APIs from self-hosted applications](https://documentation.mindsphere.io/MindSphere/howto/howto-selfhosted-api-access.html). ## Basic Syntax ```lot wrap theme={"theme":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} # Coreflux REST_API template — Siemens Insights Hub curated slice # Auth: Authorization via {value.json.authorization} (Bearer …) # BASE_ADDRESS: https://gateway.eu1.mindsphere.io/api → substitute {region} DEFINE CONNECTOR SiemensInsightsHubRestApi WITH TYPE REST_API ADD REST_API_CONFIG WITH BASE_ADDRESS "https://gateway.eu1.mindsphere.io/api" WITH ENABLE_CLIENT "true" WITH USE_SSL "true" WITH REQUEST_TIMEOUT '60' WITH DEFAULT_CONTENT_TYPE "application/json" ADD EVENT PutTimeseries WITH SOURCE_TOPIC "cloud-iot/siemens-insights-hub/timeseries/put" WITH DESTINATION_TOPIC "cloud-iot/siemens-insights-hub/timeseries/put/response" WITH METHOD "PUT" WITH ENDPOINT "/iottimeseries/v3/timeseries/{value.json.assetId}/{value.json.aspectName}" WITH HEADERS ADD "Accept" "application/json" ADD "Content-Type" "application/json" ADD "Authorization" "{value.json.authorization}" WITH BODY "{payload}" ADD EVENT GetTimeseries WITH SOURCE_TOPIC "cloud-iot/siemens-insights-hub/timeseries/get" WITH DESTINATION_TOPIC "cloud-iot/siemens-insights-hub/timeseries/get/response" WITH METHOD "GET" WITH ENDPOINT "/iottimeseries/v3/timeseries/{value.json.assetId}/{value.json.aspectName}?from={value.json.from}&to={value.json.to}&limit={value.json.limit}" WITH HEADERS ADD "Accept" "application/json" ADD "Authorization" "{value.json.authorization}" ADD EVENT CreateEvent WITH SOURCE_TOPIC "cloud-iot/siemens-insights-hub/event/create" WITH DESTINATION_TOPIC "cloud-iot/siemens-insights-hub/event/create/response" WITH METHOD "POST" WITH ENDPOINT "/eventmanagement/v3/events" WITH HEADERS ADD "Accept" "application/json" ADD "Content-Type" "application/json" ADD "Authorization" "{value.json.authorization}" WITH BODY "{payload}" ADD EVENT ListAssets WITH SOURCE_TOPIC "cloud-iot/siemens-insights-hub/asset/list" WITH DESTINATION_TOPIC "cloud-iot/siemens-insights-hub/asset/list/response" WITH METHOD "GET" WITH ENDPOINT "/assetmanagement/v3/assets?page={value.json.page}&size={value.json.size}" WITH HEADERS ADD "Accept" "application/json" ADD "Authorization" "{value.json.authorization}" ADD EVENT ListEvents WITH SOURCE_TOPIC "cloud-iot/siemens-insights-hub/event/list" WITH DESTINATION_TOPIC "cloud-iot/siemens-insights-hub/event/list/response" WITH METHOD "GET" WITH ENDPOINT "/eventmanagement/v3/events?page={value.json.page}&size={value.json.size}" WITH HEADERS ADD "Accept" "application/json" ADD "Authorization" "{value.json.authorization}" ``` ## MQTT topics | Event | Source topic | Response topic | | - | - | - | | PutTimeseries | `cloud-iot/siemens-insights-hub/timeseries/put` | `…/timeseries/put/response` | | GetTimeseries | `cloud-iot/siemens-insights-hub/timeseries/get` | `…/timeseries/get/response` | | CreateEvent | `cloud-iot/siemens-insights-hub/event/create` | `…/event/create/response` | | ListAssets | `cloud-iot/siemens-insights-hub/asset/list` | `…/asset/list/response` | | ListEvents | `cloud-iot/siemens-insights-hub/event/list` | `…/event/list/response` | ## References * [Insights Hub services index](https://developer.siemens.com/insights-hub/docs/apis/index.html) * [IoT Time Series overview](https://developer.siemens.com/insights-hub/docs/apis/iot-iottimeseries/api-iottimeseries-overview.html) * [Event Management overview](https://developer.siemens.com/insights-hub/docs/apis/advanced-eventmanagement/api-eventmanagement-overview.html) * [Self-hosted API access (OAuth)](https://documentation.mindsphere.io/MindSphere/howto/howto-selfhosted-api-access.html) # Siemens Opcenter Execution Source: https://docs.coreflux.org/latest/lot-language/routes/rest-api-apps/siemens-opcenter-execution Siemens Opcenter Execution MES: OData reads of work orders and runtime data, archive queries, plant config, OpenAPI discovery and public command invocation Technical Preview ## Overview One client-mode connector, **SiemensOpcenterExecution**, for the service layer of Siemens Opcenter Execution. Opcenter Execution Discrete, Process and other solutions built on Opcenter Execution Foundation expose the same surface: OData v4 reads on `/sit-svc`, a read-only archive on `/sit-arch`, and public commands that the solution's workers execute. Use the connector to bring MES context into your Unified Namespace: publish active work orders next to the machine data that serves them, count open operations per line, or look up a finished order in the archive. In the other direction, a machine event on the broker can call an Opcenter command such as completing an operation or recording a consumption. Every Opcenter solution is different. Entity sets and command names come from the Apps installed in your solution, and Opcenter generates an OpenAPI 3.0.3 description of them when the solution is built. Start with the discovery events, then use the exact names they return. | Event | Request | Notes | | - | - | - | | GetPlantConfig | `GET /sit-svc/config?{query}` | Configured plants; `plantid=` filters one | | GetHostMonitoring | `GET /sit-svc/monitoring?{query}` | Host and worker status, the data behind the Solution Studio monitoring screen | | GetSolutionOpenApi | `GET /sit-svc/{solution_path}/$openapi` | OpenAPI of every public command in the solution | | GetAppServiceDocument | `GET /sit-svc/{app_path}/odata/` | OData service document: the App's entity sets | | GetAppMetadata | `GET /sit-svc/{app_path}/odata/$metadata` | CSDL XML: entities, keys, properties, navigation | | GetAppOpenApi | `GET /sit-svc/{app_path}/odata/$openapi` | OpenAPI of the App's commands and entities | | QueryEntities | `GET /sit-svc/{app_path}/odata/{entity}?{query}` | Any entity set with `$filter`, `$select`, `$expand`, `$orderby`, `$top`, `$skip` | | GetEntity | `GET /sit-svc/{app_path}/odata/{entity}({key})?{query}` | One record by key | | CountEntities | `GET /sit-svc/{app_path}/odata/{entity}/$count?{query}` | Plain-text count | | ListWorkOrders | `GET /sit-svc/{app_path}/odata/WorkOrder?{query}` | Shortcut for the `WorkOrder` entity set; check the name in `$metadata` | | QueryArchive | `GET /sit-arch/{app_path}/odata/{entity}?{query}` | Archived runtime data, read-only | | InvokeCommand | `POST /sit-svc/{app_path}/odata/{command}` | Any public command; body as described in `$openapi` | Topics are `mes/siemens-opcenter-execution//`; every reply goes to `/response`. `app_path` is `Application/` for the default plant, or `/Application/` in a multi-plant installation, for example `Application/AppU4DM` or `Plant1/Application/AppU4DM`. `solution_path` is `Application` or `/Application`. ## Authentication Every event sends `Authorization: {authorization}`, so pass the whole header value, normally `"Bearer "`. Opcenter issues tokens through its authentication service (`/sit-auth`), backed by Siemens UMC or the identity provider your installation is configured with. How a service account gets a token depends on that setup, so ask your Opcenter administrator for a service account and the token procedure, and give the account only the roles the commands you plan to call need. Keep the value in a broker secret (suggested `OPCENTER_AUTHORIZATION`) and map it in with `GET SECRET`. Tokens expire, so refresh the secret on the schedule your identity provider uses. A missing or expired token returns `401`; a role without permission returns `403`. ## Basic Syntax Opcenter runs on your own servers or private cloud, so set `BASE_ADDRESS` to the host that serves `/sit-svc` (no trailing slash). ```lot theme={"theme":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} DEFINE CONNECTOR SiemensOpcenterExecution WITH TYPE REST_API ADD REST_API_CONFIG WITH BASE_ADDRESS "https://YOUR_OPCENTER_HOST" WITH ENABLE_CLIENT "true" WITH USE_SSL "true" WITH REQUEST_TIMEOUT '60' WITH DEFAULT_CONTENT_TYPE "application/json" ADD EVENT QueryEntities WITH SOURCE_TOPIC "mes/siemens-opcenter-execution/entity/query" WITH DESTINATION_TOPIC "mes/siemens-opcenter-execution/entity/query/response" WITH METHOD "GET" WITH ENDPOINT "/sit-svc/{value.json.app_path}/odata/{value.json.entity}?{value.json.query}" WITH HEADERS ADD "Accept" "application/json" ADD "Authorization" "{value.json.authorization}" ADD EVENT InvokeCommand WITH SOURCE_TOPIC "mes/siemens-opcenter-execution/command/invoke" WITH DESTINATION_TOPIC "mes/siemens-opcenter-execution/command/invoke/response" WITH METHOD "POST" WITH ENDPOINT "/sit-svc/{value.json.app_path}/odata/{value.json.command}" WITH HEADERS ADD "Accept" "application/json" ADD "Content-Type" "application/json" ADD "Authorization" "{value.json.authorization}" WITH BODY "{value.json.body}" ``` The full template, with all 12 events, is `SiemensOpcenterExecutionRestApi.lot` in the REST API app. ## MQTT examples Request bodies go under `body`, so control fields like `authorization`, `app_path`, `entity` and `query` are not sent to Opcenter. **Discover what an App offers**: publish to `mes/siemens-opcenter-execution/app-openapi/get`: ```json theme={"theme":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} { "authorization": "Bearer ", "app_path": "Application/AppU4DM" } ``` The reply lists the App's commands with their request schemas. `app-metadata/get` returns the entity sets and keys. **Read the first 20 work orders**: publish to `mes/siemens-opcenter-execution/work-order/list`. The OData query must be URL-encoded: ```json theme={"theme":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} { "authorization": "Bearer ", "app_path": "Application/AppU4DM", "query": "$top=20" } ``` Use `entity/query` with `"entity": ""` for any other entity set, and send `"query": ""` when you do not need options. **Get one record**: publish to `mes/siemens-opcenter-execution/entity/get` with `"entity"` and `"key"`. The key is the OData key literal as it appears in the URL: a Guid as is, a string in single quotes such as `"'WO-0001'"`. **Call a public command**: publish to `mes/siemens-opcenter-execution/command/invoke`: ```json theme={"theme":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} { "authorization": "Bearer ", "app_path": "Application/", "command": "", "body": { "...": "fields exactly as the command's request schema in $openapi" } } ``` The connector sends `body` unchanged, so copy its shape from the command's schema in `GetAppOpenApi` or `GetSolutionOpenApi`. ## Helper actions Keep the token in a broker secret, poll work orders every minute, and turn a line event into an Opcenter command: ```lot theme={"theme":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} DEFINE ACTION OpcenterPollActiveWorkOrders ON EVERY 1 MINUTE DO SET "auth" WITH GET SECRET "OPCENTER_AUTHORIZATION" PUBLISH TOPIC "mes/siemens-opcenter-execution/work-order/list" WITH { "authorization": {auth}, "app_path": "Application/AppU4DM", "query": "$top=50" } DEFINE ACTION OpcenterInvokeFromLine ON TOPIC "porto/line1/opcenter/command" DO SET "auth" WITH GET SECRET "OPCENTER_AUTHORIZATION" SET "cmd" WITH GET JSON "command" IN PAYLOAD AS STRING SET "args" WITH GET JSON "body" IN PAYLOAD PUBLISH TOPIC "mes/siemens-opcenter-execution/command/invoke" WITH { "authorization": {auth}, "app_path": "Application/AppU4DM", "command": {cmd}, "body": {args} } ``` Restrict who can publish to `mes/siemens-opcenter-execution/#` with a rule: its payloads carry the token, and `InvokeCommand` changes production records. ## Notes * Errors come back on the response topic: `401` for a missing or expired token, `403` when the account's role does not allow the call, `404` for an unknown App, entity set or command, `400` for a malformed OData query or command body, and `500` when a command handler fails. * Commands are executed by Opcenter workers. Some return only after the transaction commits; check the result by reading the affected entity afterwards. * `/sit-arch` is the archive database and is read-only. Use it for history and audit, and `/sit-svc` for live production data. * In a multi-plant installation each plant has its own data. Put the plant id in `app_path` and use `GetPlantConfig` with `plantid=` to check it. * Names differ between Opcenter Execution Discrete, Process and custom solutions. `ListWorkOrders` assumes a `WorkOrder` entity set; if your App names it differently, use `QueryEntities`. ## Related * [REST API Connectors](/latest/lot-language/routes/rest-api-routes) * [REST API app templates](/latest/lot-language/routes/rest-api-apps/index) * [Ignition](/latest/lot-language/routes/rest-api-apps/ignition) * [SAP Digital Manufacturing](/latest/lot-language/routes/rest-api-apps/sap-digital-manufacturing) # Siemens Teamcenter Source: https://docs.coreflux.org/latest/lot-language/routes/rest-api-apps/siemens-teamcenter LoT REST API connector for Siemens Teamcenter: read item revisions, BOMs and released drawings, run saved queries and raise problem reports from quality events Technical Preview ## Overview [Siemens Teamcenter](https://plm.sw.siemens.com/en-US/teamcenter/) is the PLM system that owns parts, revisions, BOMs, drawings and change processes for many plants. This connector lets shop-floor logic on MQTT read the released revision and BOM of a part, find its released drawing for a work instruction screen, and raise a problem report from a quality event, without custom code. **SiemensTeamcenterApi** (`SiemensTeamcenterRestApi.lot`) is one client connector on `https://YOUR_TC_SERVER` with 18 events: 1 login, 14 reads and 3 writes. No event deletes, revises or checks out anything. > [!NOTE] > > Teamcenter exposes every SOA service operation as JSON over HTTP: `POST /tc/JsonRestServices/---/` with a body of `{"header": {"state": {}, "policy": {}}, "body": {...}}`. Siemens does not publish an OpenAPI file for Teamcenter. Your installation can generate its own API description (Active Workspace `npm run genSoaApi`). The events here follow public samples: the [Teamcenter REST API Postman collection](https://github.com/Krusty84/Teamcenter_REST_API), [TCSwiftBridge](https://github.com/Krusty84/TCSwiftBridge) and the Mendix [Teamcenter connector docs](https://docs.mendix.com/appstore/industry/teamcenter-connector/services-not-included/). To add another operation, copy an event and change the operation path. ## Server host Teamcenter is installed per customer, or runs as Teamcenter X (SaaS). Replace `YOUR_TC_SERVER` with your web tier host, with no trailing slash. Add the port when the web tier does not use 443 (for example `:7001` or `:8080`). Behind the Active Workspace gateway, the same operations are also served under `/tc/RestServices/` and need an `X-XSRF-TOKEN` header. This template uses `/tc/JsonRestServices/` on the web tier. ## Authentication > [!WARNING] > > Teamcenter sessions act as their user. Keep the user and password in Hub secrets, map them in with `GET SECRET`, and never publish the session cookie on a topic other clients can read. 1. Publish the credentials on `mes/siemens-teamcenter/session/login`. The web tier answers with a `JSESSIONID` cookie (`Set-Cookie`). 2. Send `"cookie": "JSESSIONID="` in every other payload. One session holds one Teamcenter server process, so reuse it instead of logging in per call. ```json theme={"theme":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} { "request": { "header": { "state": {}, "policy": {} }, "body": { "credentials": { "user": "", "password": "", "role": "", "descrimator": "coreflux", "locale": "", "group": "" } } } } ``` > [!WARNING] > > `JSESSIONID` only comes back in the `Set-Cookie` header, not in the response body. It is not yet verified whether the broker's REST client keeps cookies per connector or passes `Set-Cookie` on. Until that is confirmed, get the session outside the broker and keep the cookie on `internal/siemens-teamcenter/cookie` (the example actions read it there). Sites that use Teamcenter SSO or Teamcenter X log in through Siemens SSO. They need a session from that login flow, which this template does not cover. ## Payload convention * `request`: the full SOA envelope, sent as the body unchanged. `header.policy` picks which properties come back (`types[].properties[].name`). * `cookie`: the session cookie, `JSESSIONID=`. * Objects are referenced as `{"uid": "...", "type": "ItemRevision"}`. Some operations take plain UID strings. ## Events | Event | Topic | Operation | Kind | What it does | | - | - | - | - | - | | `Login` | `mes/siemens-teamcenter/session/login` | `Core-2011-06-Session/login` | Read | Log in with Teamcenter user / password (Core-2011-06-Session/login). The web tier answers with Set-Cookie JSESSIONID; send it as cookie on every other event | | `GetSessionInfo` | `mes/siemens-teamcenter/session-info/get` | `Core-2007-01-Session/getTCSessionInfo` | Read | Session check: current user, group, role, server version (Core-2007-01-Session/getTCSessionInfo) | | `GetFavorites` | `mes/siemens-teamcenter/favorites/get` | `Core-2008-03-Session/getFavorites` | Read | Favorites of the session user | | `GetPreferences` | `mes/siemens-teamcenter/preferences/get` | `Administration-2012-09-PreferenceManagement/getPreferences` | Read | Read Teamcenter preferences by name (for example the default revision rule) | | `GetItemFromId` | `mes/siemens-teamcenter/item/get` | `Core-2007-01-DataManagement/getItemFromId` | Read | Item and item revision (UIDs and properties) by item\_id and revision id; first call of the BOM and drawing flows | | `GetProperties` | `mes/siemens-teamcenter/properties/get` | `Core-2006-03-DataManagement/getProperties` | Read | Properties of objects by UID: release\_status\_list / date\_released to check a revision is released, IMAN\_specification / IMAN\_Rendering for its drawing datasets | | `ExpandGrmRelationsForPrimary` | `mes/siemens-teamcenter/relations-primary/expand` | `Core-2007-09-DataManagement/expandGRMRelationsForPrimary` | Read | Secondary objects of an item revision by relation type (IMAN\_specification, IMAN\_Rendering, ...): find the drawing / PDF datasets for work instruction display | | `GetSavedQueries` | `mes/siemens-teamcenter/saved-queries/list` | `Query-2006-03-SavedQuery/getSavedQueries` | Read | All saved queries (names and UIDs) | | `DescribeSavedQueries` | `mes/siemens-teamcenter/saved-queries/describe` | `Query-2006-03-SavedQuery/describeSavedQueries` | Read | Fields (entries) of a saved query | | `PerformSearch` | `mes/siemens-teamcenter/search/perform` | `Query-2012-10-Finder/performSearch` | Read | Finder search: run a saved query (Awp0SavedQuerySearchProvider) or the inbox (Awp0InboxProvider) with a property policy | | `GetRevisionRules` | `mes/siemens-teamcenter/revision-rules/list` | `Cad-2007-01-StructureManagement/getRevisionRules` | Read | Revision rules (Latest Working, Any Status; Working, Precise, ...) | | `CreateBomWindow` | `mes/siemens-teamcenter/bom-window/create` | `Cad-2007-01-StructureManagement/createBOMWindows` | Read | Open a BOM window on an item (revision rule config); returns bomWindow and the top bomLine. Session object only, saves nothing | | `ExpandBomOneLevel` | `mes/siemens-teamcenter/bom/expand-one-level` | `Cad-2007-01-StructureManagement/expandPSOneLevel` | Read | First-level children of a BOM line (excludeFilter None2) | | `ExpandBomAllLevels` | `mes/siemens-teamcenter/bom/expand-all-levels` | `Cad-2007-01-StructureManagement/expandPSAllLevels` | Read | Full multi-level BOM below a BOM line, with item revision properties from the policy | | `CloseBomWindow` | `mes/siemens-teamcenter/bom-window/close` | `Cad-2007-01-StructureManagement/closeBOMWindows` | Read | Close BOM windows opened by CreateBomWindow (frees server memory) | | `CreateProblemReport` | `mes/siemens-teamcenter/problem-report/create` | `Core-2006-03-DataManagement/createItems` | Write | create an item of a change type such as ProblemReport (createItems; type name per your Change Management template) in a folder | | `CreateRelations` | `mes/siemens-teamcenter/relations/create` | `Core-2006-03-DataManagement/createRelations` | Write | relate two objects (for example the affected item revision to the problem report; relation type per your template) | | `StartWorkflow` | `mes/siemens-teamcenter/workflow/start` | `Workflow-2008-06-Workflow/createInstance` | Write | start a workflow process on target objects (for example submit the new problem report for review) | Replies arrive on `/response`. ## Flows ### Item revision and BOM 1. `GetItemFromId` with the item ID returns the item and revision UIDs. 2. `CreateBomWindow` with the item UID returns a BOM window and its top BOM line. 3. `ExpandBomOneLevel` or `ExpandBomAllLevels` with the top BOM line returns the children. The property policy in `header.policy` sets which item revision fields come back. 4. `CloseBomWindow` frees the window on the server. ```json theme={"theme":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} { "cookie": "JSESSIONID=", "request": { "header": { "state": {}, "policy": {} }, "body": { "infos": [ { "itemId": "PRT-000123", "revIds": [ "A" ] } ], "nRev": 1, "pref": {} } } } ``` ### Released drawing for work instruction display 1. `GetItemFromId` returns the revision UID. 2. `GetProperties` on the revision with `release_status_list`, `date_released`, `IMAN_specification` and `IMAN_Rendering` tells you whether the revision is released and which datasets hold its drawing. You can also use `ExpandGrmRelationsForPrimary` with `relationTypeName` `IMAN_specification`. 3. Show the dataset in Active Workspace, or download the file through FMS. File download (`getFileReadTickets` and FMS) is not part of this template. ```json theme={"theme":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} { "cookie": "JSESSIONID=", "request": { "header": { "state": {}, "policy": {} }, "body": { "objects": [ { "uid": "", "type": "ItemRevision" } ], "attributes": [ "object_name", "item_revision_id", "release_status_list", "date_released", "IMAN_specification", "IMAN_Rendering" ] } } } ``` ### Problem report from a quality event 1. `CreateProblemReport` creates an item of your problem report type (`ProblemReport` in the example) in a folder. 2. `CreateRelations` links the affected revision to the problem report revision. 3. `StartWorkflow` starts your change process template on it. The type name, relation type and workflow template depend on your Change Management setup. Confirm them with your Teamcenter administrator. ```json theme={"theme":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} { "cookie": "JSESSIONID=", "request": { "header": { "state": {}, "policy": {} }, "body": { "properties": [ { "clientId": "coreflux-qe-1", "itemId": "", "name": "Porosity on PRT-000123 rev A", "type": "ProblemReport", "revId": "", "uom": "", "description": "Quality event QE-1001 from line 1 press 01", "extendedAttributes": [] } ], "container": { "uid": "", "className": "Folder", "type": "Fnd0HomeFolder" }, "relationType": "contents" } } } ``` ## Example actions `docs-helper-actions.lot` holds example actions. They log in every 25 minutes with Hub secrets, look up an item or a revision's drawing datasets when a station asks, and create a problem report from a quality event. ```lot theme={"theme":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} DEFINE ACTION TcLogin ON EVERY 25 MINUTES DO SET "user" WITH GET SECRET "TC_USER" SET "password" WITH GET SECRET "TC_PASSWORD" PUBLISH TOPIC "mes/siemens-teamcenter/session/login" WITH { "request": { "header": { "state": {}, "policy": {} }, "body": { "credentials": { "user": {user}, "password": {password}, "role": "", "descrimator": "coreflux", "locale": "", "group": "" } } } } DEFINE ACTION TcReleasedDrawingLookup ON TOPIC "porto/line1/station03/work-instruction/revision-uid" DO SET "ck" WITH GET TOPIC "internal/siemens-teamcenter/cookie" AS STRING SET "rev" WITH PAYLOAD AS STRING PUBLISH TOPIC "mes/siemens-teamcenter/properties/get" WITH { "cookie": {ck}, "request": { "header": { "state": {}, "policy": {} }, "body": { "objects": [ { "uid": {rev}, "type": "ItemRevision" } ], "attributes": [ "release_status_list", "date_released", "IMAN_specification", "IMAN_Rendering" ] } } } DEFINE ACTION TcItemLookup ON TOPIC "porto/line1/station03/work-instruction/item-id" DO SET "ck" WITH GET TOPIC "internal/siemens-teamcenter/cookie" AS STRING SET "item" WITH PAYLOAD AS STRING PUBLISH TOPIC "mes/siemens-teamcenter/item/get" WITH { "cookie": {ck}, "request": { "header": { "state": {}, "policy": {} }, "body": { "infos": [ { "itemId": {item}, "revIds": [] } ], "nRev": 1, "pref": {} } } } DEFINE ACTION TcQualityEventToProblemReport ON TOPIC "porto/line1/press01/quality-event" DO SET "ck" WITH GET TOPIC "internal/siemens-teamcenter/cookie" AS STRING SET "title" WITH GET JSON "title" IN PAYLOAD AS STRING SET "detail" WITH GET JSON "detail" IN PAYLOAD AS STRING SET "folder" WITH GET TOPIC "internal/siemens-teamcenter/problem-report-folder-uid" AS STRING PUBLISH TOPIC "mes/siemens-teamcenter/problem-report/create" WITH { "cookie": {ck}, "request": { "header": { "state": {}, "policy": {} }, "body": { "properties": [ { "clientId": "coreflux-qe", "itemId": "", "name": {title}, "type": "ProblemReport", "revId": "", "uom": "", "description": {detail}, "extendedAttributes": [] } ], "container": { "uid": {folder}, "className": "Folder", "type": "Fnd0HomeFolder" }, "relationType": "contents" } } } ``` ## Not included * Delete, revise and checkout operations (left out on purpose). * File download and upload through FMS (binary content). * BOM edits (`addOrUpdateChildrenToParentLine`, `saveBOMWindows`). * Teamcenter SSO / Teamcenter X login services and the AWC gateway `/tc/RestServices/` variant. ## Connector file ```lot theme={"theme":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} # Coreflux REST_API template — Siemens Teamcenter (beta): read item revisions, properties, release status and BOMs # (BOM window + expand one / all levels), find a revision's released drawing datasets for work instruction display, # run saved queries / Finder searches, and from a quality event create a problem report, relate the affected # revision and start its workflow, over Teamcenter's JSON SOA REST services (/tc/JsonRestServices/). # Docs: https://docs.coreflux.org/latest/lot-language/routes/rest-api-apps/siemens-teamcenter # # Host: Teamcenter is installed per customer (or Teamcenter X SaaS). Replace YOUR_TC_SERVER with the web tier host # (add :port, for example :7001 or :8080, when it does not share 443). Operations live under # /tc/JsonRestServices/---/; every call is POST with a JSON envelope # {"header": {"state": {}, "policy": {}}, "body": {...}}. # Auth: session. Publish user / password on Login; the web tier returns Set-Cookie JSESSIONID. Send it back as # "cookie": "JSESSIONID=" in every other payload. SSO-only sites (Teamcenter SSO / Teamcenter X) need a session # obtained outside this route; see the docs. Keep TC user and password in Hub secrets. # Payloads: "request" is the full SOA envelope sent unchanged as the body; "cookie" is the session cookie. # GetItemFromId {"cookie": "JSESSIONID=...", "request": {"header": {"state": {}, "policy": {}}, "body": {"infos": [{"itemId": "PRT-000123", "revIds": ["A"]}], "nRev": 1, "pref": {}}}} # DEFINE CONNECTOR SiemensTeamcenterApi WITH TYPE REST_API ADD REST_API_CONFIG WITH BASE_ADDRESS "https://YOUR_TC_SERVER" WITH ENABLE_CLIENT "true" WITH USE_SSL "true" WITH REQUEST_TIMEOUT '60' WITH DEFAULT_CONTENT_TYPE "application/json" # Log in with Teamcenter user / password (Core-2011-06-Session/login). The web tier answers with Set-Cookie JSESSIONID; send it as cookie on every other event ADD EVENT Login WITH SOURCE_TOPIC "mes/siemens-teamcenter/session/login" WITH DESTINATION_TOPIC "mes/siemens-teamcenter/session/login/response" WITH METHOD "POST" WITH ENDPOINT "/tc/JsonRestServices/Core-2011-06-Session/login" WITH HEADERS ADD "Accept" "application/json" ADD "Content-Type" "application/json" WITH BODY "{value.json.request}" # Session check: current user, group, role, server version (Core-2007-01-Session/getTCSessionInfo) ADD EVENT GetSessionInfo WITH SOURCE_TOPIC "mes/siemens-teamcenter/session-info/get" WITH DESTINATION_TOPIC "mes/siemens-teamcenter/session-info/get/response" WITH METHOD "POST" WITH ENDPOINT "/tc/JsonRestServices/Core-2007-01-Session/getTCSessionInfo" WITH HEADERS ADD "Accept" "application/json" ADD "Content-Type" "application/json" ADD "Cookie" "{value.json.cookie}" WITH BODY "{value.json.request}" # Favorites of the session user ADD EVENT GetFavorites WITH SOURCE_TOPIC "mes/siemens-teamcenter/favorites/get" WITH DESTINATION_TOPIC "mes/siemens-teamcenter/favorites/get/response" WITH METHOD "POST" WITH ENDPOINT "/tc/JsonRestServices/Core-2008-03-Session/getFavorites" WITH HEADERS ADD "Accept" "application/json" ADD "Content-Type" "application/json" ADD "Cookie" "{value.json.cookie}" WITH BODY "{value.json.request}" # Read Teamcenter preferences by name (for example the default revision rule) ADD EVENT GetPreferences WITH SOURCE_TOPIC "mes/siemens-teamcenter/preferences/get" WITH DESTINATION_TOPIC "mes/siemens-teamcenter/preferences/get/response" WITH METHOD "POST" WITH ENDPOINT "/tc/JsonRestServices/Administration-2012-09-PreferenceManagement/getPreferences" WITH HEADERS ADD "Accept" "application/json" ADD "Content-Type" "application/json" ADD "Cookie" "{value.json.cookie}" WITH BODY "{value.json.request}" # Item and item revision (UIDs and properties) by item_id and revision id; first call of the BOM and drawing flows ADD EVENT GetItemFromId WITH SOURCE_TOPIC "mes/siemens-teamcenter/item/get" WITH DESTINATION_TOPIC "mes/siemens-teamcenter/item/get/response" WITH METHOD "POST" WITH ENDPOINT "/tc/JsonRestServices/Core-2007-01-DataManagement/getItemFromId" WITH HEADERS ADD "Accept" "application/json" ADD "Content-Type" "application/json" ADD "Cookie" "{value.json.cookie}" WITH BODY "{value.json.request}" # Properties of objects by UID: release_status_list / date_released to check a revision is released, IMAN_specification / IMAN_Rendering for its drawing datasets ADD EVENT GetProperties WITH SOURCE_TOPIC "mes/siemens-teamcenter/properties/get" WITH DESTINATION_TOPIC "mes/siemens-teamcenter/properties/get/response" WITH METHOD "POST" WITH ENDPOINT "/tc/JsonRestServices/Core-2006-03-DataManagement/getProperties" WITH HEADERS ADD "Accept" "application/json" ADD "Content-Type" "application/json" ADD "Cookie" "{value.json.cookie}" WITH BODY "{value.json.request}" # Secondary objects of an item revision by relation type (IMAN_specification, IMAN_Rendering, ...): find the drawing / PDF datasets for work instruction display ADD EVENT ExpandGrmRelationsForPrimary WITH SOURCE_TOPIC "mes/siemens-teamcenter/relations-primary/expand" WITH DESTINATION_TOPIC "mes/siemens-teamcenter/relations-primary/expand/response" WITH METHOD "POST" WITH ENDPOINT "/tc/JsonRestServices/Core-2007-09-DataManagement/expandGRMRelationsForPrimary" WITH HEADERS ADD "Accept" "application/json" ADD "Content-Type" "application/json" ADD "Cookie" "{value.json.cookie}" WITH BODY "{value.json.request}" # All saved queries (names and UIDs) ADD EVENT GetSavedQueries WITH SOURCE_TOPIC "mes/siemens-teamcenter/saved-queries/list" WITH DESTINATION_TOPIC "mes/siemens-teamcenter/saved-queries/list/response" WITH METHOD "POST" WITH ENDPOINT "/tc/JsonRestServices/Query-2006-03-SavedQuery/getSavedQueries" WITH HEADERS ADD "Accept" "application/json" ADD "Content-Type" "application/json" ADD "Cookie" "{value.json.cookie}" WITH BODY "{value.json.request}" # Fields (entries) of a saved query ADD EVENT DescribeSavedQueries WITH SOURCE_TOPIC "mes/siemens-teamcenter/saved-queries/describe" WITH DESTINATION_TOPIC "mes/siemens-teamcenter/saved-queries/describe/response" WITH METHOD "POST" WITH ENDPOINT "/tc/JsonRestServices/Query-2006-03-SavedQuery/describeSavedQueries" WITH HEADERS ADD "Accept" "application/json" ADD "Content-Type" "application/json" ADD "Cookie" "{value.json.cookie}" WITH BODY "{value.json.request}" # Finder search: run a saved query (Awp0SavedQuerySearchProvider) or the inbox (Awp0InboxProvider) with a property policy ADD EVENT PerformSearch WITH SOURCE_TOPIC "mes/siemens-teamcenter/search/perform" WITH DESTINATION_TOPIC "mes/siemens-teamcenter/search/perform/response" WITH METHOD "POST" WITH ENDPOINT "/tc/JsonRestServices/Query-2012-10-Finder/performSearch" WITH HEADERS ADD "Accept" "application/json" ADD "Content-Type" "application/json" ADD "Cookie" "{value.json.cookie}" WITH BODY "{value.json.request}" # Revision rules (Latest Working, Any Status; Working, Precise, ...) ADD EVENT GetRevisionRules WITH SOURCE_TOPIC "mes/siemens-teamcenter/revision-rules/list" WITH DESTINATION_TOPIC "mes/siemens-teamcenter/revision-rules/list/response" WITH METHOD "POST" WITH ENDPOINT "/tc/JsonRestServices/Cad-2007-01-StructureManagement/getRevisionRules" WITH HEADERS ADD "Accept" "application/json" ADD "Content-Type" "application/json" ADD "Cookie" "{value.json.cookie}" WITH BODY "{value.json.request}" # Open a BOM window on an item (revision rule config); returns bomWindow and the top bomLine. Session object only, saves nothing ADD EVENT CreateBomWindow WITH SOURCE_TOPIC "mes/siemens-teamcenter/bom-window/create" WITH DESTINATION_TOPIC "mes/siemens-teamcenter/bom-window/create/response" WITH METHOD "POST" WITH ENDPOINT "/tc/JsonRestServices/Cad-2007-01-StructureManagement/createBOMWindows" WITH HEADERS ADD "Accept" "application/json" ADD "Content-Type" "application/json" ADD "Cookie" "{value.json.cookie}" WITH BODY "{value.json.request}" # First-level children of a BOM line (excludeFilter None2) ADD EVENT ExpandBomOneLevel WITH SOURCE_TOPIC "mes/siemens-teamcenter/bom/expand-one-level" WITH DESTINATION_TOPIC "mes/siemens-teamcenter/bom/expand-one-level/response" WITH METHOD "POST" WITH ENDPOINT "/tc/JsonRestServices/Cad-2007-01-StructureManagement/expandPSOneLevel" WITH HEADERS ADD "Accept" "application/json" ADD "Content-Type" "application/json" ADD "Cookie" "{value.json.cookie}" WITH BODY "{value.json.request}" # Full multi-level BOM below a BOM line, with item revision properties from the policy ADD EVENT ExpandBomAllLevels WITH SOURCE_TOPIC "mes/siemens-teamcenter/bom/expand-all-levels" WITH DESTINATION_TOPIC "mes/siemens-teamcenter/bom/expand-all-levels/response" WITH METHOD "POST" WITH ENDPOINT "/tc/JsonRestServices/Cad-2007-01-StructureManagement/expandPSAllLevels" WITH HEADERS ADD "Accept" "application/json" ADD "Content-Type" "application/json" ADD "Cookie" "{value.json.cookie}" WITH BODY "{value.json.request}" # Close BOM windows opened by CreateBomWindow (frees server memory) ADD EVENT CloseBomWindow WITH SOURCE_TOPIC "mes/siemens-teamcenter/bom-window/close" WITH DESTINATION_TOPIC "mes/siemens-teamcenter/bom-window/close/response" WITH METHOD "POST" WITH ENDPOINT "/tc/JsonRestServices/Cad-2007-01-StructureManagement/closeBOMWindows" WITH HEADERS ADD "Accept" "application/json" ADD "Content-Type" "application/json" ADD "Cookie" "{value.json.cookie}" WITH BODY "{value.json.request}" # WRITE: create an item of a change type such as ProblemReport (createItems; type name per your Change Management template) in a folder ADD EVENT CreateProblemReport WITH SOURCE_TOPIC "mes/siemens-teamcenter/problem-report/create" WITH DESTINATION_TOPIC "mes/siemens-teamcenter/problem-report/create/response" WITH METHOD "POST" WITH ENDPOINT "/tc/JsonRestServices/Core-2006-03-DataManagement/createItems" WITH HEADERS ADD "Accept" "application/json" ADD "Content-Type" "application/json" ADD "Cookie" "{value.json.cookie}" WITH BODY "{value.json.request}" # WRITE: relate two objects (for example the affected item revision to the problem report; relation type per your template) ADD EVENT CreateRelations WITH SOURCE_TOPIC "mes/siemens-teamcenter/relations/create" WITH DESTINATION_TOPIC "mes/siemens-teamcenter/relations/create/response" WITH METHOD "POST" WITH ENDPOINT "/tc/JsonRestServices/Core-2006-03-DataManagement/createRelations" WITH HEADERS ADD "Accept" "application/json" ADD "Content-Type" "application/json" ADD "Cookie" "{value.json.cookie}" WITH BODY "{value.json.request}" # WRITE: start a workflow process on target objects (for example submit the new problem report for review) ADD EVENT StartWorkflow WITH SOURCE_TOPIC "mes/siemens-teamcenter/workflow/start" WITH DESTINATION_TOPIC "mes/siemens-teamcenter/workflow/start/response" WITH METHOD "POST" WITH ENDPOINT "/tc/JsonRestServices/Workflow-2008-06-Workflow/createInstance" WITH HEADERS ADD "Accept" "application/json" ADD "Content-Type" "application/json" ADD "Cookie" "{value.json.cookie}" WITH BODY "{value.json.request}" ``` # Siemens WinCC Unified (GraphQL server) Source: https://docs.coreflux.org/latest/lot-language/routes/rest-api-apps/siemens-wincc-unified LoT REST API connector for the WinCC Unified GraphQL server: browse, read and write tags, read logged values, and list or acknowledge alarms from MQTT Technical Preview ## Overview WinCC Unified is the Siemens SCADA / HMI system in TIA Portal. From V19 on, Unified Runtime on a PC includes a **GraphQL server** for reading and writing runtime data: tags, logged (historical) tag values and alarms. Siemens documents it in the online help **WinCC Unified GraphQL (RT Unified)**. The **WinccUnifiedGraphql** connector sends GraphQL queries and mutations over HTTPS. Every event is a `POST /graphql/` with a JSON body `{"query": "...", "variables": {...}}`. The topic picks the preset and the payload carries the GraphQL document, so you control exactly which fields come back. Typical uses: mirror HMI tags onto MQTT, write setpoints from MQTT, pull tag history into a historian or analytics pipeline, and acknowledge alarms from another system. Each event publishes the server's reply on `/response`. | Event | Topic | GraphQL operation | Notes | | - | - | - | - | | Login | `historian/siemens-wincc-unified/session/create` | `mutation login` | UMC user configured for Runtime -> data.login.token (Bearer) + expires; error 101 wrong credentials, 102 UMC error | | ExtendSession | `historian/siemens-wincc-unified/session/update` | `mutation extendSession` | Renew the current session (new expires) | | Logout | `historian/siemens-wincc-unified/session/delete` | `mutation logout` | End this session (allSessions true = every session of the user) | | GetSession | `historian/siemens-wincc-unified/session/get` | `query session` | Current session and user (token check) | | BrowseTags | `historian/siemens-wincc-unified/tag/browse` | `query browse` | Find tags, alarms, logging tags by name filter (wildcards) and object type | | ReadTagValues | `historian/siemens-wincc-unified/tag-value/get` | `query tagValues` | Current values (value, timestamp, quality); directRead true reads from the PLC instead of the cache | | WriteTagValues | `historian/siemens-wincc-unified/tag-value/update` | `mutation writeTagValues` | Write values \{name, value\[, timestamp, quality]}; error 2 unknown tag, 201 cannot convert value | | ReadLoggedTagValues | `historian/siemens-wincc-unified/logged-tag-value/list` | `query loggedTagValues` | Historical values of logging tags (Tag:LoggingTag, or the tag when it has one logging tag); startTime or endTime required, maxNumberOfValues if only one is set | | ListActiveAlarms | `historian/siemens-wincc-unified/active-alarm/list` | `query activeAlarms` | Active alarms; filterString = WHERE part of ChromQueryLanguage (error 301 syntax) | | ListLoggedAlarms | `historian/siemens-wincc-unified/logged-alarm/list` | `query loggedAlarms` | Alarm history from the alarm log | | AcknowledgeAlarms | `historian/siemens-wincc-unified/alarm/acknowledge` | `mutation acknowledgeAlarms` | Acknowledge alarms \{name\[, instanceID]}; instanceID 0 = every instance; 305 not acknowledgeable in this state | | ResetAlarms | `historian/siemens-wincc-unified/alarm/reset` | `mutation resetAlarms` | Reset alarms that need a reset | | ShelveAlarms | `historian/siemens-wincc-unified/alarm/shelve` | `mutation shelveAlarms` | Shelve configured alarms (optional shelveTimeout) | | UnshelveAlarms | `historian/siemens-wincc-unified/alarm/unshelve` | `mutation unshelveAlarms` | Revert shelve | | DisableAlarms | `historian/siemens-wincc-unified/alarm/disable` | `mutation disableAlarms` | Disable configured alarms (no new instances) | | EnableAlarms | `historian/siemens-wincc-unified/alarm/enable` | `mutation enableAlarms` | Enable configured alarms again | | GraphqlRequest | `historian/siemens-wincc-unified/graphql/query` | `any query / mutation` | Any query or mutation of the schema (payload decides); subscriptions are not possible over REST\_API | ## Prerequisites 1. **WinCC Unified Runtime on a PC (V19 or later)** with a project running and the GraphQL server available. Siemens documents it online for V20 and V21. Unified Comfort Panels are not covered by the Siemens GraphQL help. 2. The Runtime **IIS web server** reachable from the broker host on HTTPS 443. The GraphQL server is published at `https:///graphql/`. The GraphQL service listens on port 4000 on the runtime PC, but Siemens does not support direct access to it. Always go through IIS. 3. The broker must **trust the Runtime web server certificate**, either the self-signed certificate or a CA created with WinCC Unified Certificate Manager. The host name or IP in `BASE_ADDRESS` must be in that certificate. Use IPv4 when you connect by host name. 4. A **UMC user configured for Runtime**. Read rights are needed for queries, write rights for `writeTagValues` and the alarm mutations. Objects the user cannot read are left out of responses, and a write without permission aborts the operation. ## Authentication 1. Log in: publish the `login` mutation to `historian/siemens-wincc-unified/session/create` (example below). The response has `data.login.token` and `data.login.expires`. A wrong user or password returns `data.login.error.code` `101` and no token. `102` is a UMC error. 2. Send `"token": ""` with every other event. The connector sends it as `Authorization: Bearer `. 3. A session stays open while there is activity. `ExtendSession` (`session/update`) renews it, and `Logout` (`session/delete`) ends it. `allSessions: true` ends every session of the user. Changing the user's password and logging out with `allSessions: true` also closes them. 4. Without a valid token, the server answers with an `errors` array instead of data. When that happens, log in again. Unlike some APIs, the token is in the **response body**, so a LoT action can read it with `GET JSON "data.login.token"` and keep it internally (see Examples). > [!WARNING] > > Keep the WinCC Unified user and password in Hub secrets and add them with `GET SECRET`. Never put them in the connector file or in retained topics. Restrict who may publish to the write and alarm topics: `writeTagValues` writes to the HMI tags and, through them, to the PLC. ## Configuration * Replace `YOUR_WINCC_UNIFIED_HOST` in `BASE_ADDRESS` with the Runtime PC's host name or IP. That name must match the Runtime web server certificate. Keep `USE_SSL "true"`. * Every event is `POST /graphql/` with `Content-Type: application/json`. The body is `{value.json.body}`, sent unchanged. Use GraphQL **variables** for values. That keeps quotes out of the query string and makes the payload easy to build in LoT. * GraphQL answers HTTP `200` for most errors. Check the `errors` array (syntax, validation, not logged in) and, per item, `error.code`: `0` success, `2` cannot resolve name, `3` argument error, `201` cannot convert value to data type, `202` only leaf elements of a structure tag, `301` alarm filter syntax error, `302` / `303` invalid language, `304` / `305` alarm state does not allow the action. * Tag names follow the WinCC Unified addressing rules, for example `HMI_Tag_1` or a structure element `Motor_1.Speed`. `BrowseTags` lists them with wildcards (`*`, `?`). * `loggedTagValues` takes logging tag names (`Tag:LoggingTag`) or a tag name when that tag has only one logging tag. Set `startTime` or `endTime`. If you set only one of them, also set `maxNumberOfValues`. Timestamps are ISO 8601 UTC, for example `2026-10-05T07:00:00.000Z`. * `activeAlarms` and `loggedAlarms` take an optional `filterString`. This is the WHERE part of a ChromQueryLanguage (SQL-like) filter, for example `Priority >= 10`. Alarm names are full alarm paths, for example `HMI_RT_1::Line1_Temperature:HighHigh`. `instanceID` `0` targets every instance. * `writeTagValues` takes an optional `timestamp` and `quality` (`{"quality": "GOOD_CASCADE"}`) for the whole list, or per tag in `input`. ## Examples Log in (publish to `historian/siemens-wincc-unified/session/create`): ```json theme={"theme":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} { "body": { "query": "mutation Login($username: String!, $password: String!) { login(username: $username, password: $password) { token expires user { name fullName language autoLogoffSec } error { code description } } }", "variables": { "username": "", "password": "" } } } ``` Response on `historian/siemens-wincc-unified/session/create/response`: ```json theme={"theme":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} {"data": {"login": {"token": "", "expires": "2026-10-05T07:30:00.000Z", "user": {"name": "operator", "fullName": "Operator", "language": "en-US", "autoLogoffSec": 1800}, "error": {"code": "0", "description": "Success"}}}} ``` Read live tag values (`historian/siemens-wincc-unified/tag-value/get`): ```json theme={"theme":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} { "token": "", "body": { "query": "query TagValues($names: [String!]!, $directRead: Boolean) { tagValues(names: $names, directRead: $directRead) { name value { value timestamp quality { quality subStatus } } error { code description } } }", "variables": { "names": [ "Line1_Temperature", "Line1_Setpoint" ] } } } ``` ```json theme={"theme":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} {"data": {"tagValues": [{"name": "Line1_Temperature", "value": {"value": 21.4, "timestamp": "2026-10-05T07:00:00.000Z", "quality": {"quality": "GOOD_CASCADE", "subStatus": "NON_SPECIFIC"}}, "error": {"code": "0", "description": "Success"}}]}} ``` Write a setpoint (`historian/siemens-wincc-unified/tag-value/update`): ```json theme={"theme":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} { "token": "", "body": { "query": "mutation WriteTagValues($input: [TagValueInput]!, $timestamp: Timestamp, $quality: QualityInput) { writeTagValues(input: $input, timestamp: $timestamp, quality: $quality) { name error { code description } } }", "variables": { "input": [ { "name": "Line1_Setpoint", "value": 72.5 } ] } } } ``` Browse tags (`historian/siemens-wincc-unified/tag/browse`): ```json theme={"theme":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} { "token": "", "body": { "query": "query Browse($nameFilters: [String], $objectTypeFilters: [ObjectTypesEnum], $language: String) { browse(nameFilters: $nameFilters, objectTypeFilters: $objectTypeFilters, language: $language) { name displayName objectType dataType } }", "variables": { "nameFilters": [ "Line1_*" ], "objectTypeFilters": [ "TAG" ], "language": "en-US" } } } ``` Read the last hour of a logging tag (`historian/siemens-wincc-unified/logged-tag-value/list`): ```json theme={"theme":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} { "token": "", "body": { "query": "query LoggedTagValues($names: [String]!, $startTime: Timestamp, $endTime: Timestamp, $maxNumberOfValues: Int, $sortingMode: LoggedTagValuesSortingModeEnum) { loggedTagValues(names: $names, startTime: $startTime, endTime: $endTime, maxNumberOfValues: $maxNumberOfValues, sortingMode: $sortingMode) { loggingTagName error { code description } values { value { value timestamp quality { quality subStatus } } flags } } }", "variables": { "names": [ "Line1_Temperature:Line1_Temperature_Log" ], "startTime": "2026-10-05T06:00:00.000Z", "endTime": "2026-10-05T07:00:00.000Z", "maxNumberOfValues": 1000, "sortingMode": "TIME_ASC" } } } ``` List active alarms with priority 10 or higher (`historian/siemens-wincc-unified/active-alarm/list`): ```json theme={"theme":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} { "token": "", "body": { "query": "query ActiveAlarms($systemNames: [String], $filterString: String, $filterLanguage: String, $languages: [String]) { activeAlarms(systemNames: $systemNames, filterString: $filterString, filterLanguage: $filterLanguage, languages: $languages) { name instanceID alarmGroupID raiseTime acknowledgmentTime clearTime resetTime modificationTime state priority eventText infoText alarmClassName origin area path value hostName userName } }", "variables": { "filterString": "Priority >= 10", "languages": [ "en-US" ] } } } ``` Acknowledge an alarm (`historian/siemens-wincc-unified/alarm/acknowledge`): ```json theme={"theme":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} { "token": "", "body": { "query": "mutation AcknowledgeAlarms($input: [AlarmIdentifierInput]!) { acknowledgeAlarms(input: $input) { alarmName alarmInstanceID error { code description } } }", "variables": { "input": [ { "name": "HMI_RT_1::Line1_Temperature:HighHigh", "instanceID": 0 } ] } } } ``` Read logged alarms (`historian/siemens-wincc-unified/logged-alarm/list`): ```json theme={"theme":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} { "token": "", "body": { "query": "query LoggedAlarms($systemNames: [String], $filterString: String, $languages: [String], $startTime: Timestamp, $endTime: Timestamp, $maxNumberOfResults: Int) { loggedAlarms(systemNames: $systemNames, filterString: $filterString, languages: $languages, startTime: $startTime, endTime: $endTime, maxNumberOfResults: $maxNumberOfResults) { name instanceID raiseTime acknowledgmentTime clearTime resetTime modificationTime state priority eventText origin area } }", "variables": { "startTime": "2026-10-05T00:00:00.000Z", "endTime": "2026-10-05T07:00:00.000Z", "maxNumberOfResults": 100 } } } ``` Keep the session alive (`historian/siemens-wincc-unified/session/update`): ```json theme={"theme":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} { "token": "", "body": { "query": "mutation ExtendSession { extendSession { token expires error { code description } } }", "variables": {} } } ``` Example actions: log in with Hub secrets, keep the token internally, keep the session alive, poll tags and alarms, write a setpoint and acknowledge alarms from MQTT. Send anything to `internal/wincc-unified/login` once to log in. ```lot theme={"theme":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} # Example actions for the WinccUnifiedGraphql route (docs page). Not part of the route template. # Secrets: WINCC_UNIFIED_USER / WINCC_UNIFIED_PASSWORD = a UMC user configured for Runtime (read rights for queries, # write rights for writeTagValues and the alarm mutations). The login mutation returns the token in the response # BODY (data.login.token), so an action can keep it internally and add it to every request. The session stays # open while there is activity; the keep-alive below renews it. On an "errors" reply, publish to the login topic again. # Tag and alarm names below are examples: use the names from your WinCC Unified project (BrowseTags lists them). DEFINE ACTION WinccUnifiedLogin ON TOPIC "internal/wincc-unified/login" DO SET "user" WITH GET SECRET "WINCC_UNIFIED_USER" SET "password" WITH GET SECRET "WINCC_UNIFIED_PASSWORD" PUBLISH TOPIC "historian/siemens-wincc-unified/session/create" WITH { "body": { "query": "mutation ($u: String!, $p: String!) { login(username: $u, password: $p) { token expires error { code description } } }", "variables": { "u": {user}, "p": {password} } } } DEFINE ACTION WinccUnifiedRememberToken ON TOPIC "historian/siemens-wincc-unified/session/create/response" DO SET "token" WITH GET JSON "data.login.token" IN PAYLOAD AS STRING KEEP TOPIC "internal/wincc-unified/token" WITH {token} DEFINE ACTION WinccUnifiedKeepAlive ON EVERY 5 MINUTES DO SET "token" WITH GET TOPIC "internal/wincc-unified/token" PUBLISH TOPIC "historian/siemens-wincc-unified/session/update" WITH { "token": {token}, "body": { "query": "mutation { extendSession { expires error { code description } } }" } } DEFINE ACTION WinccUnifiedPollLine1 ON EVERY 10 SECONDS DO SET "token" WITH GET TOPIC "internal/wincc-unified/token" PUBLISH TOPIC "historian/siemens-wincc-unified/tag-value/get" WITH { "token": {token}, "body": { "query": "query ($names: [String!]!) { tagValues(names: $names) { name value { value timestamp quality { quality } } error { code description } } }", "variables": { "names": [ "Line1_Temperature", "Line1_Setpoint", "Line1_Running" ] } } } DEFINE ACTION WinccUnifiedWriteSetpoint ON TOPIC "plant/line1/setpoint/set" DO SET "token" WITH GET TOPIC "internal/wincc-unified/token" SET "value" WITH PAYLOAD AS DOUBLE PUBLISH TOPIC "historian/siemens-wincc-unified/tag-value/update" WITH { "token": {token}, "body": { "query": "mutation ($input: [TagValueInput]!) { writeTagValues(input: $input) { name error { code description } } }", "variables": { "input": [ { "name": "Line1_Setpoint", "value": {value} } ] } } } DEFINE ACTION WinccUnifiedPollAlarms ON EVERY 30 SECONDS DO SET "token" WITH GET TOPIC "internal/wincc-unified/token" PUBLISH TOPIC "historian/siemens-wincc-unified/active-alarm/list" WITH { "token": {token}, "body": { "query": "query ($languages: [String]) { activeAlarms(languages: $languages) { name instanceID state priority raiseTime eventText } }", "variables": { "languages": [ "en-US" ] } } } DEFINE ACTION WinccUnifiedAcknowledgeFromMqtt ON TOPIC "plant/line1/alarm/ack" DO SET "token" WITH GET TOPIC "internal/wincc-unified/token" SET "alarm" WITH GET JSON "name" IN PAYLOAD AS STRING PUBLISH TOPIC "historian/siemens-wincc-unified/alarm/acknowledge" WITH { "token": {token}, "body": { "query": "mutation ($input: [AlarmIdentifierInput]!) { acknowledgeAlarms(input: $input) { alarmName alarmInstanceID error { code description } } }", "variables": { "input": [ { "name": {alarm}, "instanceID": 0 } ] } } } ``` ## Not covered * **Subscriptions** (`tagValues`, `activeAlarms`, `reduState`). They need a WebSocket with the `graphql-transport-ws` protocol, which REST\_API connectors do not use. Poll `ReadTagValues` / `ListActiveAlarms` on a timer instead. * `loginSWAC` (UMC SWAC single sign-on with `nonce` and a signed claim) and `identityProviderURL`. You can still send them through `GraphqlRequest`. * Unified Comfort Panels, WinCC Unified Openness / ODK, and the Industrial Edge WinCC Unified apps. * Engineering in TIA Portal (projects, screens, users). This connector only reads and writes runtime data. ## Related * [REST API connectors](/latest/lot-language/routes/rest-api-routes) * [REST API app catalog](/latest/lot-language/routes/rest-api-apps/index) * [WinCC Unified GraphQL (RT Unified), V21](https://docs.tia.siemens.cloud/r/en-us/v21/wincc-unified-graphql-rt-unified) * [WinCC Unified GraphQL requirements (RT Unified), V20](https://docs.tia.siemens.cloud/r/en-us/v20/wincc-unified-graphql-rt-unified/basics-rt-unified/requirements-rt-unified) # 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. > [!WARNING] > > This page is the **documentation MCP** at `https://docs.coreflux.org/mcp` — it searches official docs for your assistant. It does **not** start or control a broker. > > To let an assistant run broker commands and LoT Actions on a live broker, enable the [MCP Server](/latest/mqtt-broker/mcp-server) (`Mcp.Enabled`). To call *external* MCP tools from LoT, use an [MCP connector](/latest/lot-language/routes/mcp-routes) (`TYPE MCP` / `CALL MCP`). > [!TIP] > > **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. **Native** 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. ![Documentation page contextual menu expanded, showing Copy page, View as Markdown, Open in ChatGPT, Open in Claude, Copy MCP Server, Copy MCP install command, Connect to Cursor, and Connect to VS Code](https://mintcdn.com/coreflux/2Qlxw_T8xvkddvBy/images/MCP/action-menu-install-mcp.png?fit=max\&auto=format\&n=2Qlxw_T8xvkddvBy\&q=85\&s=ab62d6ffe924c7ef063007d4fc93c08b) Open the contextual menu from Copy page on any documentation page | 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 | **VS Code** **1. Open the contextual menu** On any Coreflux documentation page, click **Copy page** (top right) to expand the menu. **2. Connect to VS Code** Select **Connect to VS Code**. VS Code opens the MCP server installation flow for **Coreflux Documentation**. **3. Install the server** On the MCP server page, click **Install** (current window) or **Install in Workspace** (project only). Confirm trust if prompted. ![VS Code MCP Server tab for Coreflux Documentation showing Install and Install in Workspace buttons and configuration with Type http and the MCP server URL](https://mintcdn.com/coreflux/2Qlxw_T8xvkddvBy/images/MCP/vscode-install-mcp.png?fit=max\&auto=format\&n=2Qlxw_T8xvkddvBy\&q=85\&s=e83435c5b09e548d23b3b3ef1cb90b4a) VS Code MCP server page after Connect to VS Code **4. Enable tools in Copilot** Reload the window if needed, then open Copilot Chat in **Agent** mode and enable the Coreflux tools in the tool picker. **Cursor** **1. Open the contextual menu** On any Coreflux documentation page, click **Copy page** (top right) to expand the menu. **2. Connect to Cursor** Select **Connect to Cursor**. Cursor opens the MCP server installation flow for **Coreflux Documentation**. **3. Install the server** In **Settings → Tools & MCPs**, review the pre-filled **Install MCP Server?** form (name, type, and URL), then click **Install**. ![Cursor Settings Tools page showing Install MCP Server dialog for Coreflux Documentation with Name, Type streamableHttp, URL, and Install button](https://mintcdn.com/coreflux/2Qlxw_T8xvkddvBy/images/MCP/cursor-install-mcp.png?fit=max\&auto=format\&n=2Qlxw_T8xvkddvBy\&q=85\&s=4ba9395dd8281d8683178457bfcb5f20) Cursor Tools settings after Connect to Cursor **4. Confirm in Cursor** Verify **Coreflux Documentation** appears under **Installed MCP Servers** with tools enabled. > [!NOTE] > > 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. **Other clients** For Claude Desktop, Claude.ai, or other MCP clients that are not in the menu: **1. Copy the server URL** Open the contextual menu and choose **Copy MCP Server**. Paste the URL into your client's MCP configuration. **2. Or use the install command** Choose **Copy MCP install command** and run the copied `npx add-mcp` command in your terminal, then follow the prompts for your editor. **3. Finish setup in your client** Restart the client and confirm the **Coreflux Documentation** server is connected. See the **Manual** tab for editor-specific configuration examples. **Manual** 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":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} https://docs.coreflux.org/mcp ``` > [!TIP] > > 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** Cursor supports MCP servers natively. Add the Coreflux MCP to your global configuration or to a specific project. **1. Open MCP Configuration** 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. **2. Add the Coreflux MCP Server** Add the following entry to the `mcpServers` object: ```json wrap theme={"theme":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} { "mcpServers": { "coreflux": { "url": "https://docs.coreflux.org/mcp" } } } ``` **3. Restart Cursor** 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. **4. Verify the Connection** Open Cursor Settings and navigate to the **MCP** section. You should see `coreflux` listed as a connected server with its tools available. > [!NOTE] > > If you already have other MCP servers configured, simply add the `"coreflux"` entry alongside them inside the existing `mcpServers` object. **Claude Desktop** Claude Desktop supports MCP servers through its configuration file. **1. Locate the Configuration File** Open the Claude Desktop configuration file at: **Windows** ```text wrap theme={"theme":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} %APPDATA%\Claude\claude_desktop_config.json ``` **macOS** ```text wrap theme={"theme":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} ~/Library/Application Support/Claude/claude_desktop_config.json ``` **Linux** ```text wrap theme={"theme":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} ~/.config/Claude/claude_desktop_config.json ``` If the file doesn't exist, create it. **2. Add the Coreflux MCP Server** Add or merge the following configuration: ```json wrap theme={"theme":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} { "mcpServers": { "coreflux": { "url": "https://docs.coreflux.org/mcp" } } } ``` **3. Restart Claude Desktop** 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** Claude.ai (the web interface) supports remote MCP server connections through its Integrations settings. **1. Open Claude.ai Settings** Go to [claude.ai](https://claude.ai), click your profile icon, and navigate to **Settings**. **2. Navigate to Integrations** Select the **Integrations** section in the settings panel. **3. Add the Coreflux MCP Server** Click **Add Integration** (or **Add MCP Server**), and enter the Coreflux MCP Server URL: ```text wrap theme={"theme":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} https://docs.coreflux.org/mcp ``` Give it a name like **Coreflux** and confirm. **4. Start a Conversation** Open a new conversation. The Coreflux tools will be available for Claude to use when relevant to your prompts. > [!NOTE] > > Remote MCP server support on Claude.ai may require a Pro or Team plan. Check your plan's feature availability. **VS Code** VS Code supports MCP servers through GitHub Copilot's agent mode. Ensure you have the **GitHub Copilot** extension installed and updated. **1. Open MCP Configuration** 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`. **2. Add the Coreflux MCP Server** In `.vscode/mcp.json`, add: ```json wrap theme={"theme":{"light":"css-variables","dark":"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":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} { "mcp": { "servers": { "coreflux": { "type": "sse", "url": "https://docs.coreflux.org/mcp" } } } } ``` **3. Reload VS Code** Reload the window (`Ctrl+Shift+P` → **Developer: Reload Window**). The Coreflux MCP tools will become available in Copilot's agent mode. **4. Confirm trust and enable tools** 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. **5. Use in Copilot Chat** 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":{"light":"css-variables","dark":"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. ``` > [!TIP] > > 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. > [!WARNING] > > **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, Connectors — 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 > **Be Specific with Your Questions** > > The more specific your question, the better the results. Instead of asking "tell me about connectors," try "how do I configure a PostgreSQL data storage connector with authentication." Specific queries help the tools return more relevant documentation. > **Ask for Source References** > > 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. > **Use for Code Review** > > 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 connector?"* This ensures your code follows the latest documented patterns. > **Add the Agent Skills** > > The MCP answers questions on demand. On broker 2.22 or later, the [Coreflux agent skills](/ai/agent-skills) give your assistant LoT and broker knowledge before it asks. Install both for the most reliable LoT. > **Leverage Conversation Context** > > 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. > [!WARNING] > > 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 > **[Developing LoT Using AI](/ai/developing-with-lot)** > > A guide on utilizing AI agents to write, debug, and deploy LoT scripts directly through the MCP interface. > **[AI-Assisted Development Best Practices](/ai/best-practices)** > > Strategic advice on prompt engineering, security guardrails, and maintaining consistency when using AI to manage industrial assets. # Account, Fleet, and Cloud Mode Source: https://docs.coreflux.org/hub/account-and-fleet Sign in to Coreflux HUB with an organization account, open brokers from the fleet, and see how Cloud mode differs from a HUB you run yourself A Coreflux account is how you see every broker your organization has registered, and how the HUB connects to those brokers without a local password. The account lives at [portal.coreflux.org](https://portal.coreflux.org). The HUB only displays the fleet and opens a broker; registration, licensing, and enrollment stay in the portal. > [!TIP] > > **Like a key cabinet for the sites you run.** Signing in shows the labeled keys (the fleet). Taking a key opens that site. Cutting a new key, or deciding who may hold one, still happens at the front desk — the portal. ## When to use an account | You want to… | Use | | - | - | | Open a broker you saved by host, port, and username | **This machine** on the login card. Those connections stay in this HUB. They do not need an account | | See brokers the organization registered, and connect as yourself | **Coreflux Fleet**, after **Sign in with Coreflux** | | Activate a license or enroll a broker you are already connected to | [System Config → License](/hub/system/configuration#license). The portal issues the code or token; the HUB sends it to the broker | The login card shows **This machine** and **Coreflux Fleet** as two tabs. **This machine** is the default once this HUB has any saved connection. Otherwise the card opens on **Coreflux Fleet**. The tabs are hidden while you are editing a connection. *** The fleet list is read-only. Nothing on it is starred, edited, or stored as a saved connection. ## Sign in **Desktop HUB** The desktop app signs in with no extra configuration. It talks to [portal.coreflux.org](https://portal.coreflux.org). **1. Open Coreflux Fleet** On the login card, select **Coreflux Fleet**. **2. Sign in with Coreflux** Click **Sign in with Coreflux**. The HUB opens the account sign-in page in your browser and returns to the HUB when it finishes. The card then shows **Signed in as** and your display name. **3. Sign out** **Sign out** on that line ends the HUB session. It does not delete saved connections on **This machine**. If credential storage is off, the card adds **this session only; credential storage is off**. You sign in again after the next launch. **Browser HUB** A HUB you open in a browser shows **Sign in with Coreflux** only when the HUB server has all three of these set: | Variable | Role | | - | - | | `COREFLUX_OIDC_CLIENT_ID` | Account client the HUB uses to sign in | | `HUB_PUBLIC_URL` | Origin users type in the browser, with no trailing slash. The HUB's callback is `{HUB_PUBLIC_URL}/api/account/callback`, and that exact URL must be registered for the account client | | `HUB_SESSION_SECRET` | Encrypts the session cookie. At least 32 characters. Use the same value on every replica. Changing it signs everyone out | `COREFLUX_OIDC_CLIENT_SECRET` is optional and only for a confidential client. The desktop app ignores these variables. To use the HUB assistant in a browser HUB, also set `HUB_AGENT_ALLOWED_HOSTS`. See [Allowed hosts in a browser HUB](/hub/ai-assistant#allowed-hosts-in-a-browser-hub). The browser never receives the access token. If the session has expired, the HUB reports **Your Coreflux session has expired — sign in again.** If the three variables are missing, a fleet connect reports **Coreflux sign-in is not enabled on this Hub.** **Signed in as** shows your display name: `preferred_username` from the account token, or your email when that claim is absent. The status bar and the account menu use the same name. ## The fleet list After sign-in the card shows **FLEET BROKERS** and the count on the same line, for example **FLEET BROKERS 4**. * **Organization** — A dropdown appears only when the account belongs to more than one organization. **All organizations** lists every membership. * **Refresh** — Loads the list again. * **Register broker** — Opens the portal. With one organization, or with one selected in the dropdown, that is the organization's fleet page (`/orgs/…/fleet` on the portal). **All organizations** opens the portal home so you can pick. * **Sign out** — Ends the account session. Each row is a broker the platform reported. The HUB does not invent the badges: | Badge | Meaning | | - | - | | **Online**, **Offline**, **Never reported** | An enrolled broker, from the platform's liveness | | **Enrolling**, **Registered**, **Un-enrolled**, **Requested**, **Deploying**, **Failed**, **Removed** | Any other phase the platform sent | | **Trial**, **Trial (Growth)**, or **Licensed (Growth)** | The platform's licence bucket. A plan name, when the platform sent one, appears in parentheses | | **Cloud** or **Self-hosted** | Where that broker runs | A row the HUB can address has **Connect**. A row that still needs enrollment has **Enrol in portal**. Anything else with no address has **Open in portal**. Address lines: * A readable host is shown as `protocol://host:port`. * A **Cloud** broker with no address yet reads **No address yet — assigned when the deploy finishes**. * Any other broker with no address reads **No address registered — add one in the portal**. Empty fleet: **No connections available yet**, with **Register broker**. The platform did not answer: **Can't reach the Coreflux platform. Your saved connections are unaffected.** **Retry** loads the list again. Saved connections on **This machine** stay usable. If some organizations loaded and others did not, the rows that loaded stay, with **Couldn't load brokers for .** **Connect** does not copy the broker into **This machine**. The next time you want that broker from the fleet, you sign in and connect again. A broker you also saved by hand remains a separate card. ## What Connect sends **Connect** uses your account access token as the MQTT password. The broker must have [OIDC bearer authentication](/latest/mqtt-broker/oidc-authentication) enabled, and its organization binding must include your account. Until [enrollment](/latest/mqtt-broker/licensing) finishes, the broker still accepts local usernames and refuses account tokens. That is the link between the products: 1. You register and license the broker at [portal.coreflux.org](https://portal.coreflux.org). 2. You enroll the broker so it learns the organization id ([License tab](/hub/system/configuration#license) or `-enroll`). 3. The fleet lists that broker for accounts in the organization. 4. **Connect** presents the account token. The broker maps the token's groups into LoT rules. The portal's own screens are not documented here. **Register broker**, **Enrol in portal**, and **Open in portal** leave the HUB and open the portal. ## Cloud mode and a HUB you run yourself Two different "cloud" words show up: | Name | What it is | What you do | | - | - | - | | **Cloud** badge on a fleet row | The broker is hosted for you. **Self-hosted** means the broker runs on infrastructure you operate | Read the badge. You do not set it in the HUB | | **Cloud mode** on the HUB process | A shared regional HUB. Its MQTT proxy only dials brokers in that region | Leave it off on the desktop app and on a HUB you install beside your own broker | Desktop and self-hosted HUB builds connect to the broker URL you asked for. That is **This machine** (the host and port you saved) and **Coreflux Fleet** (the address the platform returned). Cloud mode is on only when the HUB process has `HUB_CLOUD_MODE` set to `true` or `1`. It also needs `HUB_CLOUD_BASE_DOMAIN` (the regional suffix, such as `eu1.coreflux.cloud`). The proxy then accepts only `mqtts://` URLs whose host is a single name under that suffix, on `HUB_CLOUD_BROKER_PORT` (default `8883`). Any other scheme, port, or region is rejected. If Cloud mode is on and `HUB_CLOUD_BASE_DOMAIN` is missing, every connection is rejected. Do not enable Cloud mode on a desktop app or a HUB container you use to reach a broker on your own network. Those installs leave `HUB_CLOUD_MODE` unset. Account sign-in and Cloud mode are independent. A self-hosted HUB can show **Sign in with Coreflux** with Cloud mode off, as long as the browser sign-in variables above are set and the brokers accept account tokens. *** ## Limits > [!WARNING] > > The HUB does not register brokers, issue licenses, or complete enrollment by itself. Those stay at [portal.coreflux.org](https://portal.coreflux.org). A fleet row without an address cannot be connected until the portal has one. * Fleet rows are not editable, and **Connect** does not save a connection. * Signing in from the connection editor leaves the page and drops an unsaved draft. * **Sign out** ends the HUB session. The account provider's own browser session can still be active, so the next **Sign in with Coreflux** may not ask for a password. * The web HUB cannot show token subject or expiry in the connection editor. The browser does not hold the token. ## Next Steps > **[OIDC and Bearer Authentication](/latest/mqtt-broker/oidc-authentication)** > > Configure the broker so an account access token is accepted at MQTT CONNECT. > **[Licensing and Enrollment](/latest/mqtt-broker/licensing)** > > Activate a license and bind the broker to the organization the fleet lists. # 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 ![Animated sequence: opening the AI Assistant from the dock, typing a prompt, and the assistant responding and applying changes in Coreflux HUB](https://mintcdn.com/coreflux/EldKmEArSuwejnkZ/images/ai-assistant/hub-ai-demo.gif?s=a21be6b72ad7e94afe95ee1a9afd6bdb) Ask the AI Assistant to build something — it opens, drafts, and deploys in the same panel ## 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. > [!TIP] > > **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 Connectors, Actions, and panels where your permissions allow. * **Explore without guesswork** — Ask what topics exist, which Connectors 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. The picker beside the composer has two kinds of target: | Target | Where the loop runs | Use it when | | - | - | - | | **HUB** | In the HUB, over the broker's [MCP Server](/latest/mqtt-broker/mcp-server) | Conversation with an operator. Provider keys stay on the HUB. Every write asks first | | **Broker agent** | On the broker, as a `DEFINE AGENT` | The assistant must run with the HUB closed, be called by another agent, or be reached by another service | A broker agent is a [LoT agent](/latest/lot-language/routes/ai-routes) with `WITH HUB_VISIBLE "true"` and an interaction topic. The HUB does not list a legacy `TYPE AGENT` connector as a target. ## 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. ![AI Setup provider list grouped into Cloud (OpenAI selected, Anthropic) and Local/Private (Ollama with NO API KEY tag)](https://mintcdn.com/coreflux/EldKmEArSuwejnkZ/images/ai-assistant/hub-ai-providers.png?fit=max\&auto=format\&n=EldKmEArSuwejnkZ\&q=85\&s=5f2755a9981563d9bd1abcddd9c3d86b) Provider picker in the AI Setup wizard — cloud providers (OpenAI, Anthropic, OpenAI-compatible) and a local/private option (Ollama) * **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** or **Anthropic**, or any **OpenAI-compatible** endpoint, when your organization allows traffic to leave the network. > [!NOTE] > > **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. On the broker path the wizard writes that URL onto a `TYPE LLM` connector. See [Picking a provider](/latest/lot-language/routes/ai-routes#picking-a-provider). ### Ollama requirements Ollama is supported with requirements: the model must call tools, and its context must hold the HUB assistant's prompt. > [!NOTE] > > HUB 2.12 sets Ollama's context (`num_ctx`) for each run, so the Ollama setting no longer matters. It uses at least `HUB_OLLAMA_MIN_CTX` (default 16384) and up to `HUB_OLLAMA_MAX_CTX` (default 32768), never above the model's own maximum. A prompt too large for the model ends the run with a clear error instead of being truncated. On low-memory or CPU-only hosts, lower `HUB_OLLAMA_MAX_CTX`. > > The same release ends a run when the model sends nothing for `HUB_AGENT_FIRST_OUTPUT_TIMEOUT_MS`, or goes silent mid-answer for `HUB_AGENT_IDLE_TIMEOUT_MS`. Both default to 120000 (120 s), and time spent inside broker tools doesn't count. Raise them on slow hardware, where the first answer can take minutes. These are HUB server environment variables. > [!NOTE] > > On a HUB older than 2.12, raise Ollama's **Settings > Context length** yourself. The prompt is about 4.5k tokens in Insight mode and 9k to 15k tokens in Agent mode, and those versions silently cut the start of it when the context is too small. 16k is the minimum; 128k is a safe choice. > [!TIP] > > 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 asks whether it should run **in the HUB** or **on the broker**, then walks through **provider**, **model**, and **API key**. When the broker path is not signed in as a local broker user, it also asks which broker user the agent's `DEFAULT_USER` should be. You only do this once per broker; after that, clicking the Coreflux icon opens the assistant directly. > [!NOTE] > > The screenshots below were taken on the previous wizard, which installed an agent connector and said "Install the Agent route". The current wizard starts with **In the HUB** (the default) or **On the broker**. The provider, model, and key steps are unchanged. **1. Open the AI Setup wizard** 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, or an OpenAI-compatible endpoint) or **Local / Private** (Ollama). Pick one and click **Continue**. ![Coreflux HUB with the AI Setup wizard open on the right, titled Set up AI Assistant with the note Install the Agent route on your broker to enable the AI assistant, a step indicator, Cloud providers OpenAI (selected) and Anthropic, Local / Private Ollama tagged No API key, and a Continue button](https://mintcdn.com/coreflux/9PekAdlvyyrySLnc/images/ai-assistant/hub-ai-setup-provider-light.png?fit=max\&auto=format\&n=9PekAdlvyyrySLnc\&q=85\&s=6183b5872491bd19d4bddbc16ebb2130) AI Setup wizard, step 1: pick a cloud provider (OpenAI, Anthropic, OpenAI-compatible) or a local Ollama model, then Continue **2. Pick a model** 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. ![AI Setup wizard model step listing suggested models such as gpt-5.5, gpt-5.4-mini, and a custom model field with gpt-5.4-mini typed in](https://mintcdn.com/coreflux/EldKmEArSuwejnkZ/images/ai-assistant/hub-ai-setup-model.png?fit=max\&auto=format\&n=EldKmEArSuwejnkZ\&q=85\&s=def936ab3298db1a916726f2f79d29fa) AI Setup wizard step 2 — choose a suggested model or type a custom one **3. Add your API key** 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. > [!NOTE] > > If you picked **Ollama** in the earlier step, the wizard skips this screen entirely—your local Ollama endpoint does not need a key. ![AI Setup wizard API key step with an OpenAI API key input, a note that the key is stored as an encrypted secret on the broker, and an install summary card](https://mintcdn.com/coreflux/EldKmEArSuwejnkZ/images/ai-assistant/hub-ai-setup-apikey.png?fit=max\&auto=format\&n=EldKmEArSuwejnkZ\&q=85\&s=558bc27b8c12a978a8f523e591a4c681) AI Setup wizard step 3 — paste your provider API key; the broker stores it as an encrypted secret > **Where do I get an API 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/) > * **OpenAI-compatible** — the key your endpoint's operator issues, if it needs one > > If you picked **Ollama**, no key is required—the broker talks to your local Ollama endpoint directly. **4. Install and you're ready** Finish the wizard. **In the HUB** stores the provider and an MCP token for this connection, then the assistant calls the broker as that token's user. **On the broker** deploys a `TYPE LLM` connector named `BrokerAgentLlm` and a `DEFINE AGENT` named `BrokerAgent` on the interaction topic `hub/assistant`, visible in the picker. A key you paste is stored with `-setSecret` and referenced from the connector. Ollama needs no key. An OpenAI-compatible endpoint gets a key only when you supply one, and the connector is pinned to Chat Completions. ![AI Setup wizard showing a Setting up AI Assistant progress indicator with Setting API key secret status](https://mintcdn.com/coreflux/EldKmEArSuwejnkZ/images/ai-assistant/hub-ai-setup-installing.png?fit=max\&auto=format\&n=EldKmEArSuwejnkZ\&q=85\&s=020cb3cafaa3d294a32252521f5dd1ed) AI Setup finishing — the broker provisions the assistant in the background > [!TIP] > > 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. **On the broker**, the wizard also adds subscribe rules so your user can read `$SYS/Coreflux/Agents/#` and the `hub/assistant/#` replies. `DEFAULT_USER` is your local MQTT login. A Coreflux account session is not a broker user — the wizard asks for a **Broker user** and will not install until you choose one. A tool call succeeds only when both you and that user are allowed to make it. The installed agent is granted `read_mqtt`, `read_lot`, `write_mqtt`, and `create_lot` for actions, models, connectors, and panels. Writing grants are deployed with `WITH LEVEL "write"`. Re-running setup sends `-addConnector` and `-addAgent` again. Both update in place, so the agent keeps its conversations. The broker refuses to remove an LLM connector an agent still uses. Agents are their own LoT entity. Connectors Manager lists `BrokerAgentLlm` and leaves `BrokerAgent` out. The LoT Editor deploys an agent cell with `-addAgent` and removes it with `-removeAgent`. *** ## Manage broker agents The **Agents** app on the dock lists every `DEFINE AGENT` on the broker as a card, next to an **Activity** tab. Each card has **Configure**, **Disable** / **Enable**, **Open in LoT Editor**, **Show activity**, and **Remove**. A card with no status shows offline. `DEFINE AGENT` is a preview feature in broker 2.22. ### Disable an agent The power icon on a card redeploys the agent with `DISABLE true`, or without it. A disabled agent stays defined but takes no tasks. Its card is dimmed, with a **Disabled** pill and a grey status dot. On a broker without `DISABLE` support, the HUB changes nothing and says so. In a cluster, the change applies on the node you're connected to. ![Coreflux HUB Agents window, Agents tab, 2 agents: the FlightsTrackerAgent card is dimmed with a Disabled pill and a grey status dot and shows LocalOllama and read\_mqtt (1 topic); the GateOpsAgent card has a green status dot and shows LocalOllama, ollama, llama3.2 and read\_mqtt (1 topic), write\_mqtt (1 topic). Each card has Configure, power, LoT, activity, and remove icons](https://mintcdn.com/coreflux/9PekAdlvyyrySLnc/images/ai-assistant/hub-agents-disabled-light.png?fit=max\&auto=format\&n=9PekAdlvyyrySLnc\&q=85\&s=7ba27b4b2ed67ab48576b3795cf7b319) Agents app with FlightsTrackerAgent disabled and GateOpsAgent running ### Configure an agent **Configure** and **Add agent** open a form with three tabs: | Tab | What you set | | - | - | | **Configuration** | Name, LLM connector, description, default user, and **Enabled**. Then the system prompt, guardrails, interaction topic, **Hub visible**, memory, and peers, and the limits (max iterations, max tokens, task timeout, max in flight) | | **Tools** | The `ADD TOOL` grants, with their topics or targets and level | | **LoT** | The `DEFINE AGENT` source, with live checks | ![Coreflux HUB Configure GateOpsAgent dialog with Configuration, Tools 2, and LoT tabs; the Agent section shows Name GateOpsAgent, LLM connector LocalOllama, Description Suggests gate changes when departures run late, Default user flights-ops, and the Enabled switch on, with Cancel and Save](https://mintcdn.com/coreflux/9PekAdlvyyrySLnc/images/ai-assistant/hub-agent-config-form-light.png?fit=max\&auto=format\&n=9PekAdlvyyrySLnc\&q=85\&s=221a3bee482408c4063915922e1c1df1) Configure GateOpsAgent: the Configuration tab The LoT stays the source of truth. The form rewrites only the clauses you change and keeps everything else as written, including comments, `GET SECRET` and `GET ENV` values, and clauses the form has no field for, which it lists under **Kept as written**. **Save** deploys with `-addAgent`. Running tasks and session memory are kept unless you rename the agent. See [Agent syntax](/latest/lot-language/agents/syntax). *** ## Coming from a TYPE AGENT assistant A `TYPE AGENT` connector from an earlier release is skipped when the broker loads, and the HUB does not list it, install it, or migrate it. There is no in-panel update that rewrites it. Rewrite it as a `TYPE LLM` connector plus `DEFINE AGENT`, using the table in [Coming from TYPE AGENT](/latest/lot-language/agents/overview#coming-from-type-agent), or run the wizard again and let it install `BrokerAgentLlm` and `BrokerAgent`. Your previous provider, model, and key are not copied for you. > [!NOTE] > > The setup wizard is how you install the assistant. It is not a settings screen for an assistant that is already there. Once the HUB target is configured, the menu offers **Change Hub provider…**, which updates the provider, key, and model and keeps the same MCP token. *** ## 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. **1. Hover to preview** 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. ![Coreflux HUB dock with the Coreflux icon highlighted and a compact Coreflux AI popover showing Ask anything placeholder and Insight and Agent toggles](https://mintcdn.com/coreflux/EldKmEArSuwejnkZ/images/ai-assistant/hub-ai-hover.png?fit=max\&auto=format\&n=EldKmEArSuwejnkZ\&q=85\&s=4832f01bd4c40af988f40d1c774d0d31) Hover the dock Coreflux icon to open the compact Coreflux AI bubble with Ask anything and mode pills **2. Click to open 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. ![Coreflux HUB with the full AI Assistant panel open showing suggested actions, Sources with LoT Editor, and Ask me anything input](https://mintcdn.com/coreflux/EldKmEArSuwejnkZ/images/ai-assistant/hub-ai-open.png?fit=max\&auto=format\&n=EldKmEArSuwejnkZ\&q=85\&s=051b1570ab0d0c92aebdee66e0254294) Full AI Assistant panel — suggested prompts, Sources, and the main chat input ## 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—Connectors, Actions, Models, panels—subject to your permissions. On the **HUB** target, each write pauses for Approve or Reject. On a **broker agent**, writing tools run when the grant says `WITH LEVEL "write"` and both you and `DEFAULT_USER` are allowed. | You want the assistant to **build or change** integrations, logic, or dashboards. | | **Insight** | **Read-only.** The HUB target never shows write tools. A broker agent is sent `"mode": "insight"`, and the broker refuses every writing tool as out of scope, including tools a peer agent would have run. | 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. > **[Build a dashboard](/hub/dashboards/overview)** > > 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). > **Develop an Action in LoT** > > Describe triggers, conditions, and outputs in plain language; the assistant proposes LoT you review and deploy without leaving the conversation. > **Calculate a new KPI** > > Describe the metric or inputs you care about; the assistant drafts logic, confirms details with you, and deploys an Action in Agent mode. > **Explain your broker state** > > Ask what Connectors 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. **1. Ask** Type a goal such as **Calculate a new KPI**. The assistant loads context and may offer quick actions that match what you said. ![AI Assistant showing Agent mode selected and suggested prompts including Calculate a new KPI](https://mintcdn.com/coreflux/EldKmEArSuwejnkZ/images/ai-assistant/hub-ai-open.png?fit=max\&auto=format\&n=EldKmEArSuwejnkZ\&q=85\&s=051b1570ab0d0c92aebdee66e0254294) AI Assistant in Agent mode with suggested prompts including Calculate a new KPI **2. Clarify** 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. ![AI Assistant conversation where the assistant asks the user to provide KPI formula or source data with a Type your answer field](https://mintcdn.com/coreflux/EldKmEArSuwejnkZ/images/ai-assistant/hub-ai-ask-user.png?fit=max\&auto=format\&n=EldKmEArSuwejnkZ\&q=85\&s=8ce9eee3ebf0ebfe0e336b4734dbf7ce) Assistant asking for KPI formula or source data before continuing **3. Deploy** 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. ![AI Assistant showing deployed CalculateKPI Action with Running status and Open in LoT Editor link](https://mintcdn.com/coreflux/EldKmEArSuwejnkZ/images/ai-assistant/hub-ai-action-deployed.png?fit=max\&auto=format\&n=EldKmEArSuwejnkZ\&q=85\&s=a84807cf92b4bb27a348a2bc1d9b0f87) Deployed Action with Running status and Open in LoT Editor link > [!TIP] > > 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. ![AI Assistant in Insight mode responding to 'Please create a list of connectors I have on broker' with a table showing Name, Type, Status, Description, and Events columns for Terminal, BrokerAgent, and MyS7Route](https://mintcdn.com/coreflux/EldKmEArSuwejnkZ/images/ai-assistant/hub-ai-insight-routes.png?fit=max\&auto=format\&n=EldKmEArSuwejnkZ\&q=85\&s=93cc5ba1d07a972a3e4aad3faf1a075a) Insight mode — list of Connectors returned as a table after a natural-language question Try questions like **Create me a list of Connectors** or **Which Connectors are in a warning state?** The assistant reads broker state and renders the response in the panel without using write tools. ## Built-in safeguards * **The HUB target runs as the MCP token's user** — Every call passes the broker's own permission check. Writes pause until you approve them. A request that names no mode is treated as Insight. * **A broker agent runs as the person who asked, capped by `DEFAULT_USER`** — Both must be allowed. A refusal names the user who lacked the right. * **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. * **A shared web HUB does not call arbitrary hosts**: see [Allowed hosts in a browser HUB](#allowed-hosts-in-a-browser-hub). ### Allowed hosts in a browser HUB The HUB assistant connects from the HUB server to the broker's MCP endpoint and, for Ollama or an OpenAI-compatible provider, to the model's base URL. On a browser or Docker HUB that other people can reach, the assistant stays off until both of these are true: 1. [Coreflux sign-in](/hub/account-and-fleet#sign-in) is configured on the HUB server, and the user is signed in. 2. `HUB_AGENT_ALLOWED_HOSTS` lists the hosts the assistant may reach. | Variable | Role | | - | - | | `HUB_AGENT_ALLOWED_HOSTS` | Comma-separated list of the broker MCP hosts and Ollama or OpenAI-compatible servers the HUB assistant may connect to. A bare host (`broker.example.com`) matches any port. `host:port` (`10.0.0.5:6278`) matches only that port. Matching is exact and case-insensitive: `evilbroker.example.com` does not match `broker.example.com` | ```bash wrap theme={"theme":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} HUB_AGENT_ALLOWED_HOSTS=broker.example.com,10.0.0.5:6278 ``` * **Unset or empty**: the assistant is off and reports **HUB\_AGENT\_ALLOWED\_HOSTS names no broker it may reach.** * **Sign-in not configured**: the assistant is off and tells you to use a broker agent or the desktop app. * **Not signed in**: **Sign in with your Coreflux account to use the Hub assistant.** * **Host not listed**: **This Hub may not reach ``. Add it to HUB\_AGENT\_ALLOWED\_HOSTS.** OpenAI and Anthropic use their fixed API addresses and need no entry. The desktop app listens only on loopback and ignores this variable. Broker agents run on the broker and are not affected. *** ## Broker agents in the HUB **LoT → Agents** (also the dock and the status-bar **Agents** widget) is where you manage broker agents and watch them work. Add and configure from that window. Saving an existing agent sends `-addAgent` again, which updates it in place and keeps its conversations. Removing an agent cancels its running tasks and forgets its session memory. The activity view is read-only: it follows the task stream, and a question started by `CALL AGENT` shows up as a short preview. Connectors Manager lists connectors only. An LLM connector can show how many agents use it. The broker refuses to remove that connector while an agent still names it. *** ## Best practices > **Start in Insight when you are learning** > > Use Insight to map topics, Connectors, and health first. Switch to Agent only when you are ready for the assistant to create or update broker objects. > **Always review what was deployed** > > Open new Actions or Connectors in the **LoT Editor** or **Connectors** app to read names, triggers, and bindings before you rely on them in production. ## Next steps > **[LoT Editor](/hub/lot-editor)** > > Open and refine Actions, Models, and Rules the assistant creates—same editor, full control. > **[Connectors overview](/hub/routes/overview)** > > Add or monitor Connectors the assistant can read and help you configure next. # Cluster Viewer Source: https://docs.coreflux.org/hub/cluster-viewer See the nodes, leader, links, LoT ownership, and partitions of a Coreflux cluster from the broker host you are connected to ## Why Use Cluster Viewer? A cluster is several broker hosts acting as one system. Cluster Viewer draws that system from the broker host you are already connected to, so you can see who leads, which links are up, which node owns each exclusive piece of LoT, and how partitions are placed — without opening a shell on every host. > [!NOTE] > > Cluster Viewer carries a **Beta** badge on its window. The dock icon is labeled **Cluster**. It appears only while the connected broker host is publishing cluster status. > [!TIP] > > **Like a seating chart that updates itself.** You see who is leading, who is sitting in, and who is holding each shared task — from the one host you opened. ## When to Use It * **Confirm the cluster is up** — See which members are alive and who the leader is * **Check a link** — See whether data-plane links between hosts are connected * **Find who runs an exclusive piece of LoT** — Timers, `ON START` actions, exclusive connectors, and scheduled connector events run on one node * **See partition placement** — See which host owns each partition and where the copies sit Clustering is an Enterprise license feature. If the license does not grant it, the broker runs standalone and this icon stays off the dock. See [High-availability clustering](/latest/mqtt-broker/clustering) and [Licensing](/latest/mqtt-broker/licensing). *** Open **Cluster** on the dock. The window is read-only. It reads the cluster status the connected broker host publishes. It does not elect a leader, move ownership, or change cluster settings. If clustering is off, the icon is not on the dock. Opening the window another way shows that the connected broker is not publishing cluster status. ## What the Window Shows A strip across the top summarizes the cluster as this broker host sees it: | Item | What it tells you | | - | - | | **Cluster** | The cluster identity | | **Leader** | The host that currently leads | | **Members** | How many members are alive out of the member count | | **Term** / **Commit** | How far leader election and the committed log have progressed | | **LoT** | Whether nodes are on the same committed LoT version, or the versions are mixed | | **Data plane** | How many links between hosts are up | | **Listeners** | Whether any server-connector listener on a member has failed | The strip also says the view is **Live** (or how old the status is), which host you are seeing it from, and that it is **read-only**. The canvas draws each member and the links between them. Select a node to open it on the **Node** tab. Select a link to see each direction. The leader is marked on its node. A host the status names but does not describe yet is drawn as unidentified. > [!WARNING] > > The picture is the view from the broker host you are connected to. It is not a second login to every member. ## Details **Node** The selected host: whether it is alive, its role, and its endpoint when the cluster published one. For the host you are connected to, the tab also shows the term and commit index, and whether that host holds the LoT lease. **LoT** shows the version this host has applied against the version the cluster has committed, whether those match, and any apply errors. **Connector listeners** lists the server connectors that host reported. **Outgoing data-plane links** lists the links that start on this host. ![Coreflux HUB Cluster Viewer window with the Node tab selected: the cluster summary strip across the top, the cluster members and links on the canvas, and the node details panel](https://mintcdn.com/coreflux/9PekAdlvyyrySLnc/images/cluster-viewer/hub-cluster-viewer-node-light.png?fit=max\&auto=format\&n=9PekAdlvyyrySLnc\&q=85\&s=3304d205524e2aaa61ab65c8148a1547) Node tab: the selected host, its role, LoT version, connector listeners, and outgoing data-plane links **Links** **Replication** on the host you are connected to: connected peers, forward backlog, dropped forwards, quorum failures, and how long the last quorum took. **Data-plane links** lists every reported link, with its state (connected, connecting, disconnected, or unknown). A stale report is marked stale. ![Coreflux HUB Cluster Viewer window with the Links tab selected: the cluster summary strip across the top, the cluster members and links on the canvas, and the links details panel](https://mintcdn.com/coreflux/9PekAdlvyyrySLnc/images/cluster-viewer/hub-cluster-viewer-links-light.png?fit=max\&auto=format\&n=9PekAdlvyyrySLnc\&q=85\&s=b6c4bc1d5c2b32904f7410f31fd3fc97) Links tab: replication on the connected host and the state of every data-plane link **Ownership** Which host runs each piece of LoT that must run on exactly one node: | Kind | What it is | | - | - | | **Actions (timers, ON START)** | Scheduled actions, and actions that run once when the cluster session starts | | **Exclusive connectors** | Connectors that hold a device, a serial port, or a server port | | **Scheduled connector events** | A connector's timed events | **Owner** is the host that runs it. **Next** is the host that takes it if the owner is lost. **Epoch** goes up each time the entity moves. **Unowned** means no live host can run it, so it runs nowhere until one can. An exclusive connector is defined on every node, but only its owner opens the connection. The others hold it ready. Select a node on the canvas first if you want the list limited to entities that node owns. ![Coreflux HUB Cluster Viewer window with the Ownership tab selected: the cluster summary strip across the top, the cluster members and links on the canvas, and the ownership details panel](https://mintcdn.com/coreflux/9PekAdlvyyrySLnc/images/cluster-viewer/hub-cluster-viewer-ownership-light.png?fit=max\&auto=format\&n=9PekAdlvyyrySLnc\&q=85\&s=3098b8f9b250f96a0bc10111f13db364) Ownership tab: which host owns each exclusive action, connector, and scheduled connector event **Partitions** The partition map: how many partitions there are, the replication factor, how many are under-replicated, and which host owns each one. Under-replicated partitions still work. Their replica set is smaller than the factor because there are not enough live hosts to place every copy — for example while a host is down. A healthy cluster shows none. Pick a host to highlight the partitions it owns, the ones where it only keeps a copy, and the ones it does not hold. ![Coreflux HUB Cluster Viewer window with the Partitions tab selected: the cluster summary strip across the top, the cluster members and links on the canvas, and the partitions details panel](https://mintcdn.com/coreflux/9PekAdlvyyrySLnc/images/cluster-viewer/hub-cluster-viewer-partitions-light.png?fit=max\&auto=format\&n=9PekAdlvyyrySLnc\&q=85\&s=08e8a96be261df65232867e5e2f0b2a9) Partitions tab: the partition map, replication factor, and owner of each partition **Clients** The MQTT clients connected to each node right now, with a total across the cluster. A client is connected to one node. Behind a load balancer, the node can change each time it reconnects. Under **Show the clients of**, pick **All nodes** or one node. Filter by client ID, user, or address, and sort by **Last active**, **Newest connection**, **Most queued**, or **Client id**. The connections the nodes open to each other are hidden unless you turn on **Show the cluster's own connections**. Each card shows the client ID, user, address, MQTT version, messages in and out, and when it was last active. A **queued** badge counts messages waiting for that client; a number that keeps growing means the client isn't keeping up. Expand a card for when it connected, its last packet, bytes in and out, and how long the broker keeps its session after it disconnects. Lists refresh every 7 seconds. Other nodes' clients are relayed by those nodes, so they can be a few seconds older. An older broker reports only the clients of the node you're connected to. ![Coreflux HUB Cluster Viewer for cluster acme-plant, leader acme-mqtt-2, 3/3 members alive. The Clients tab shows 9 clients (+12 cluster connections) and 0 queued messages, the node filters All nodes, acme-mqtt-1 4, acme-mqtt-2 3, and acme-mqtt-3 2, the filter box, the Last active sort, and the Show the cluster's own connections checkbox, above cards for hmi-panel-03, the HUB's own connection, line1-plc-gw, and energy-meter-gw, each with node, user, address, MQTT version, messages in and out, and last activity](https://mintcdn.com/coreflux/9PekAdlvyyrySLnc/images/cluster-viewer/hub-cluster-viewer-clients-light.png?fit=max\&auto=format\&n=9PekAdlvyyrySLnc\&q=85\&s=c447f955d698db7e81dfa195823d229a) Clients tab on a three-node cluster: nine clients across acme-mqtt-1, acme-mqtt-2, and acme-mqtt-3, sorted by last active An older broker host may publish only part of this view. The window then names what is missing and still shows the status it did receive. *** ## Next Steps > **[High-Availability Clustering](/latest/mqtt-broker/clustering)** > > Turn clustering on, choose discovery, and read what failover does and does not cover. > **[Connections Manager](/hub/connections)** > > Open the broker host whose cluster status this window reads. # 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 ![Coreflux HUB login card with Local Connections 2: Coreflux local at mqtt://localhost:18840 tagged Coreflux and Mosquitto local at mqtt://localhost:18830 tagged MQTT, both Online with an anonymous identity and Connect buttons, plus All and Starred filters and New Connection](https://mintcdn.com/coreflux/9PekAdlvyyrySLnc/images/connections/hub-login-screen-light.png?fit=max\&auto=format\&n=9PekAdlvyyrySLnc\&q=85\&s=19b9b3a1e41004e0baee97fcf8ccdf4b) Coreflux HUB login screen with two saved local connections, their broker type and live status, and a New Connection button ## 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 **1. Open New 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. **2. Fill in the broker details** 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. ![Coreflux HUB connection form for Coreflux local with protocol mqtt://, host localhost, port 18840, the preview mqtt://localhost:18840, an Authentication row No username or password marked Default, Add user, Advanced options, and Save, Test, and Connect buttons](https://mintcdn.com/coreflux/9PekAdlvyyrySLnc/images/connections/hub-login-setup-light.png?fit=max\&auto=format\&n=9PekAdlvyyrySLnc\&q=85\&s=8a0bc7ffdfd5adeaff1721a8a0c3acf2) Connection form for Coreflux local: name, protocol, host, port, and its identities **3. Add an identity** 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. **4. Test, then Save or Connect** 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. ## Default connection (web / Docker) A web HUB started with Docker can open **New Connection** with the broker already filled in, so a lab or demo does not make every visitor type the address. Set the variables on the HUB container. The HUB reads them when the container starts. The desktop app ignores them. Setting `HUB_DEFAULT_CONNECTION_HOST` turns the feature on. The other variables are optional. The values pre-fill the **New Connection** form only. They never create or rewrite a saved connection. The connection is stored when someone clicks **Save** or **Connect**, the same as a form they typed. | Variable | What it sets | | - | - | | `HUB_DEFAULT_CONNECTION_HOST` | Broker host. Setting this turns the feature on | | `HUB_DEFAULT_CONNECTION_NAME` | Name on the form. When omitted, the name is filled in when the connection is saved | | `HUB_DEFAULT_CONNECTION_PROTOCOL` | `mqtt`, `mqtts`, `ws`, or `wss`. A trailing `://` is accepted. When omitted, the form uses `ws` | | `HUB_DEFAULT_CONNECTION_PORT` | Port. When omitted, the usual port for that protocol | | `HUB_DEFAULT_CONNECTION_PATH` | WebSocket path. When omitted, `mqtt` | | `HUB_DEFAULT_CONNECTION_USERNAME` | Username on the form. When omitted, the form keeps its usual default identity | | `HUB_DEFAULT_CONNECTION_PASSWORD` | Password for that username. Ignored when no username is set | The [Compose example](/latest/quick-start/installation) sets host `broker`, protocol `mqtt`, and port `1883` for a broker on the same Docker network. > [!WARNING] > > The password is delivered to every browser that loads the HUB. Use this only for shared lab or demo credentials. ## Saved Connections Each saved connection appears as a card on the login screen, showing its name, broker URL, and a reachability status the HUB probes in the background. You can see whether that broker answers before you open it. | Status | Meaning | | - | - | | **Checking** | The HUB is still probing this broker | | **Online** | The broker answered | | **Sign-in failed** | The broker answered, and the last sign-in did not succeed | | **Offline** | The broker did not answer | * **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. * **Connect** — pick an identity from the card's dropdown and click **Connect**. With a card focused, the Enter key does the same. * **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 pill without leaving the app: choose **Switch user** to reconnect as a different identity on the *current* broker, or **Switch 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. The rest of that menu is covered in [Desktop shell](/hub/overview#logo-and-the-account-pill). ## 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. > [!NOTE] > > This is separate from **connector** certificate management (trust stores for OPC UA and similar connectors running *on* the broker) — see the connector's own Certificate Management section when you configure a [Connector](/hub/routes/overview). ## When the Broker Is Not Coreflux The HUB detects the broker when you connect. You do not pick a broker type. When the connection is a generic MQTT broker, the dock keeps **Data Viewer** and **UNS Viewer**. Connectors, the LoT Editor, LoT Flow, Dashboards, and Cluster stay hidden. The status bar offers **Know more about Coreflux**, which explains what a Coreflux broker adds and links to the docs, the install guide, and [coreflux.org](https://coreflux.org). The account menu does not show **LoT Templates** or broker administration. While the HUB is still checking, the dock shows those same two apps. **Know more about Coreflux** appears once the check settles on a generic broker. After that session, the saved card is tagged **MQTT**. A card tagged **Coreflux** means the last session reached a Coreflux broker, and the usual apps are available. The tag is absent until a session has settled. > [!NOTE] > > A Coreflux user without system access is still a Coreflux broker. You keep the usual apps. The LoT Editor still needs system access on that broker host, and the status bar can show **No SYS**. That session is not the generic-broker case, and the card is tagged **Coreflux**. ## 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. ## Coreflux account and fleet Saved connections are the **This machine** tab: a host, a port, and a username you store in this HUB. **Coreflux Fleet** is the other tab. It lists brokers your organization registered at [portal.coreflux.org](https://portal.coreflux.org) and connects with your account token instead of a saved password. Fleet rows are not added to this list. See [Account, fleet, and Cloud mode](/hub/account-and-fleet). *** ## Next Steps > **[Installation](/latest/quick-start/installation)** > > Install the broker and HUB, and make your first connection. > **[Managing Connectors](/hub/routes/overview)** > > Configure connectors, including per-connector 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. ![A titled Zone 1 Status card containing a temperature readout, a status indicator, and a Details button](https://mintcdn.com/coreflux/ry-30sFxFVWzN72U/images/dashboard/components/card-full.png?fit=max\&auto=format\&n=ry-30sFxFVWzN72U\&q=85\&s=6f70e4f99fc925a6f366c02adce28287) A card grouping an asset's readout, status, and action into one unit on the panel grid 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. > [!TIP] > > **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. **Basic** A titled card with one readout inside: ![A titled Zone 1 Status card containing a temperature readout](https://mintcdn.com/coreflux/ry-30sFxFVWzN72U/images/dashboard/components/card-basic.png?fit=max\&auto=format\&n=ry-30sFxFVWzN72U\&q=85\&s=35f61413a5bb1c975c70b82b6ed1fdb0) A titled card grouping a single temperature readout ```lot wrap theme={"theme":{"light":"css-variables","dark":"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" ``` **Full example** A card combining a readout, an indicator, and a navigation button — the two extra children are the new parts: ![A titled Zone 1 Status card with a temperature value, a status indicator, and a Details button](https://mintcdn.com/coreflux/ry-30sFxFVWzN72U/images/dashboard/components/card-full.png?fit=max\&auto=format\&n=ry-30sFxFVWzN72U\&q=85\&s=6f70e4f99fc925a6f366c02adce28287) A Zone 1 Status card with a temperature readout, a status indicator, and a Details button ```lot wrap focus={9-17} theme={"theme":{"light":"css-variables","dark":"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. > [!NOTE] > > 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. **Basic** ![A tabbed container switching between sub-views on one panel](https://mintcdn.com/coreflux/ry-30sFxFVWzN72U/images/dashboard/components/tabs.png?fit=max\&auto=format\&n=ry-30sFxFVWzN72U\&q=85\&s=190e1e6a76ff7681e1b9e88d412482db) A tab strip switching between sub-views on one panel (current build labels tabs from their content) ```lot wrap theme={"theme":{"light":"css-variables","dark":"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 > **[Display components](/hub/dashboards/components/display)** > > The readouts you place inside cards and tabs. > **[Common patterns](/hub/dashboards/patterns)** > > 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. ![A dashboard panel showing several display widgets: a temperature gauge, a pressure readout, a green machine status indicator, a status text line, and a live throughput chart](https://mintcdn.com/coreflux/ry-30sFxFVWzN72U/images/dashboard/other/display-showcase.png?fit=max\&auto=format\&n=ry-30sFxFVWzN72U\&q=85\&s=348c7d4807b7d8847dcdae57138286c7) A showcase panel mixing the display widgets on this page: a gauge, a value readout, an indicator, text, and a live chart 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. > [!TIP] > > **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. **Basic** A radial gauge bound to a temperature topic: ![Radial gauge filled about two-thirds with an orange arc, reading 82.0 °C](https://mintcdn.com/coreflux/ry-30sFxFVWzN72U/images/dashboard/components/gauge.png?fit=max\&auto=format\&n=ry-30sFxFVWzN72U\&q=85\&s=9c3c9dd97bd0c144c93478b5cfbbebb4) A radial gauge bound to a temperature topic, reading 82.0 °C ```lot wrap theme={"theme":{"light":"css-variables","dark":"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" ``` **Full example** A gauge with a label, one decimal place, and two configured thresholds: ![Radial gauge labeled Production Rate showing the live production rate](https://mintcdn.com/coreflux/ry-30sFxFVWzN72U/images/dashboard/components/gauge-full.png?fit=max\&auto=format\&n=ry-30sFxFVWzN72U\&q=85\&s=c2fff65712d01bc3505ca6ee6882f1f2) A labeled production-rate gauge with warning and critical thresholds configured ```lot wrap theme={"theme":{"light":"css-variables","dark":"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. **Basic** A pressure readout with two decimals: ![Numeric value readout showing 3.42 bar](https://mintcdn.com/coreflux/ry-30sFxFVWzN72U/images/dashboard/components/value-basic.png?fit=max\&auto=format\&n=ry-30sFxFVWzN72U\&q=85\&s=ce3c932fe08a63c8a6d1528570477a5d) A numeric readout showing the live pump pressure in bar ```lot wrap theme={"theme":{"light":"css-variables","dark":"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 ``` **A JSON Data example** Extracting one field from a JSON MQTT payload — `JSON_PATH` and the label are the new parts: ![Large numeric value readout showing 1284.50 Wh extracted from a JSON payload](https://mintcdn.com/coreflux/ry-30sFxFVWzN72U/images/dashboard/components/value.png?fit=max\&auto=format\&n=ry-30sFxFVWzN72U\&q=85\&s=548c92c9d46401efb14f5eb377c66b9d) The value widget showing 1284.50 Wh, extracted from a JSON payload field ```lot wrap focus={3,5} theme={"theme":{"light":"css-variables","dark":"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` > [!NOTE] > > 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`. **Basic** A static heading rendered at large size: ![A large static text line reading Zone 1 Overview](https://mintcdn.com/coreflux/ry-30sFxFVWzN72U/images/dashboard/components/text-basic.png?fit=max\&auto=format\&n=ry-30sFxFVWzN72U\&q=85\&s=4f14a1b82c246e6d1d6ef0fef794a6d4) A static section heading rendered at large size ```lot wrap theme={"theme":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} ADD COMPONENT "SectionTitle" WITH TYPE "label" SET LABEL "Zone 1 Overview" SET SIZE "large" ``` **Full example** Dynamic text whose content updates live from the broker — the `BIND CONTENT` line is what makes it live: ![A large text line showing a live status message from the system message topic](https://mintcdn.com/coreflux/ry-30sFxFVWzN72U/images/dashboard/components/text-live.png?fit=max\&auto=format\&n=ry-30sFxFVWzN72U\&q=85\&s=2900e1241db40d85f0dcb642093e2b85) A large status line whose text updates live from the system message topic ```lot wrap focus={2} theme={"theme":{"light":"css-variables","dark":"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 | | `MARKDOWN` | Boolean | `false` | Render the content as sanitized markdown. Raw HTML, images, and unsafe links are removed | `SET MARKDOWN true` formats a bound briefing or note. A full panel is in [Markdown text](/latest/lot-language/panels/defining-panels#markdown-text). **Bind targets:** `CONTENT`, `COLOR`, `TITLE`, `LABEL` ## Indicator Maps incoming values to colors — running/stopped/fault at a glance. **Basic** A pump-running dot — green when the bound topic reads `true`: ![A green pump status indicator dot](https://mintcdn.com/coreflux/ry-30sFxFVWzN72U/images/dashboard/components/indicator-basic.png?fit=max\&auto=format\&n=ry-30sFxFVWzN72U\&q=85\&s=74db3c3c0743e148f76a6c05d8234978) A status dot that turns green while the pump-running topic reads true ```lot wrap theme={"theme":{"light":"css-variables","dark":"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" ``` **Full example** Three states mapped to three colors, rendered large — the extra mapping and size are highlighted: ![A large green machine status indicator dot for the running state](https://mintcdn.com/coreflux/ry-30sFxFVWzN72U/images/dashboard/components/indicator-large.png?fit=max\&auto=format\&n=ry-30sFxFVWzN72U\&q=85\&s=d03616ddc371adef4b1c5e5fe2c66780) A large green dot for running; switches to red or yellow as the status topic changes ```lot wrap theme={"theme":{"light":"css-variables","dark":"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. **Basic** One topic, one trace — every published value becomes a point: ![A realtime line chart labeled Line Throughput trending as values arrive](https://mintcdn.com/coreflux/ry-30sFxFVWzN72U/images/dashboard/components/realtime-chart-basic.png?fit=max\&auto=format\&n=ry-30sFxFVWzN72U\&q=85\&s=a44d88f1acd67db81e2173eb9de35619) A realtime-chart building a live trend point by point from the throughput topic ```lot wrap theme={"theme":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} ADD COMPONENT "ThroughputTrend" WITH TYPE "realtime-chart" SET LABEL "Line Throughput" BIND VALUE TO TOPIC "factory/throughput" ``` **Full example** The same idea with an explicit chart type, a y-axis unit, and a series label — the additions are highlighted: ![A realtime line chart labeled Message Count Trend building a live trend as points arrive](https://mintcdn.com/coreflux/ry-30sFxFVWzN72U/images/dashboard/components/realtime-chart-full.png?fit=max\&auto=format\&n=ry-30sFxFVWzN72U\&q=85\&s=5b5353a0f8f39c764ece8f5bd1f10db2) A fully configured realtime-chart trending the message count ```lot wrap theme={"theme":{"light":"css-variables","dark":"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" ``` > [!NOTE] > > **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. **Basic** A static image loaded from a URL: ![An image component on a dashboard panel rendering a remote picture](https://mintcdn.com/coreflux/ry-30sFxFVWzN72U/images/dashboard/components/image-url.png?fit=max\&auto=format\&n=ry-30sFxFVWzN72U\&q=85\&s=da5b8a9d7ff2ff782a718deb9de8303a) An image component rendering a remote URL on the panel ```lot wrap theme={"theme":{"light":"css-variables","dark":"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 ``` **Full example** A live feed whose source URL is published over MQTT — the `BIND SRC` line replaces the static source: ![An image component rendering a picture whose URL was published to a camera snapshot topic](https://mintcdn.com/coreflux/ry-30sFxFVWzN72U/images/dashboard/components/image-topic.png?fit=max\&auto=format\&n=ry-30sFxFVWzN72U\&q=85\&s=cd78420f09d93731dfb671f9a6e6f0e3) The image swaps live whenever a new source is published to the snapshot topic ```lot wrap theme={"theme":{"light":"css-variables","dark":"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. **Basic** Each array element becomes a row; `COLUMNS` picks and orders the fields: ![A table with time, barcode, product, and status columns showing recent scans](https://mintcdn.com/coreflux/ry-30sFxFVWzN72U/images/dashboard/components/table.png?fit=max\&auto=format\&n=ry-30sFxFVWzN72U\&q=85\&s=623c8fc914491f4a61a47c0e610b972e) A four-column table populated from a JSON array topic ```lot wrap theme={"theme":{"light":"css-variables","dark":"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. **Basic** ![A list widget showing alarm entries from the zone1 alarms topic](https://mintcdn.com/coreflux/ry-30sFxFVWzN72U/images/dashboard/components/list.png?fit=max\&auto=format\&n=ry-30sFxFVWzN72U\&q=85\&s=4995d6e427f28360e88a22108d9ba3b5) A scrollable list of alarm entries from a topic ```lot wrap theme={"theme":{"light":"css-variables","dark":"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. **Basic** The banner appears while the alarm flag is true and shows the live message: ![An alert banner showing a pump pressure critical message](https://mintcdn.com/coreflux/ry-30sFxFVWzN72U/images/dashboard/components/alert.png?fit=max\&auto=format\&n=ry-30sFxFVWzN72U\&q=85\&s=e7dcc7473dfdd5371642458dddfb02f6) A banner that appears while the alarm flag is true and shows the live message ```lot wrap theme={"theme":{"light":"css-variables","dark":"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" ``` > [!NOTE] > > 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 > **A property set with WITH is silently ignored** > > 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 value widget shows a JSON blob instead of a number** > > 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 > **[Input & control components](/hub/dashboards/components/inputs)** > > Buttons, toggles, sliders, and fields that write back to the broker. > **[Data binding](/latest/lot-language/panels/data-binding)** > > 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. ![A two-column operator panel with Start and Emergency Stop buttons, an Enable Production toggle, a setpoint slider, and device name and pressure fields](https://mintcdn.com/coreflux/ry-30sFxFVWzN72U/images/dashboard/other/inputs-overview.png?fit=max\&auto=format\&n=ry-30sFxFVWzN72U\&q=85\&s=9507498bcc35f2564a2ced1d606e5b0d) An operator panel built from the components on this page: buttons, a toggle, a slider, and entry fields 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. > [!TIP] > > **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 **Basic** A button that publishes a command when clicked: ![A Start Production button on a dashboard](https://mintcdn.com/coreflux/ry-30sFxFVWzN72U/images/dashboard/components/button.png?fit=max\&auto=format\&n=ry-30sFxFVWzN72U\&q=85\&s=cd6b3b083b4e42d4d4d0c42ce1976ef1) A button that publishes a start command when clicked ```lot wrap theme={"theme":{"light":"css-variables","dark":"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 ``` **Full example** A danger-styled emergency stop — the `STYLE` line is the new part: ![A red danger-styled Emergency Stop button](https://mintcdn.com/coreflux/ry-30sFxFVWzN72U/images/dashboard/components/button-danger.png?fit=max\&auto=format\&n=ry-30sFxFVWzN72U\&q=85\&s=cf98eec449ec0e9b9bdda516a671b4db) A red danger-styled Emergency Stop button ```lot wrap focus={3} theme={"theme":{"light":"css-variables","dark":"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 | | `AWAIT` | Quoted component ids | — | Comma-separated ids that show a pending indicator until their next message. The button stays busy | | `AWAIT_TIMEOUT` | Seconds | `120` | How long to wait before a timeout note | `SET AWAIT "Briefing, StatusLamp"` keeps the button busy and marks those components pending until each one receives its next message. A full panel is in [Wait on a button](/latest/lot-language/panels/actions-events#wait-on-a-button). **Events:** `ON CLICK DO` ## Toggle / Switch Binary on/off control. `switch` is an alias for `toggle`. **Basic** The toggle reflects the bound topic live — publish `true` or `false` to it and the switch follows: ![An Enable Production toggle switched on](https://mintcdn.com/coreflux/ry-30sFxFVWzN72U/images/dashboard/components/toggle.png?fit=max\&auto=format\&n=ry-30sFxFVWzN72U\&q=85\&s=4c0f81d53f5c464097468abe793ed9c2) A labeled toggle reflecting the live state of its bound topic ```lot wrap theme={"theme":{"light":"css-variables","dark":"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` > [!WARNING] > > **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. **Basic** The slider tracks its bound topic live — the thumb moves as new values arrive: ![A temperature setpoint slider set partway along its 0 to 200 degree range](https://mintcdn.com/coreflux/ry-30sFxFVWzN72U/images/dashboard/components/slider-basic.png?fit=max\&auto=format\&n=ry-30sFxFVWzN72U\&q=85\&s=182df2e5cbb0cbbef6964d442f81ac6c) A 0–200 °C setpoint slider tracking the live value of its bound topic ```lot wrap theme={"theme":{"light":"css-variables","dark":"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` > [!WARNING] > > **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. **Basic** A labeled field pre-filled from its topic; typing publishes the new value: ![A Device Name text field showing pump-station-1](https://mintcdn.com/coreflux/ry-30sFxFVWzN72U/images/dashboard/components/text-field.png?fit=max\&auto=format\&n=ry-30sFxFVWzN72U\&q=85\&s=c726cca5e1f6c4fce03fdfa93bc56cfb) A labeled text field pre-filled with the current value from its topic ```lot wrap theme={"theme":{"light":"css-variables","dark":"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. **Basic** Accepts only numbers and publishes the limit on change: ![A numeric input field for a maximum pressure limit](https://mintcdn.com/coreflux/ry-30sFxFVWzN72U/images/dashboard/components/number-field.png?fit=max\&auto=format\&n=ry-30sFxFVWzN72U\&q=85\&s=d1ed70b5baf478df5766d0fc7c7537f3) A numeric field that accepts only numbers and publishes the limit on change ```lot wrap theme={"theme":{"light":"css-variables","dark":"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 > **I typed in a field but nothing was published** > > 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 flips back on its own** > > 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 > **[Actions & events](/latest/lot-language/panels/actions-events)** > > Everything a control can do: publish, navigate, invoke, trigger, and branch. > **[Container components](/hub/dashboards/components/containers)** > > 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. ![Animation showing dashboard widgets updating in real time as MQTT topic values change on the broker](https://mintcdn.com/coreflux/EldKmEArSuwejnkZ/images/dashboard/manager/hub-dashboards-live-update.gif?s=4cc30da855182c2b17ae960b90b4a980) Live data updates flow into widgets as broker values change ## 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. ![Coreflux HUB Dashboards window in Preview grid view with Panels 1, a live thumbnail card for Solar energy site · Energy twin with Public and Published badges and a timestamp, and the List view, Preview grid, Dashboards Builder, themes, create, and refresh controls](https://mintcdn.com/coreflux/9PekAdlvyyrySLnc/images/dashboard/manager/hub-dashboards-manager-grid-light.png?fit=max\&auto=format\&n=9PekAdlvyyrySLnc\&q=85\&s=aef54227e6a3c8be9758849e570cfeb3) Dashboard Manager in Preview grid view: each panel shows a live thumbnail with its name, visibility, state, and last update ## 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. > **[Dashboards in the HUB](/hub/dashboards/overview)** > > The widget tour, the 60-second first panel, and what dashboards can do. > **[Defining a panel](/latest/lot-language/panels/defining-panels)** > > 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 (Message Queuing Telemetry Transport — lightweight messaging for devices and applications) topic that already carries the signal from your machines, databases, or Connectors. When that topic’s value changes, the widget updates. You do not configure polling or manual refresh for normal operation. ![Coreflux HUB with the Dashboards window in the background and the Solar energy site · Energy twin panel open in a floating window showing a 3D site view with live labels and Site power cards for PV, Site load, Grid, Battery, and Fleet state of charge](https://mintcdn.com/coreflux/9PekAdlvyyrySLnc/images/dashboard/manager/hub-dashboards-panel-window-light.png?fit=max\&auto=format\&n=9PekAdlvyyrySLnc\&q=85\&s=77e8d1a36d6ecb4db50def201f54106a) A panel opened in its own window above the Dashboard Manager, with the dock still visible ## Viewing a Panel You can experience the same panel in more than one layout depending on where you need it. **As a window** Open the panel as a **floating window** on top of the HUB. That keeps the rest of the HUB—connectors, Data Viewer, configuration—one click away while you watch live values or try layout changes. ![Coreflux HUB with the KPI Operational Dashboard expanded across the workspace, showing Status reading good, a Message Count trend line chart, and a Message Per Minute gauge reading 196](https://mintcdn.com/coreflux/EldKmEArSuwejnkZ/images/dashboard/manager/hub-dashboards-panel-window-tab.png?fit=max\&auto=format\&n=EldKmEArSuwejnkZ\&q=85\&s=d873dc2aeabab536d9befb54a73c895a) Panel as a floating window over the HUB — live widgets stay visible while you work elsewhere **Full screen** 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. ![Clean full-screen view of the KPI Operational Dashboard with no HUB chrome, showing Status reading good, a Message Count line chart, and a Message Per Minute gauge reading 174 on a dark background](https://mintcdn.com/coreflux/EldKmEArSuwejnkZ/images/dashboard/manager/hub-dashboards-panel-fullscreen.png?fit=max\&auto=format\&n=EldKmEArSuwejnkZ\&q=85\&s=1a5638f3918fefa04c4b66f948a4962f) Panel in full view — a clean surface for wall displays and shared links ## Managing Panels The toggle in the Panels header switches between **List view** and **Preview grid**. The HUB remembers your choice; the list is the default. In the grid, each card shows a live thumbnail of the dashboard, and hovering it shows the same quick actions. Thumbnails don't run `ON MOUNT` actions, and cameras, microphones, video, web views, and barcode scanners show a placeholder. 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 | ![Coreflux HUB Dashboards app showing the KPI Operational Dashboard row with Public and Published badges and three icons on the right for opening as a link, viewing source code, and deleting the dashboard](https://mintcdn.com/coreflux/EldKmEArSuwejnkZ/images/dashboard/manager/hub-dashboards-manager-actions.png?fit=max\&auto=format\&n=EldKmEArSuwejnkZ\&q=85\&s=6fcf9d368243527c211a8c60ffa9ff91) Panel row with quick actions — open as link, view source, and delete ## 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 connectors, 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. > [!TIP] > > **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 ![Coreflux HUB Dashboard Manager highlighting the external link action on a panel row to open a shareable dashboard URL](https://mintcdn.com/coreflux/EldKmEArSuwejnkZ/images/dashboard/manager/hub-dashboards-external-link.png?fit=max\&auto=format\&n=EldKmEArSuwejnkZ\&q=85\&s=828c67a5b2cc460c3f9ef25244116ba3) Open the panel as a shareable URL for TVs, kiosks, and external displays > [!WARNING] > > Deleting a panel from the list removes that dashboard permanently after confirmation. ## Best Practices > **Name panels by audience** > > Use names that tell you who it is for (for example “Line 3 — Shift Lead”) so the list stays scannable. > **Start small** > > Ship a minimal panel with the three signals people ask for most, then add depth once it is in daily use. > **Match symbols to reality** > > Use pumps, valves, and tanks where operators already think in those terms; use charts where trends matter more than mimic layout. > **Verify before you share** > > Check labels, ranges, and safety-related controls before handing the panel URL to operators — they will rely on this screen. *** ## Next Steps > **[Data Viewer](/hub/mqtt/data-viewer)** > > Browse topics and payloads so you know what to bind each widget to. > **[AI Assistant](/hub/ai-assistant)** > > 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. ![Coreflux dashboard panel Solar energy site · Energy twin with a 3D site scene labelled with live values for grid, containers, factory load, and PV arrays, and Site power cards for PV, Site load, Grid, Battery, a Fleet state of charge gauge, and Peak import cap](https://mintcdn.com/coreflux/Uf6-R5aKoVdKVLMM/images/dashboard/overview-hero-light.png?fit=max\&auto=format\&n=Uf6-R5aKoVdKVLMM\&q=85\&s=d66a730bd8466d29aa748fa31e47a610) A published panel running maximized: a live 3D site view next to KPI cards and a gauge, all bound to broker topics 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. > [!TIP] > > **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 **In the Dashboard Manager** 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: ![The Sensor Monitor panel rendering a single radial gauge bound to the temperature topic](https://mintcdn.com/coreflux/ry-30sFxFVWzN72U/images/dashboard/other/first-panel.png?fit=max\&auto=format\&n=ry-30sFxFVWzN72U\&q=85\&s=48f432ea78be15567897c387e374eac7) The saved panel: a live gauge reading 82.0 °C from the temperature topic ```lot wrap theme={"theme":{"light":"css-variables","dark":"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). **Ask the AI Assistant** 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). > [!NOTE] > > 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 **Gauge** A value with context: range, unit, and thresholds. ![A radial temperature gauge reading 82.0 °C](https://mintcdn.com/coreflux/ry-30sFxFVWzN72U/images/dashboard/components/gauge-threshold.png?fit=max\&auto=format\&n=ry-30sFxFVWzN72U\&q=85\&s=b79957bdaedaf310c62a27d90117ddbb) A radial gauge bound to the temperature topic, with a 75 °C warning threshold configured ```lot wrap theme={"theme":{"light":"css-variables","dark":"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" ``` **Value** The big number — unit, decimals, wall-readable. ![A numeric readout labeled Error Rate showing 1.25 percent](https://mintcdn.com/coreflux/ry-30sFxFVWzN72U/images/dashboard/components/value-error-rate.png?fit=max\&auto=format\&n=ry-30sFxFVWzN72U\&q=85\&s=06d34f7c733788d3397485f270a01bb9) A formatted error-rate readout with two decimals ```lot wrap theme={"theme":{"light":"css-variables","dark":"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" ``` **Indicator** State at a glance — values mapped to colors. ![A green machine status indicator dot for the running state](https://mintcdn.com/coreflux/ry-30sFxFVWzN72U/images/dashboard/components/indicator-status.png?fit=max\&auto=format\&n=ry-30sFxFVWzN72U\&q=85\&s=9553919702a348c0ae388c8b11f18c0f) A status dot that turns green for running and red for stopped ```lot wrap theme={"theme":{"light":"css-variables","dark":"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" ``` **Alert** A banner that appears when a condition is true. ![An alert banner showing a pump pressure critical message](https://mintcdn.com/coreflux/ry-30sFxFVWzN72U/images/dashboard/components/alert.png?fit=max\&auto=format\&n=ry-30sFxFVWzN72U\&q=85\&s=e7dcc7473dfdd5371642458dddfb02f6) An alert banner showing the live alarm message while the alarm flag is true ```lot wrap theme={"theme":{"light":"css-variables","dark":"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 **Real-time trend** Builds a live trend from individual data points on a topic. ![A realtime line chart trending as message count points arrive](https://mintcdn.com/coreflux/ry-30sFxFVWzN72U/images/dashboard/components/realtime-chart-full.png?fit=max\&auto=format\&n=ry-30sFxFVWzN72U\&q=85\&s=5b5353a0f8f39c764ece8f5bd1f10db2) A realtime-chart building a line from values as they arrive ```lot wrap theme={"theme":{"light":"css-variables","dark":"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" ``` **Table** Rows from a JSON array topic or query result. ![A table with time, barcode, product, and status columns showing recent scans](https://mintcdn.com/coreflux/ry-30sFxFVWzN72U/images/dashboard/components/table.png?fit=max\&auto=format\&n=ry-30sFxFVWzN72U\&q=85\&s=623c8fc914491f4a61a47c0e610b972e) A table populated from a JSON array topic ```lot wrap theme={"theme":{"light":"css-variables","dark":"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 **Toggle** On/off switch that tracks the live state of its bound topic. ![A pump enable toggle switched on](https://mintcdn.com/coreflux/ry-30sFxFVWzN72U/images/dashboard/components/toggle-pump.png?fit=max\&auto=format\&n=ry-30sFxFVWzN72U\&q=85\&s=7e5aa0ef6d5fa569af39fbca00c45118) A pump-enable toggle reflecting the live pump state ```lot wrap theme={"theme":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} ADD COMPONENT "PumpControl" WITH TYPE "toggle" SET LABEL "Pump Enable" BIND STATE TO TOPIC "zone1/pump/state" ``` **Button** Click to act — publish a command to any topic, or navigate to another panel. ![A Start Production button on a dashboard](https://mintcdn.com/coreflux/ry-30sFxFVWzN72U/images/dashboard/components/button.png?fit=max\&auto=format\&n=ry-30sFxFVWzN72U\&q=85\&s=cd6b3b083b4e42d4d4d0c42ce1976ef1) A button that publishes a start command when clicked ```lot wrap theme={"theme":{"light":"css-variables","dark":"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 ``` **Slider** Numeric value tracked live on a range. ![A conveyor speed slider set to 65 percent](https://mintcdn.com/coreflux/ry-30sFxFVWzN72U/images/dashboard/components/slider-speed.png?fit=max\&auto=format\&n=ry-30sFxFVWzN72U\&q=85\&s=d28f7e799225dc20c6812dfa4b9b5ead) A 0–100 % speed slider tracking the live conveyor speed ```lot wrap theme={"theme":{"light":"css-variables","dark":"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 Connectors. 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 > **[Build your first dashboard](/latest/quick-start/building-dashboards)** > > A 10-minute guided walk from topic to published panel. > **[Component reference](/hub/dashboards/components/display)** > > 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. ![A KPI dashboard with a Message Count gauge, a rising Message Count Trend line chart, and an Error Rate readout](https://mintcdn.com/coreflux/ry-30sFxFVWzN72U/images/dashboard/overview-hero.png?fit=max\&auto=format\&n=ry-30sFxFVWzN72U\&q=85\&s=269c34293a365f1c597cbcd399f12cb8) The Live KPI dashboard recipe from this page, running with live data Each recipe is a complete, working panel. Copy it, swap the topics for yours, and adjust ranges and labels. > [!TIP] > > **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. ![A dashboard with a temperature gauge reading 82 °C and a green running status indicator](https://mintcdn.com/coreflux/ry-30sFxFVWzN72U/images/dashboard/patterns/minimal-monitoring.png?fit=max\&auto=format\&n=ry-30sFxFVWzN72U\&q=85\&s=5b513f764cd48a4f2b056f1424912b00) Two-column panel: a temperature gauge on the left, a running status indicator on the right ```lot wrap theme={"theme":{"light":"css-variables","dark":"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. ![A KPI dashboard with a Message Count gauge at 135 msg, a rising Message Count Trend line chart, and an Error Rate readout of 1.25 percent](https://mintcdn.com/coreflux/ry-30sFxFVWzN72U/images/dashboard/patterns/live-kpi-dashboard.png?fit=max\&auto=format\&n=ry-30sFxFVWzN72U\&q=85\&s=69894d551b403fd7d46c5e3b612ac7af) Three-column KPI wall: a message-count gauge, its live trend, and the error-rate readout ```lot wrap theme={"theme":{"light":"css-variables","dark":"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. ![A two-column operator panel with Start and Stop Conveyor buttons, a speed setpoint number field, and a conveyor speed slider](https://mintcdn.com/coreflux/ry-30sFxFVWzN72U/images/dashboard/patterns/control-panel.png?fit=max\&auto=format\&n=ry-30sFxFVWzN72U\&q=85\&s=7bf104e56761bbaae30067a2eab551dc) Operator panel with start/stop buttons, a speed setpoint field, and a live conveyor speed slider ```lot wrap theme={"theme":{"light":"css-variables","dark":"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. > [!WARNING] > > **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":{"light":"css-variables","dark":"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 > **[Build your first dashboard](/latest/quick-start/building-dashboards)** > > Walk through one of these panels end to end with live data. > **[Dashboards in the HUB](/hub/dashboards/overview)** > > 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 ![Coreflux HUB LoT Editor showing digital-twin-energy-site badged LIVE with energy-site.lotnb, a markdown cell titled Solar energy site · Modbus TCP + BACnet/IP, a DEFINE ROUTE TwinEnergyModbusU1 cell badged DEPLOYED, BROKER RUNTIME with type counts, CONFIGURATION, LIBRARY, a Deployed chip, main branch, and Deploy All](https://mintcdn.com/coreflux/Uf6-R5aKoVdKVLMM/images/lot-editor/hub-lot-editor-light.png?fit=max\&auto=format\&n=Uf6-R5aKoVdKVLMM\&q=85\&s=99cf12e43faa20ef201525799789f24a) The LoT Editor with the live project's notebook open: a markdown introduction above deployed connector cells, Projects lists the project badged LIVE, Broker runtime shows its entities, and the header shows Deployed plus Deploy All ## 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. > [!NOTE] > > **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. > [!TIP] > > **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. ![Animated walkthrough of clicking the LoT Editor icon in the Coreflux HUB dock to open the editor](https://mintcdn.com/coreflux/7tBTxEEkDVX4IRGH/images/lot-editor/hub-lot-editor-open.gif?s=db008f31858c416cad334d6e604c5daf) Opening LoT Editor from the HUB dock: the notebook-and-pen icon is fourth from the left, and the editor opens above the dock The window header carries the current project and notebook plus the controls that act on the whole notebook: ![Coreflux HUB LoT Editor window above the dock, a template project's notebook.lotnb in the breadcrumb, 12 not running chip, orange Deploy All, Projects listing the project as local, Library Components 28, LoT Editor dock icon active](https://mintcdn.com/coreflux/7tBTxEEkDVX4IRGH/images/lot-editor/hub-lot-editor-opening.png?fit=max\&auto=format\&n=7tBTxEEkDVX4IRGH\&q=85\&s=cc046a052474a3e221d3b07ddd1a867d) LoT Editor open on a template project with the dock still visible: breadcrumb project › notebook.lotnb, 12 not running, Deploy All, and the notebook-and-pen dock icon lit with an orange dot | Control | What it does | | - | - | | **Breadcrumb** (`project › 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 CONNECTOR`, `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. A project installed from a template can use this for a **live document preview** — widgets on a topic such as `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. > [!NOTE] > > **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. ![Coreflux HUB LoT Editor TwinEnergyProjectKpi cell hovered with the DEPLOYED badge and toolbar controls, DEFINE ACTION TwinEnergyProjectKpi ON EVERY 1 SECOND reading energy-site topics, and the strip TwinEnergyProjectKpi Not yet run with Open in Action Console](https://mintcdn.com/coreflux/Uf6-R5aKoVdKVLMM/images/lot-editor/hub-lot-editor-cell-light.png?fit=max\&auto=format\&n=Uf6-R5aKoVdKVLMM\&q=85\&s=b484791c5871dd84fb153a09bb321933) TwinEnergyProjectKpi hovered and deployed: drag handle, Run cell, LoT, DEPLOYED, then Run or trace, Move up, Move down, Duplicate, and Delete, with the run strip below | 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 | | **Enabled** switch (one ACTION, CONNECTOR, or AGENT per cell) | Turns the entity off or on without removing it — see [Disable an entity](#disable-an-entity) | | **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). ### Disable an entity A cell that holds exactly one action, connector, or agent has an **Enabled** switch in its toolbar. Switch it off to write `DISABLE true` on the line after `DEFINE`. Switch it on to remove that line. The broker keeps a disabled entity defined and listed, but doesn't run it. ![Coreflux HUB LoT Editor with the acme-bottling project open on its Plant actions section: the Heartbeat cell, publishing acme/plant/heartbeat every 5 seconds, with its Enabled switch on, and the AlarmRelay cell with its switch off labelled Disabled, the line DISABLE true under DEFINE ACTION AlarmRelay, and the result Action AlarmRelay added successfully (disabled)](https://mintcdn.com/coreflux/Uf6-R5aKoVdKVLMM/images/lot-editor/hub-lot-editor-cell-enabled-light.png?fit=max\&auto=format\&n=Uf6-R5aKoVdKVLMM\&q=85\&s=a739ffa917c06f8dbde8a943695ef557) Two Action cells: Heartbeat with the Enabled switch on, and AlarmRelay switched off, which writes DISABLE true into its code * With a broker connected, the cell is redeployed right away, so the notebook and the broker stay in step. * Offline, the change stays in the cell and goes out with the next deploy. * Models, rules, and cells with several entities have no switch. * On a broker that doesn't support `DISABLE`, the switch is locked and its tooltip says **Requires a broker with DISABLE support**. See [Disable and Enable](/latest/mqtt-broker/commands#disable-and-enable) for the matching broker commands. ## 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**. ![Coreflux HUB LoT Editor SampleProcessor console on the Traces tab showing no messages yet, timeline playback controls, and copy that traces record every execution for five minutes](https://mintcdn.com/coreflux/7tBTxEEkDVX4IRGH/images/lot-editor/hub-lot-editor-action-console.png?fit=max\&auto=format\&n=7tBTxEEkDVX4IRGH\&q=85\&s=027470dfafc40dda6eba53940e30548e) SampleProcessor Traces tab before a capture: timeline chrome and the note that traces last five minutes while a single run lives on the Run tab | 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 | ![Coreflux HUB LoT Editor SampleProcessor console on the Run tab with topic demo/sensor/value, payload 60, Use dropdown, and Run once play button](https://mintcdn.com/coreflux/7tBTxEEkDVX4IRGH/images/lot-editor/hub-lot-editor-run-once.png?fit=max\&auto=format\&n=7tBTxEEkDVX4IRGH\&q=85\&s=4ca68d1b354953da18067f01396c08a9) SampleProcessor Run tab: topic demo/sensor/value, payload 60, Use menu, and the orange Run once control > [!WARNING] > > **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 | ![Coreflux HUB LoT Editor showing notebook cells badged NOT RUNNING with Compare with broker controls, and a header chip reading a not-running count](https://mintcdn.com/coreflux/7tBTxEEkDVX4IRGH/images/lot-editor/hub-lot-editor-cell-states.png?fit=max\&auto=format\&n=7tBTxEEkDVX4IRGH\&q=85\&s=089fde4d4225dc42e24d8c7ca069c82a) Cells that are not on the live project are badged NOT RUNNING — the header chip counts them, and Compare with broker is available on each toolbar > [!NOTE] > > `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: ![Coreflux HUB LoT Editor showing SampleMqttBridge in diff mode, header BROKER · SampleMqttBridge vs LOCAL, CLIENT\_ID changed from bridge-client to bridge, PROJECTS Sample LIVE, orange 1 differs chip and main branch](https://mintcdn.com/coreflux/7tBTxEEkDVX4IRGH/images/lot-editor/hub-lot-editor-cell-diff.png?fit=max\&auto=format\&n=7tBTxEEkDVX4IRGH\&q=85\&s=3de709341eaca5dfc8b572f533da8eb0) A cell in diff mode with the header 1 differs: BROKER · SampleMqttBridge ⟷ LOCAL, the removed CLIENT\_ID bridge-client in red and the local CLIENT\_ID bridge in green 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":{"light":"css-variables","dark":"css-variables"},"languages":{"custom":["/languages/lot.json"]}} DEFINE MODEL SampleTableRow COLLAPSED ADD DOUBLE "value" ADD STRING "status" ADD STRING "timestamp" ``` > [!WARNING] > > 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. *** ## Start from a template A new project starts with an empty `notebook.lotnb`. Worked examples come from **LoT Templates**, which browses the library at [templates.coreflux.org](https://templates.coreflux.org). The built-in Sample notebook is not part of the HUB. Open **LoT Templates** from **Help → LoT Templates**, or choose **From a template…** in the new-project dialog that `+ New` opens. Each template has three tabs: | Tab | What you see | | - | - | | **Overview** | What the template is for | | **Notebook** | The notebook cells, read-only | | **Flow** | How the LoT in the template fits together | Installing a template creates a LoT Editor project on this HUB. It does not deploy to the broker. Change the example values, then deploy the cells the same way you deploy any notebook. ## Clone from git **+ New → From git…** copies a public git repository into a new LoT Editor project. The clone copies files only. It does not load the project, and it does not deploy any cell. Open the notebook and run the cells you want on the broker. | Limit | What it means | | - | - | | Public repositories | The HUB does not accept private credentials | | 30 seconds | The git clone stops if it has not finished | | Whole repository | The repository root becomes the project root | | 64 MB | A project download larger than 64 MB is refused | *** ## 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. ![Coreflux HUB LoT Editor sidebar with PROJECTS listing a template project LIVE, notebook.lotnb, Entities 12, and its models; BROKER RUNTIME listing that project with type counts; CONFIGURATION Variables Secrets Internal Rules 35; LIBRARY Components 28 Resources 0; header Deployed and main](https://mintcdn.com/coreflux/7tBTxEEkDVX4IRGH/images/lot-editor/hub-lot-editor-sidebar.png?fit=max\&auto=format\&n=7tBTxEEkDVX4IRGH\&q=85\&s=6a109a8c2d3c7e1c37fcce28f40c5e2a) The LoT Editor sidebar: PROJECTS lists a template project badged LIVE with notebook.lotnb and Entities 12, BROKER RUNTIME shows that project with Actions, Models, Rules, Connectors, Python, Themes, and Panels, then CONFIGURATION and LIBRARY | 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 with an empty `notebook.lotnb`. **From a template…** opens [LoT Templates](#start-from-a-template). **From git…** clones a public repository ([Clone from git](#clone-from-git)). 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: {name}`** for the live project, 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`, `Connectors`, `Agents`, `Things`, `Python`, `Themes`, `Panels` — each with a count and names beneath. Click **`Project: {name}`** or **Standalone** to open the inspector: ![Coreflux HUB Broker runtime inspector with All 37, Actions 19, Models 9, Rules 0, Routes 9, Python 0, Themes 0, and Panels 0 filters, Filter by name, and a table of running TwinEnergy actions and models, each in project digital-twin-energy-site and notebook energy-site.lotnb](https://mintcdn.com/coreflux/Uf6-R5aKoVdKVLMM/images/lot-editor/hub-lot-editor-broker-runtime-light.png?fit=max\&auto=format\&n=Uf6-R5aKoVdKVLMM\&q=85\&s=b5a941979edc02d71c8aed9a27dc9a4f) Broker runtime inspector: type filters and a table of STATUS, TYPE, NAME, PROJECT, and NOTEBOOK for every entity running on the broker The inspector is where mixed ownership is obvious: rows that belong to the open project point at `notebook.lotnb`; a connector you added on its own, outside any project, 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*. Those counts can disagree — **Connectors (1)** under the project versus **Connectors (2)** on the broker — because a connector added from Connectors Manager is running but not declared in the notebook. An agent (`DEFINE AGENT`) is its own entity. The editor deploys that cell with `-addAgent` and removes it with `-removeAgent`. It does not appear as a connector. The LLM connector it uses does. See [AI Assistant](/hub/ai-assistant) and [AI Connectors and Agents](/latest/lot-language/routes/ai-routes). ### Things in the editor A cell with `DEFINE THING` blocks deploys as one Thing payload. The broker expands it; the HUB never does. See [LoT THING](/latest/lot-language/things/overview). * **Run cell** first checks the cell with `-addThing --dryRun`, together with every template it references in the workspace. Problems show as markers in the code. If they pass, the same payload is applied. `-addThing` updates in place, so a Thing is never removed first. * When you change a template that other Things use, the editor asks **Also redeploy N other Things?** before it applies. * **Broker runtime → Things** lists each root Thing, its instances, and the entities they own. Owned entities are read-only and leave the per-type folders. The inspector table includes Things. A **Templates** subfolder shows which Things use each template. The project's **Entities** folder has a **Things** group too. * On a broker without THING support, THING cells show a red chip, **Requires a broker with THING support**, and don't deploy. * A THING cell in the older syntax offers an update. It previews the changes (`TEMPLATE`, and `{p}` to `{{p}}`) and applies them only when you confirm. **Flow preview** in the editor toolbar opens a **Flow** pane with the [Things view](/hub/lot-flow#things-tree-and-flat-views) of the open notebook, from the same dry run. Click an entity to bring the cell that owns it into view. ![Coreflux HUB LoT Editor on the acme-bottling notebook. Under Broker runtime, the Things folder lists Acme with 16 entities and its line1 and line2 BottlingLine instances, Utilities with 4, and Templates with 3. The notebook shows the cell with DEFINE THING Acme and DEFINE THING Utilities, and the read-only Flow preview pane, built from the broker's dry run, shows All Things with 2 root Things: MQTT clients feeding the Acme and Utilities cards, and the Acme alarms going to Not in a Thing](https://mintcdn.com/coreflux/Uf6-R5aKoVdKVLMM/images/lot-editor/hub-lot-editor-things-flow-preview-light.png?fit=max\&auto=format\&n=Uf6-R5aKoVdKVLMM\&q=85\&s=8ece8ec2230b2c9213e0906dcf63b9ac) Broker runtime → Things with the Acme root expanded, and the Flow pane previewing the open notebook's Things 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. > [!NOTE] > > **Connectors and Python scripts deploy from the editor too.** Connectors still have their own [Connectors 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. ![Coreflux HUB LoT Editor sidebar scrolled to the Library with Components expanded, listing Gauge, Indicator, Value, Text, Header, Card, Container, Chart, Realtime Chart, Table, List, Button, Switch, Toggle, Slider, Input, Text Field, Number Field, Select, Date Picker, Map, Markers, Trace, SVG, Image, Video, Camera, Audio, Barcode, Web View, and Twin Controls with group labels, and Resources below](https://mintcdn.com/coreflux/Uf6-R5aKoVdKVLMM/images/lot-editor/hub-lot-editor-library-full-light.png?fit=max\&auto=format\&n=Uf6-R5aKoVdKVLMM\&q=85\&s=f4bd7a802f93c7ad6d703b80687c24dc) Library open with Components expanded: visual component types listed with their group (display, data viz, input, maps and graphics, media, digital twin), then Resources ### 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. A template project's live preview is this capability in use: a card of gauge, status, and trend sitting in the document. ![Coreflux HUB LoT Editor markdown cell titled Live document preview with a Sensor overview card showing a gauge at 65.0, status normal, and a live trend chart](https://mintcdn.com/coreflux/7tBTxEEkDVX4IRGH/images/lot-editor/hub-lot-editor-live-preview.png?fit=max\&auto=format\&n=7tBTxEEkDVX4IRGH\&q=85\&s=f56fd16e7097462257c1bcddb944c846) Live document preview in a template project: Sensor overview card with a gauge at 65.0, status normal, and a live trend on demo/sensor/value 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. ![Coreflux HUB LoT Editor Library Resources with Documents expanded to site-notes.md and Add file, Images collapsed with count 1](https://mintcdn.com/coreflux/7tBTxEEkDVX4IRGH/images/lot-editor/hub-lot-editor-library.png?fit=max\&auto=format\&n=7tBTxEEkDVX4IRGH\&q=85\&s=a48e3215b3a464fdf2d32b28d696edfb) Library Resources: Documents lists site-notes.md with Add file…, Images shows a count of 1 | 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: {name}`** (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). ![Context menu for the digital-twin-energy-site project in the Coreflux HUB LoT Editor with New notebook, New folder, Import .lotnb, Import file, Push project to broker, Pull project from broker, Version history, Inspect, Activate, Deactivate, Rename, Delete local copy, and Remove project from broker in red](https://mintcdn.com/coreflux/Uf6-R5aKoVdKVLMM/images/lot-editor/hub-lot-editor-project-menu-light.png?fit=max\&auto=format\&n=Uf6-R5aKoVdKVLMM\&q=85\&s=b1de7710d3bd7dbab1546706ab06d1ae) The project context menu: notebooks and import, Push and Pull, Version history, Inspect, Activate and Deactivate, then Rename, Delete local copy, and Remove project from broker | 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: ![Context menu on the Broker runtime project in the Coreflux HUB LoT Editor with Adopt into project, Open in LoT Flow, Download all entities to notebook, and Remove all entities from broker in red](https://mintcdn.com/coreflux/Uf6-R5aKoVdKVLMM/images/lot-editor/hub-lot-editor-broker-menu-light.png?fit=max\&auto=format\&n=Uf6-R5aKoVdKVLMM\&q=85\&s=4478729d7cf78baf27d769cf01f2b94a) The Broker runtime context menu: Adopt into project, Open in LoT Flow, Download all entities to notebook, and Remove all entities from broker | 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: ![Coreflux HUB version history dialog for digital-twin-energy-site on main at ba3e485, explaining that history comes from the broker's git repository with auto-commit while active, RECENT COMMITS listing ba3e485 Initial project commit with Restore, a Branch / tag / commit to checkout field, and Checkout, Pull, Push, and refresh buttons](https://mintcdn.com/coreflux/Uf6-R5aKoVdKVLMM/images/lot-editor/hub-lot-editor-version-history-light.png?fit=max\&auto=format\&n=Uf6-R5aKoVdKVLMM\&q=85\&s=2f5e689648b640e271af79f91991accd) Version history for digital-twin-energy-site on main: the note that history comes from the broker's git repository and that entity changes auto-commit while the project is active, recent commits with Restore, and Checkout, Pull, and Push 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 > [!NOTE] > > **Entity changes auto-commit while the project is active.** You do not commit by hand — deploying an entity produces a commit like `Update connector: BacnetPortoHq`, which is why the history reads as a log of deployments. > [!WARNING] > > 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**: ![Context menu on the Models entity type group in the Coreflux HUB LoT Editor Broker runtime showing Download all models to notebook… and Remove all models… in red](https://mintcdn.com/coreflux/Uf6-R5aKoVdKVLMM/images/lot-editor/hub-lot-editor-entity-menu-light.png?fit=max\&auto=format\&n=Uf6-R5aKoVdKVLMM\&q=85\&s=23d5170f2ac72fbff7fbdc6c18350ed1) The context menu on the Models group offers Download all models to notebook… and a red Remove all models… 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 connector(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 | > [!WARNING] > > **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. ![Coreflux HUB dialog titled Remove project from broker in red, warning that this permanently deletes BMS from the broker including all its files and its entire git history and cannot be undone, with a Type BMS to confirm field and a greyed-out Remove permanently button beside Cancel](https://mintcdn.com/coreflux/7tBTxEEkDVX4IRGH/images/lot-editor/hub-lot-editor-remove-project.png?fit=max\&auto=format\&n=7tBTxEEkDVX4IRGH\&q=85\&s=dd4382034bd5b27a3afc4c80930feba1) The broker project removal dialog: the description spells out that the git history goes too, and Remove permanently stays disabled until the project name is typed into the field 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. > [!NOTE] > > 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 > **[LoT Flow](/hub/lot-flow)** > > See the entities you just deployed as a live dependency graph. > **[VS Code LoT Notebooks](/lot-notebooks/usage)** > > 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, Connectors, and topics deployed on your broker as a live dependency graph ![Coreflux HUB LoT Flow with the Entities rail listing 19 Actions and 9 Models, and a canvas showing topic groups such as u20/ and u1/ feeding TwinEnergyModbus route nodes badged MODBUS\_TCP, whose output groups connect to Action nodes like TwinEnergyTrackContext ON TOPIC and TwinEnergyProjectKpi ON EVERY 1 SECOND](https://mintcdn.com/coreflux/Uf6-R5aKoVdKVLMM/images/lot-flow/hub-lot-flow-light.png?fit=max\&auto=format\&n=Uf6-R5aKoVdKVLMM\&q=85\&s=a2e62ee81f95645778dd07f7971d8937) LoT Flow for an energy-site project: write topics feed MODBUS\_TCP connectors, their topic groups fan out to ON TOPIC and ON EVERY Actions, and the Entities rail lists Actions and Models ## Why Use LoT Flow? Once you have more than a handful of Actions, Models, and Connectors 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. > [!NOTE] > > 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. > [!TIP] > > **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 Connector 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`**. > [!NOTE] > > 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. Groups start collapsed; a search opens every group with a match: | Group | Contents | | - | - | | **Actions** | Event-driven logic, each badged with its trigger type | | **Models** | Data schemas that map topic values into structured documents | | **Connectors** | Connections to external systems, badged with the connector type | | **Agents** | `DEFINE AGENT` agents on the broker | | **Things** | Root Things and their instances, when the broker has any | | **Python** | The individual Python functions your Actions call | Selecting an entity focuses it on the canvas and opens its detail panel. > [!NOTE] > > 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. ## Things, Tree, and Flat Views When the broker has at least one root Thing, a toggle at the top-right of the canvas offers three views. Things is the default, and the HUB remembers your choice. | View | What it shows | | - | - | | **Things** | One card per Thing and one line per pair of parties that exchange data. Drill in one level at a time, for example `All Things › Plant › line1 › oven`. Ports stand for whatever lies outside the current level. Entities that belong to no Thing are grouped as **Not in a Thing** | | **Tree** | The whole containment tree on one canvas: `All Things`, the roots, then every instance. Each node shows `ROOT` or `OF