> ## Documentation Index
> Fetch the complete documentation index at: https://docs.coreflux.org/llms.txt
> Use this file to discover all available pages before exploring further.

# ACTION Triggers

> 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](/v2.14/lot-language/models/publish-model)). The timed-action examples below use that pattern.

### 1. `ON EVERY` *N* *UNIT* — fixed interval

Fires repeatedly every *N* units of time. Common for monitoring, polling, and heartbeats.

```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
DEFINE ACTION <Name>
ON EVERY <N> <UNIT> 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":"css-variables","languages":{"custom":["/languages/lot.json"]}}
DEFINE ACTION SystemHeartbeat
ON EVERY 5 SECONDS DO
    PUBLISH TOPIC "system/heartbeat" WITH "alive"
    PUBLISH TOPIC "system/heartbeat/timestamp" WITH TIMESTAMP "UTC"
```

```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
DEFINE ACTION PollMachineTemperature
ON EVERY 30 SECONDS DO
    SET "temp" WITH GET TOPIC "machine/sensor/temperature"
    IF {temp} > 85 THEN
        PUBLISH TOPIC "alerts/temperature/high" WITH {temp}
    PUBLISH TOPIC "machine/temperature/last_check" WITH TIMESTAMP "UTC"
```

```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
DEFINE ACTION FastSensorSample
ON EVERY 500 MS DO
    PUBLISH TOPIC "sensors/vibration/sample" WITH GET TOPIC "sensors/vibration/raw"
```

### 2. `ON EVERY` *DAY* `AT` — weekly, specific day

Fires once per week on the named weekday at the given **local** time. Use `"HH:mm"` or `"HH:mm:ss"`; seconds are optional.

| Keyword | Fires on |
| - | - |
| `MONDAY` … `SUNDAY` | That weekday each week |

```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
DEFINE ACTION <Name>
ON EVERY MONDAY AT "08:00:00" DO
    -- body
```

```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
DEFINE MODEL ProductionWeeklyReport COLLAPSED
    ADD STRING "week"
    ADD INT "total_parts"
    ADD INT "total_rejects"

DEFINE ACTION WeeklyProductionReport
ON EVERY MONDAY AT "08:00:00" DO
    SET "week_total" WITH GET TOPIC "factory/line1/parts_produced"
    SET "week_rejects" WITH GET TOPIC "factory/line1/rejects"
    PUBLISH MODEL ProductionWeeklyReport TO "reports/weekly/production" WITH
        week = TIMESTAMP "UTC"
        total_parts = {week_total}
        total_rejects = {week_rejects}
    PUBLISH TOPIC "factory/line1/parts_produced" WITH 0
    PUBLISH TOPIC "factory/line1/rejects" WITH 0
```

### 3. `ON EVERY WEEKDAY AT` — Monday–Friday

Fires Monday through Friday at the same **local** time—one action instead of five separate daily schedules.

```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
DEFINE MODEL ShiftMorningBriefing COLLAPSED
    ADD STRING "line_status"
    ADD STRING "pending_orders"
    ADD STRING "shift_start"

DEFINE ACTION ShiftStartBriefing
ON EVERY WEEKDAY AT "07:30" DO
    SET "line_status" WITH GET TOPIC "factory/line1/status"
    SET "pending_orders" WITH GET TOPIC "erp/orders/pending"
    PUBLISH MODEL ShiftMorningBriefing TO "shift/briefing/morning" WITH
        line_status = {line_status}
        pending_orders = {pending_orders}
        shift_start = TIMESTAMP "UTC"
```

### 4. `ON EVERY WEEKEND AT` — Saturday and Sunday

Fires on both weekend days at the specified **local** time—useful for maintenance windows or weekend-only reporting.

```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
DEFINE ACTION WeekendMaintenanceWindow
ON EVERY WEEKEND AT "06:00:00" DO
    PUBLISH TOPIC "maintenance/window/open" WITH "true"
    PUBLISH TOPIC "maintenance/window/start" WITH TIMESTAMP "UTC"
    PUBLISH TOPIC "notifications/maintenance" WITH "Weekend maintenance window is now open"
```

### 5. `ON TIMESTAMP` — daily fixed time

Fires **every day** at the given **local** wall-clock time. Unlike `ON EVERY 1 DAY`, this tracks clock time, not elapsed time since the broker or last run started.

```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
DEFINE ACTION <Name>
ON TIMESTAMP "08:00:00" DO
    -- body
```

`"HH:mm"` without seconds is accepted.

```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
DEFINE ACTION DailyCounterReset
ON TIMESTAMP "00:00:00" DO
    PUBLISH TOPIC "factory/line1/parts_produced" WITH 0
    PUBLISH TOPIC "factory/line1/rejects" WITH 0
    PUBLISH TOPIC "factory/line1/day_start" WITH TIMESTAMP "UTC"
```

### 6. `ON TIMESTAMP` — one-shot, exact date and time

Fires **once** at the exact instant given, then never again (the scheduler moves the next run far into the future). Format is strictly **`dd-MM-yyyy HH:mm:ss`** (day–month–year). The timestamp is treated as **UTC**.

```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
DEFINE ACTION SystemCutoverNotice
ON TIMESTAMP "01-04-2026 06:00:00" DO
    PUBLISH TOPIC "system/cutover/notice" WITH "New firmware version going live in 1 hour"
    PUBLISH TOPIC "notifications/all" WITH "Scheduled maintenance begins at 07:00 UTC"
```

### Combination patterns

Real systems often split schedules across several actions—one trigger per concern.

> **Fast poll + daily summary**
>
> ```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
> DEFINE MODEL DailyTemperatureReport COLLAPSED
>     ADD DOUBLE "max"
>     ADD DOUBLE "min"
>     ADD STRING "date"
>
> DEFINE ACTION SensorPoll
> ON EVERY 10 SECONDS DO
>     SET "value" WITH GET TOPIC "sensors/temperature/raw"
>     PUBLISH TOPIC "sensors/temperature/current" WITH {value}
>
> DEFINE ACTION DailySensorSummary
> ON TIMESTAMP "00:00:00" DO
>     SET "max" WITH GET TOPIC "sensors/temperature/max"
>     SET "min" WITH GET TOPIC "sensors/temperature/min"
>     PUBLISH MODEL DailyTemperatureReport TO "reports/daily/temperature" WITH
>         max = {max}
>         min = {min}
>         date = TIMESTAMP "UTC"
>     PUBLISH TOPIC "sensors/temperature/max" WITH 0
>     PUBLISH TOPIC "sensors/temperature/min" WITH 0
> ```

> **Weekday operations + weekend maintenance**
>
> ```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
> DEFINE ACTION WeekdayProductionStart
> ON EVERY WEEKDAY AT "06:00" DO
>     PUBLISH TOPIC "factory/line1/mode" WITH "production"
>     PUBLISH TOPIC "factory/line1/shift_start" WITH TIMESTAMP "UTC"
>
> DEFINE ACTION WeekendMaintenance
> ON EVERY WEEKEND AT "07:00" DO
>     PUBLISH TOPIC "factory/line1/mode" WITH "maintenance"
>     PUBLISH TOPIC "notifications/maintenance/start" WITH TIMESTAMP "UTC"
> ```

> **Weekly reset + daily snapshot + live throughput**
>
> ```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
> DEFINE MODEL DashboardThroughput COLLAPSED
>     ADD INT "parts"
>     ADD DOUBLE "rate_per_min"
>     ADD INT "updated"
>
> DEFINE ACTION WeeklyKpiReset
> ON EVERY MONDAY AT "00:00:00" DO
>     PUBLISH TOPIC "kpi/weekly/parts_produced" WITH 0
>     PUBLISH TOPIC "kpi/weekly/reset_time" WITH TIMESTAMP "UTC"
>
> DEFINE ACTION DailyKpiSnapshot
> ON TIMESTAMP "23:59:00" DO
>     SET "daily_parts" WITH GET TOPIC "factory/line1/parts_produced"
>     PUBLISH TOPIC "kpi/daily/snapshot" WITH {daily_parts}
>     PUBLISH TOPIC "kpi/daily/snapshot_time" WITH TIMESTAMP "UTC"
>
> DEFINE ACTION LiveThroughput
> ON EVERY 1 MINUTE DO
>     SET "parts" WITH GET TOPIC "factory/line1/parts_produced"
>     SET "rate" WITH GET TOPIC "factory/line1/production_rate"
>     PUBLISH MODEL DashboardThroughput TO "dashboard/live/throughput" WITH
>         parts = {parts}
>         rate_per_min = {rate}
>         updated = TIMESTAMP "UNIX"
> ```

> **One-shot cutover + ongoing heartbeat**
>
> ```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
> DEFINE ACTION CutoverActivation
> ON TIMESTAMP "01-07-2026 06:00:00" DO
>     PUBLISH TOPIC "system/version" WITH "2.0"
>     PUBLISH TOPIC "system/cutover/complete" WITH TIMESTAMP "UTC"
>
> DEFINE ACTION V2Heartbeat
> ON EVERY 30 SECONDS DO
>     SET "ver" WITH GET TOPIC "system/version"
>     IF {ver} EQUALS "2.0" THEN
>         PUBLISH TOPIC "system/v2/heartbeat" WITH "running"
> ```

### Error handling

If a time string does not match an expected format, the broker logs an error similar to:

`Invalid DATE_TIME format: <value>. 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/<ActionName>/Error`

Subscribe to `$SYS/Coreflux/Actions/+/Error` to observe failures from any action.

### At a glance — valid time triggers

```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
ON EVERY 500 MS DO
ON EVERY 10 SECONDS DO
ON EVERY 5 MINUTES DO
ON EVERY 1 HOUR DO
ON EVERY 1 DAY DO
ON EVERY 1 WEEK DO
ON EVERY 1 MONTH DO
ON EVERY MONDAY AT "08:00:00" DO
ON EVERY WEEKDAY AT "07:30" DO
ON EVERY WEEKEND AT "06:00" DO
ON TIMESTAMP "07:00" DO
ON TIMESTAMP "00:00:00" DO
ON TIMESTAMP "25-12-2026 09:00:00" DO
```

### More examples

> **System heartbeat**
>
> ```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
> DEFINE ACTION SystemHeartbeat
> ON EVERY 5 SECONDS DO
>     PUBLISH TOPIC "system/status" WITH "online"
>     PUBLISH TOPIC "system/timestamp" WITH TIMESTAMP "UTC"
> ```

> **Counter with state**
>
> ```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
> DEFINE ACTION IncrementCounter
> ON EVERY 10 SECONDS DO
>     IF GET TOPIC "counter/value" == EMPTY THEN
>         PUBLISH TOPIC "counter/value" WITH 0
>     ELSE
>         PUBLISH TOPIC "counter/value" WITH (GET TOPIC "counter/value" + 1)
> ```

***

## Topic-Based Triggers

MQTT-driven actions use a **topic trigger** — the condition that tells the broker *when* to run the action body. Both triggers share the same topic patterns and wildcards; the difference is whether repeated identical payloads should be ignored.

| Trigger | When the action runs |
| - | - |
| **`ON TOPIC`** | On **every** message that matches the topic pattern |
| **`ON CHANGE`** | When the message matches the pattern **and** the payload is **different** from the last message on that same topic |

### `ON TOPIC` — react to every message

Use `ON TOPIC` when each publish matters: commands, alarms, counters, or any flow where duplicate payloads still need processing.

```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
DEFINE ACTION ProcessTemperature
ON TOPIC "sensors/+/temperature" DO
    SET "sensor_id" WITH TOPIC POSITION 2
    SET "temp" WITH PAYLOAD AS DOUBLE
    PUBLISH TOPIC "processed/" + {sensor_id} + "/temperature" WITH {temp}
```

**Key characteristics:**

* Any client (or route) that publishes to a matching topic runs the action.
* Publishing the same value twice runs the action **twice**.
* Wildcard `+` matches one topic level (same rules as [MQTT wildcards](/v2.14/mqtt-broker/protocol#wildcards)).

### `ON CHANGE` — react only when the value changes

Use `ON CHANGE` for telemetry, status fields, or setpoints that are published often but only need logic when the reading actually changes. This reduces CPU load, database writes, and downstream noise.

```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
DEFINE ACTION ProcessTemperatureOnChange
ON CHANGE "sensors/+/temperature" DO
    SET "sensor_id" WITH TOPIC POSITION 2
    SET "temp" WITH PAYLOAD AS DOUBLE
    PUBLISH TOPIC "processed/" + {sensor_id} + "/temperature" WITH {temp}
```

**Key characteristics:**

* The **first** message on a given topic always runs the action (there is no previous value yet).
* Later messages run the action only if the **payload bytes** are not identical to the last message on that **exact** topic name.
* Example: `sensors/line1/temperature` and `sensors/line2/temperature` are tracked separately.
* Wildcard patterns work the same as `ON TOPIC`; change is evaluated per real topic, not per pattern.

> [!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":"css-variables","languages":{"custom":["/languages/lot.json"]}}
DEFINE ACTION LogEveryReading
ON TOPIC "sensors/+/temperature" DO
    PUBLISH TOPIC "logs/all" WITH PAYLOAD
```

On value change only:

```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
DEFINE ACTION ProcessWhenDifferent
ON CHANGE "sensors/+/temperature" DO
    SET "temp" WITH PAYLOAD AS DOUBLE
    PUBLISH TOPIC "analytics/updated" WITH {temp}
```

| Publish to | Payload | `LogEveryReading` | `ProcessWhenDifferent` |
| - | - | - | - |
| `sensors/a/temperature` | `22.5` | Runs | Runs (first value) |
| `sensors/a/temperature` | `22.5` | Runs | Skipped (unchanged) |
| `sensors/a/temperature` | `23.1` | Runs | Runs (changed) |

Inside the action body, **`PAYLOAD`** is the content of the message that fired the trigger, and **`TOPIC POSITION N`** is segment *N* of the actual topic (1-based), useful with `+`.

### Wildcard Patterns

LoT supports MQTT wildcards for flexible topic matching:

**Single-Level (+)**

The `+` wildcard matches exactly one topic level:

```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
DEFINE ACTION SensorRouter
ON TOPIC "sensors/+/temperature" DO
    // POSITION 1="sensors", POSITION 2=wildcard (sensor_id), POSITION 3="temperature"
    SET "sensor_id" WITH TOPIC POSITION 2
    PUBLISH TOPIC "processed/" + {sensor_id} + "/temp" WITH PAYLOAD
```

> [!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":"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":"css-variables","languages":{"custom":["/languages/lot.json"]}}
DEFINE ACTION InitUNSRoot
ON START DO
    KEEP TOPIC "config/uns_root" WITH "factory/lisbon"
    KEEP TOPIC "factory/info/name" WITH "Lisbon Plant"
    KEEP TOPIC "factory/info/timezone" WITH "Europe/Lisbon"
    PUBLISH TOPIC "system/log" WITH "UNS root initialised"
```

**Key characteristics:**

* Runs once when the broker starts, and again when you first deploy (or redeploy) the action while the broker is already up — you do not need a restart to seed bootstrap topics
* Cold-start behaviour is unchanged: every `ON START` action still runs once after the MQTT broker is ready. Deploying before the broker is ready defers until startup so the body is not double-run
* A failing `ON START` body is logged but does not fail the deploy
* `KEEP TOPIC` publishes with the retain flag so the value is delivered to any future subscriber immediately on connection
* Use it to set default configuration values and retained state that must survive restarts
* There is no `ON STOP` equivalent — `ON START` is the correct place for setup logic

> [!NOTE]
>
> Action `PUBLISH` and `KEEP` are ordinary MQTT publishes: the payload is on the topic and any client subscribed to that topic receives it. What they skip is the **publish ACL** that applies to connected MQTT users — a restrictive rule on that topic does not stop the action from publishing, so bootstrap topics from `ON START` (and other actions) are not silently dropped. Clients still need subscribe permission to receive those messages, and anything a client publishes itself remains fully subject to rules.

***

## Client Lifecycle Triggers

Use **`ON CONNECT`**, **`ON DISCONNECT`**, and **`ON SUBSCRIBE`** when automation must react to MQTT session events instead of publishes. Typical uses include audit trails, connection dashboards, onboarding retained topics when a UI client connects, or tracking which topics clients subscribe to.

| Trigger | When the action runs |
| - | - |
| **`ON CONNECT DO`** | After a client passes authentication and the broker accepts the MQTT CONNECT packet |
| **`ON DISCONNECT DO`** | When the broker detects a client disconnection (clean, unclean, or session takeover) |
| **`ON SUBSCRIBE ["pattern"] DO`** | After the broker accepts a subscription (RBAC check passed); optional pattern limits which subscriptions fire the action |

> [!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":"css-variables","languages":{"custom":["/languages/lot.json"]}}
DEFINE ACTION AuditConnect
ON CONNECT DO
    PUBLISH TOPIC "audit/connect/" + CLIENTID WITH USER + "@" + ENDPOINT
    KEEP TOPIC "sessions/" + CLIENTID + "/clean" WITH CLEANSESSION
```

**Key characteristics:**

* Fires for every successful client connection, including routes and HUB clients.
* Session context variables (`CLIENTID`, `USER`, `ENDPOINT`, `CLEANSESSION`) reflect the connecting client.
* `USER IN GROUP`, `USER EQUALS`, and `USER HAS` evaluate against the session user, the same way they do for `ON TOPIC`.

### `ON DISCONNECT DO`

Runs when the broker detects a client disconnection — clean disconnect, unclean disconnect, or session takeover.

```lot wrap focus={2} theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
DEFINE ACTION AuditDisconnect
ON DISCONNECT DO
    PUBLISH TOPIC "audit/disconnect/" + CLIENTID WITH DISCONNECTREASON
```

**Key characteristics:**

* `DISCONNECTREASON` carries the disconnect type or MQTT reason code for the session that ended.
* `CLIENTID`, `USER`, and `ENDPOINT` reflect the disconnecting session when available from the connect handshake.

### `ON SUBSCRIBE DO`

Runs after the broker accepts a subscription and RBAC allows it. Use a topic pattern to limit which subscriptions fire the action; omit the pattern to run on **every** allowed subscription.

```lot wrap focus={2} theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
DEFINE ACTION TrackSensorSubscriptions
ON SUBSCRIBE "sensors/#" DO
    PUBLISH TOPIC "audit/subscriptions" WITH CLIENTID + " -> " + SUBSCRIBETOPIC + " qos=" + SUBSCRIBEQOS
```

Track every allowed subscription:

```lot wrap focus={2} theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
DEFINE ACTION TrackAllSubscriptions
ON SUBSCRIBE DO
    KEEP TOPIC "audit/last/subscribe" WITH SUBSCRIBETOPIC
```

**Key characteristics:**

* Supports the same MQTT wildcards as `ON TOPIC`: `+` (single level) and `#` (multi-level).
* `SUBSCRIBETOPIC` is the topic filter the client subscribed to; `SUBSCRIBEQOS` is the requested QoS (`0`, `1`, or `2`).
* Lifecycle subscribe dispatch runs for all allowed subscriptions, including Visu and system topics, unless you filter them in the action body.

### Session context variables

Available inside lifecycle-triggered actions (and still available on `ON TOPIC` / `ON CHANGE` for the **publisher**):

| Variable | `ON CONNECT` | `ON DISCONNECT` | `ON SUBSCRIBE` | `ON TOPIC` / `ON CHANGE` |
| - | - | - | - | - |
| `CLIENTID` | Connecting client | Disconnecting client | Subscribing client | Publishing client |
| `USER` | Authenticated username | Session username | Session username | Publisher username |
| `ENDPOINT` | Client address (`ip:port`) | Last known address | Address from connect | Empty |
| `CLEANSESSION` | `"true"` / `"false"` | Value from connect | Value from connect | Empty |
| `DISCONNECTREASON` | Empty | Disconnect type / MQTT reason | Empty | Empty |
| `SUBSCRIBETOPIC` | Empty | Empty | Subscribed topic filter | Empty |
| `SUBSCRIBEQOS` | `-1` | `-1` | QoS level (`0`, `1`, or `2`) | `-1` |

### Not the same as Visu `ON CHANGE`

In **dashboard / Visu (LoTV)** panels, `ON CHANGE DO` on a slider or text field is a **UI event** in the browser (user edits a control). That is separate from broker **`ON CHANGE "topic" DO`**, which reacts to **MQTT publishes** inside the broker.

Lifecycle triggers (`ON CONNECT`, `ON DISCONNECT`, `ON SUBSCRIBE`) are also broker-side only. The broker still publishes `$SYS/Coreflux/Comms/Logins` on connect for observability, but LoT lifecycle actions give you programmatic automation with session context variables.

| Context | Syntax | What fires it |
| - | - | - |
| Broker LoT action | `ON CHANGE "sensors/temp" DO` | MQTT message with changed payload |
| Broker LoT action | `ON CONNECT DO` | MQTT client connects |
| Broker LoT action | `ON DISCONNECT DO` | MQTT client disconnects |
| Broker LoT action | `ON SUBSCRIBE "pattern" DO` | Client subscribes to a matching filter |
| Visu component | `ON CHANGE DO PUBLISH …` | User changing a control in the UI |

***

## Callable Actions

Actions **without** a trigger clause (`ON TOPIC`, `ON EVERY`, `ON START`, `ON CONNECT`, and so on) are **callable**—they run only when another ACTION calls them with `CALL ACTION`.

Use callables for reusable logic (conversions, validation, shared math) that several trigger-based Actions need. They accept typed `INPUT` parameters and can `RETURN` one or more `OUTPUT` values.

> [!NOTE]
>
> **Full guide:** [Callable ACTIONs](./callable-actions)—reusable logic, on-demand scripts, and `CALL ACTION` syntax.

```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
DEFINE ACTION CalculateAverage
INPUT value1 AS DOUBLE
INPUT value2 AS DOUBLE
DO
    SET "avg" WITH (({value1} + {value2}) / 2)
RETURN
    OUTPUT avg

DEFINE ACTION ProcessSensorPair
ON TOPIC "sensors/pair/data" DO
    SET "temp1" WITH (GET JSON "sensor1" IN PAYLOAD AS DOUBLE)
    SET "temp2" WITH (GET JSON "sensor2" IN PAYLOAD AS DOUBLE)
    CALL ACTION CalculateAverage
        WITH value1 = {temp1}, value2 = {temp2}
        RETURN result
    PUBLISH TOPIC "sensors/average" WITH {result}
```

***

## Comparing Trigger Types

| Aspect | Time-Based | Topic-Based | ON START | Client lifecycle | Callable |
| - | - | - | - | - | - |
| **Trigger** | Clock interval or schedule | MQTT message | Broker startup or first deploy | Connect, disconnect, subscribe | Manual command |
| **Frequency** | Interval, daily, weekly, or once | Event-driven | Once | Per session event | On-demand |
| **Payload Access** | No | Yes (`PAYLOAD`) | No | Session context (`CLIENTID`, `USER`, …) | Via parameters |
| **Best For** | Monitoring, reporting | Data processing | Initialization | Audit, session automation | Reusable utilities |

***

## Next Steps

> **[Callable ACTIONs](./callable-actions)**
>
> Reusable logic, on-demand scripts, and `CALL ACTION`.

> **[Operations](./operations)**
>
> Learn about SET, GET, PUBLISH, and conditional logic.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.