Skip to main content

Rules Overview

Automation rules are the core of HomeCore. A rule defines what happens when a specific event occurs. Rules are stored as RON (Rusty Object Notation) files in the rules/ directory and hot-reloaded on every file change — no restarts needed.

Device references​

For device-based triggers, conditions, and actions, prefer device = "canonical.name" instead of device_id = "plugin_specific_id".

Preferred:

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

Still supported:

[trigger]
type = "device_state_changed"
device_id = "yolink_front_door"

The device field can contain:

  • a canonical name such as living_room.floor_lamp
  • a unique display name such as Kitchen Speaker
  • a raw device_id if needed

If a display name matches more than one device, HomeCore disables the rule with an explicit ambiguity error instead of guessing.

Data model​

Every rule has exactly three parts:

trigger     — what event causes this rule to be evaluated
conditions — optional checks that ALL must pass (AND logic), can be empty []
actions — what to do (run in sequence by default)

Execution flow​

Event arrives (device state change, time trigger, webhook, etc.)
→ filter rules whose trigger matches the event type
→ sort matching rules by priority (highest first)
→ for each rule:
→ if rule is paused or disabled → skip
→ if cooldown_secs active → record "cooldown" and skip
→ evaluate trigger_condition Rhai gate (if set)
→ evaluate conditions (short-circuit AND — stop at first failure)
→ if all pass → execute actions
→ record full trace in fire history ring buffer
→ StopRuleChain action skips remaining lower-priority rules

Rule file format (RON)​

Rule(
id: "00000000-0000-0000-0000-000000000000", // all zeros = auto-generated on first load
name: "My rule",
enabled: true,
priority: 10, // higher = evaluated first; range [-1000, 1000]
tags: ["lighting", "morning"],

trigger: DeviceStateChanged(
device_id: "yolink_front_door",
attribute: Some("open"),
// to: Some(true), // optional: only fire when attribute changes TO this value
),

conditions: [
TimeWindow(
start: "08:00:00",
end: "22:00:00",
),
],

actions: [
RuleAction(
enabled: true,
action: Notify(
channel: "telegram",
message: "Front door opened",
),
),
],

// Optional fields (shown with defaults):
// cooldown_secs: Some(300),
// run_mode: Single, // Parallel | Single | Restart | Queued { max_queue: 10 }
// trigger_condition: Some("device_state(\"mode_night\")[\"on\"] == true"),
// required_expression: Some("current_hour() >= 6 && current_hour() < 22"),
)

Creating rules via API​

RULE_ID=$(curl -s -X POST http://localhost:8080/api/v1/automations \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"name": "Front door alert",
"enabled": true,
"priority": 10,
"tags": ["security"],
"trigger": {
"DeviceStateChanged": {
"device_id": "entryway.front_door",
"attribute": "open"
}
},
"conditions": [
{"TimeWindow": {"start": "08:00:00", "end": "22:00:00"}}
],
"actions": [
{"enabled": true, "action": {"Notify": {"channel": "telegram", "message": "Front door opened"}}}
]
}' | jq -r .id)

Rules created via API are immediately written to a .ron file in rules/ and take effect instantly.

Hot-reload​

Edit any .ron file in rules/ and save — the rule is reloaded within 200 ms. No API call, no restart.

# Edit a rule directly
vim rules/front_door_alert.ron

# Check it was reloaded (watch the server log)
# INFO hc_core::rule_loader: Hot-reloaded rule "Front door alert"

Run modes​

Every rule has a run_mode that controls how concurrent firings are handled when the rule is triggered while a previous firing is still executing.

Run modeBehavior
ParallelDefault. Multiple firings run concurrently.
SingleOnly one firing at a time. Additional triggers while a firing is in progress are dropped.
RestartA new trigger cancels any in-progress firing and starts fresh.
QueuedFirings queue up and execute sequentially. Optional max_queue limits queue depth.
id       = ""
name = "Motion light — single mode"
enabled = true
run_mode = "single" # parallel | single | restart | queued

# For queued mode, optionally limit the queue
# max_queue = 5

Use Single for idempotent rules where re-firing is wasteful. Use Restart for rules with long delays where a new trigger should reset the sequence. Use Queued when every trigger must be processed but order matters.


Priority​

Rules with higher priority are evaluated before rules with lower priority for the same event. The order matters when using StopRuleChain.

Priority 100 → evaluated first
Priority 10 → evaluated second
Priority 0 → default
Priority -10 → evaluated last

Broken rules​

HomeCore never fails to start because of a bad rule file. A rule that fails to parse becomes a disabled stub:

# Find all broken rules
curl -s http://localhost:8080/api/v1/automations \
-H "Authorization: Bearer $TOKEN" \
| jq '[.[] | select(.error != null) | {name, error}]'

Fix the RON file and save — the watcher replaces the stub automatically.

Key source files​

FileWhat it contains
crates/hc-types/src/rule.rsRule, Trigger, Condition, Action types
crates/hc-core/src/engine.rsTrigger matching, condition evaluation, fire history
crates/hc-core/src/executor.rsAction execution, all action type handlers
crates/hc-core/src/rule_loader.rsRON loading, hot-reload watcher
rules/Live rule files (RON)
rules/examples/Reference patterns and commented examples