Skip to main content

Triggers

A trigger defines what event causes a rule to be evaluated. Every rule has exactly one trigger.

Trigger reference

DeviceStateChanged

Fires when any MQTT state publish arrives for a device.

[trigger]
type = "device_state_changed"
device = "entryway.front_door"

# Optional: only fire when this attribute changes
attribute = "open"

# Optional: only fire when the attribute changes TO this specific value
to = true
FieldRequiredDescription
deviceyes¹Preferred device reference: canonical name, unique display name, or raw device ID.
device_idyes¹Backward-compatible alias for device.
device_ids (alias devices)noAdditional device IDs — fires when any of these changes. When non-empty the primary device is also included in the set.
attributenoNarrow to one attribute (e.g. "on", "open", "temperature").
tonoOnly fire when the new value equals this.
fromnoOnly fire when the previous value equals this.
not_tonoOnly fire when the new value is not this.
not_fromnoOnly fire when the previous value is not this.
for_duration_secsno"Sticky" trigger: only fire after the new value has held for this many seconds.
change_kindnoProvenance filter: homecore, physical, external, or unknown.
change_sourcenoExact-match filter on the change source label (plugin id or rule id).

¹ One of device or device_ids must be set.

If you use a display name and more than one device matches it, HomeCore marks the rule invalid with an ambiguity error.

Examples:

# Fire on any state change for a device
[trigger]
type = "device_state_changed"
device = "hallway.dimmer"

# Fire only when the "on" attribute changes (either direction)
[trigger]
type = "device_state_changed"
device = "living_room.floor_lamp"
attribute = "on"

# Fire only when a door opens (open = true)
[trigger]
type = "device_state_changed"
device = "garage.main_door"
attribute = "open"
to = true

# Multi-device: any of three contact sensors going open
[trigger]
type = "device_state_changed"
device_ids = ["sensor_front", "sensor_back", "sensor_garage"]
attribute = "open"
to = true

# Only fire when the change came from a physical actor (not from a rule)
[trigger]
type = "device_state_changed"
device = "living_room.lamp"
attribute = "on"
change_kind = "physical"

# Sticky: motion has held "true" for at least 30 seconds
[trigger]
type = "device_state_changed"
device = "motion_hallway"
attribute = "motion"
to = true
for_duration_secs = 30

TimeOfDay

Fires at a specific time on specified days of the week. Computed in local wall-clock time.

[trigger]
type = "time_of_day"
time = "07:30" # HH:MM
days = ["Mon", "Tue", "Wed", "Thu", "Fri"]

Valid day names: Mon, Tue, Wed, Thu, Fri, Sat, Sun

Catch-up on restart: If HomeCore restarts after a TimeOfDay trigger's scheduled time, the scheduler fires it immediately if the missed time falls within catchup_window_minutes (default 15).


SunEvent

Fires at sunrise or sunset, computed locally from your latitude/longitude in config.

[trigger]
type = "sun_event"
event = "sunset" # "sunrise" | "sunset"
offset_minutes = -30 # minutes before (-) or after (+) the event

Examples:

# At sunrise
[trigger]
type = "sun_event"
event = "sunrise"

# 30 minutes before sunset
[trigger]
type = "sun_event"
event = "sunset"
offset_minutes = -30

# 15 minutes after sunrise
[trigger]
type = "sun_event"
event = "sunrise"
offset_minutes = 15

Cron

Fires on a repeating cron schedule. Uses 6-field expressions (second, minute, hour, day-of-month, month, day-of-week) evaluated in local wall-clock time.

[trigger]
type = "cron"
expression = "0 30 7 * * Mon-Fri"
# ^ ^ ^ ^ ^ ^
# s m h dom mo dow
ExpressionMeaning
0 0 7 * * *7:00 AM every day
0 30 7 * * Mon-Fri7:30 AM weekdays only
0 */15 * * * *Every 15 minutes
0 0 */2 * * *Every 2 hours
0 0 8 1 * *8:00 AM on the 1st of every month
0 0 20 * * Sat,Sun8:00 PM weekends only

Rules with invalid cron expressions are automatically disabled at startup with an error field set.

# Check for invalid cron rules
curl -s http://localhost:8080/api/v1/automations \
-H "Authorization: Bearer $TOKEN" \
| jq '[.[] | select(.error | strings | contains("cron")) | {name, error}]'

WebhookReceived

Fires when an HTTP POST arrives at /api/v1/webhooks/{path}. No authentication required. The path acts as the shared secret.

[trigger]
type = "webhook_received"
path = "front-door-bell-a3f9c2"

The webhook URL: POST http://homecore.local/api/v1/webhooks/front-door-bell-a3f9c2

The request body (if valid JSON) is forwarded as the body field in the event payload and accessible in ScriptExpression conditions:

# Condition using webhook body
[[conditions]]
type = "script_expression"
script = 'event.body["pin"] == "1234"'

Example — trigger from a cloud service:

# Fire the trigger from any HTTP client — a script, a scheduler, a physical button, etc.
curl -X POST http://homecore.local/api/v1/webhooks/front-door-bell-a3f9c2 \
-H "Content-Type: application/json" \
-d '{"source": "doorbell", "action": "pressed"}'

CustomEvent

Fires when a FireEvent action emits the matching event_type. Enables clean rule chaining — one rule fires an event, other rules react to it in the same process with no MQTT round-trip.

[trigger]
type = "custom_event"
event_type = "morning_routine_started"

This is the primary mechanism for fan-out (one event → many parallel reactions) and pipeline patterns (chain of rules that each do one thing).

Example chain:

# Rule 1: motion sensor → start morning routine
[trigger]
type = "device_state_changed"
device_id = "motion_bedroom"
attribute = "motion"
to = true

[[actions]]
type = "fire_event"
event_type = "morning_routine_started"
payload = {}

# Rule 2: morning routine → turn on coffee
[trigger]
type = "custom_event"
event_type = "morning_routine_started"

[[actions]]
type = "set_device_state"
device_id = "smart_plug_coffee"
state = { on = true }

# Rule 3: morning routine → set thermostat
[trigger]
type = "custom_event"
event_type = "morning_routine_started"

[[actions]]
type = "set_device_state"
device_id = "thermostat_main"
state = { target_temp = 70 }

SystemStarted

Fires once, immediately after the rule engine finishes pre-populating its device cache on startup. Use to catch state that may have changed while HomeCore was offline.

[trigger]
type = "system_started"

Always pair with DeviceState conditions to guard the action:

# Alert if garage door was left open across a restart
[trigger]
type = "system_started"

[[conditions]]
type = "device_state"
device_id = "yolink_garage_door"
attribute = "open"
op = "Eq"
value = true

[[actions]]
type = "notify"
channel = "telegram"
message = "Garage door is OPEN (detected on startup)"

DeviceAvailabilityChanged

Fires when a device comes online or goes offline.

[trigger]
type = "device_availability_changed"
device_id = "hue_001788fffe6841b3_1"

# Optional: only fire in one direction
to = false # only when device goes offline

# Optional: sticky guard — only fire after the new state has held this long
for_duration_secs = 60
FieldRequiredDescription
device_id (alias device)yesDevice to monitor.
tonotrue = online only, false = offline only, omitted = both.
for_duration_secsnoOnly fire after the new availability state has held for this many seconds (debounces flapping devices).

MqttMessage

Fires when a raw MQTT message arrives on a matching topic. Supports MQTT wildcards.

[trigger]
type = "mqtt_message"
topic_pattern = "homecore/devices/+/state" # + = one level
# topic_pattern = "homecore/devices/#" # # = rest of path

Use this for non-standard integrations or when you need to react to any MQTT traffic, not just typed device events.


ManualTrigger

Never fires automatically. Only activated via the /test dry-run endpoint.

[trigger]
type = "manual_trigger"

Useful for rules that should only ever run when explicitly tested.


ModeChanged

Fires when a named mode transitions on or off.

[trigger]
type = "mode_changed"
mode_id = "mode_night" # optional — omit to fire on any mode change
to = true # optional — only when turning on (false = only off)
FieldRequiredDescription
mode_idnoMode device id (e.g. "mode_night"). Omit to match any mode.
tonotrue = only when the mode turns on, false = only when it turns off, omitted = both.

ButtonEvent

Fires when a physical button device (e.g. a Pico remote, keypad, or smart switch) reports a button press.

[trigger]
type = "button_event"
device_id = "lutron_pico_bedroom"
button_number = 2 # optional: specific button number
event = "Pushed" # "Pushed" | "Held" | "DoubleTapped" | "Released"
FieldRequiredDescription
device_id (alias device)yesButton device.
button_numbernoSpecific button number. Omit to match any button on the device.
eventyesOne of Pushed, Held, DoubleTapped, Released.

Implementation note: button events arrive as DeviceStateChanged events with attribute name matching the event type (pushed, held, double_tapped, released) and the button number as the value. The ButtonEvent trigger is a typed wrapper that handles the routing for you.


NumericThreshold

Fires when a numeric attribute crosses a threshold value. Unlike DeviceStateChanged + to, this trigger fires only on the crossing edge (the transition itself), not on every change while the value remains past the threshold.

[trigger]
type = "numeric_threshold"
device_id = "sensor_basement_temp"
attribute = "temperature"
op = "CrossesAbove" # crossing edge only
value = 80.0
for_duration_secs = 60 # optional: debounce
FieldRequiredDescription
device_id (alias device)yesDevice to monitor.
attributeyesNumeric attribute name.
opyes"Above" / "Below" / "CrossesAbove" / "CrossesBelow" (see below).
valueyesThreshold value.
for_duration_secsnoOnly fire if the threshold condition has held for at least this many seconds (debounce).
opFires when
AboveThe new value is > value (every change while above).
BelowThe new value is < value (every change while below).
CrossesAboveThe previous value was ≤ value and the new value is > value (one-shot edge).
CrossesBelowThe previous value was ≥ value and the new value is < value (one-shot edge).

For one-shot alerting (e.g. "fire once when temp goes above 80"), use CrossesAbove / CrossesBelow. For "any sample while we're past the threshold", use Above / Below.


HubVariableChanged

Fires when a hub variable is written via SetHubVariable action or API. The trigger does not filter on values — combine with a HubVariable condition for value-based filtering.

[trigger]
type = "hub_variable_changed"
name = "alarm_armed" # optional — omit to watch all hub vars
FieldRequiredDescription
namenoSpecific variable name. Omit to fire on any hub variable change.

Periodic

Fires at a fixed interval. Simpler than Cron for basic repeating tasks.

[trigger]
type = "periodic"
every_n = 15
unit = "Minutes" # "Minutes" | "Hours" | "Days" | "Weeks"
FieldRequiredDescription
every_nyesInterval count.
unityesMinutes, Hours, Days, or Weeks.

CalendarEvent

Fires based on .ics calendar events. See Advanced: Calendar triggers for full configuration.

[trigger]
type = "calendar_event"
calendar_id = "us_holidays" # optional — stem of the .ics filename
title_contains = "Holiday" # optional — case-insensitive substring
offset_minutes = -30 # fire 30 min before event start
FieldRequiredDescription
calendar_idnoStem of the .ics filename (e.g. "us_holidays" for us_holidays.ics). Omit to match any loaded calendar.
title_containsnoCase-insensitive substring match on the event title. Omit to match any event.
offset_minutesnoNegative = before event start, positive = after, default 0.

DeviceBatteryLow

Fires once when a device's battery percentage crosses the configured low threshold. Hysteresis is enforced by the watcher (latches at threshold_pct, clears at threshold_pct + recover_band_pct), so the trigger fires only on the crossing edge — not on every battery report.

device_id is optional: omit it to match any battery-powered device.

[trigger]
type = "device_battery_low"
# device_id = "yolink_d88b4c0400064299" # optional — omit to match any device

Thresholds live in [battery] in homecore.toml. See Battery monitoring for the full feature, including how to identify which device fired the trigger inside an action.


DeviceBatteryRecovered

Counterpart to DeviceBatteryLow. Fires once when a previously-low device's battery climbs back above threshold_pct + recover_band_pct.

[trigger]
type = "device_battery_recovered"
# device_id = "yolink_d88b4c0400064299" # optional

Trigger context in conditions

For WebhookReceived triggers, the request body is available in ScriptExpression conditions as event.body. For DeviceStateChanged, event.device_id, event.attribute, and event.value are available.

For DeviceBatteryLow and DeviceBatteryRecovered, the firing device's id is exposed to Rhai via trigger_device(). Pair it with device_state(id) inside a RunScript action to read the device name and current battery level.