# Automation Patterns

This document covers native Home Assistant automation constructs that should be used instead of templates.

## Table of Contents

1. [Native Conditions](#native-conditions)
2. [Trigger Types](#trigger-types)
3. [Wait Actions](#wait-actions)
4. [Automation Modes](#automation-modes)
5. [Continue on Error](#continue-on-error)
6. [Stopping a Sequence](#stopping-a-sequence)
7. [Variables](#variables)
8. [Capturing Action Responses](#capturing-action-responses)
9. [Repeat Actions](#repeat-actions)
10. [if/then vs choose](#ifthen-vs-choose)
11. [Parallel Actions](#parallel-actions)
12. [Trigger IDs](#trigger-ids)
13. [Documenting Automations & Scripts](#documenting-automations--scripts)
14. [Disabling Automations](#disabling-automations)

---

## Native Conditions

### State Condition

The most common condition type. Supports multiple states, attributes, and duration.

```yaml
# Single state
condition: state
entity_id: light.living_room
state: "on"

# Multiple acceptable states (OR logic)
condition: state
entity_id: vacuum.robot
state:
  - "cleaning"
  - "returning"

# Attribute check
condition: state
entity_id: climate.thermostat
attribute: hvac_action
state: "heating"

# Duration check (entity has been in state for X time)
condition: state
entity_id: binary_sensor.motion
state: "off"
for:
  minutes: 5
```

### Numeric State Condition

For numeric comparisons. Always prefer over template conditions with `| float`.

```yaml
# Above threshold
condition: numeric_state
entity_id: sensor.temperature
above: 25

# Below threshold
condition: numeric_state
entity_id: sensor.humidity
below: 30

# Range
condition: numeric_state
entity_id: sensor.battery
above: 20
below: 80

# Attribute numeric check
condition: numeric_state
entity_id: sun.sun
attribute: elevation
below: -6
```

### Time Condition

For time-based restrictions. Handles midnight crossing automatically.

```yaml
# Time range (handles midnight crossing!)
condition: time
after: "22:00:00"
before: "06:00:00"

# Weekday filter
condition: time
weekday:
  - mon
  - tue
  - wed
  - thu
  - fri

# Combined
condition: time
after: "09:00:00"
before: "17:00:00"
weekday:
  - mon
  - tue
  - wed
  - thu
  - fri
```

### Sun Condition

For sunrise/sunset-based logic with optional offsets.

```yaml
# After sunset
condition: sun
after: sunset

# Before sunrise with offset
condition: sun
before: sunrise
before_offset: "01:00:00"

# After sunset by 30 minutes
condition: sun
after: sunset
after_offset: "00:30:00"
```

### Zone Condition

For presence detection based on zones.

```yaml
condition: zone
entity_id: person.john
zone: zone.home

# Or check if NOT in zone
condition: not
conditions:
  - condition: zone
    entity_id: person.john
    zone: zone.home
```

### And/Or/Not Conditions

Combine conditions with logical operators.

```yaml
# AND (default when multiple conditions listed)
condition: and
conditions:
  - condition: state
    entity_id: light.kitchen
    state: "on"
  - condition: numeric_state
    entity_id: sensor.brightness
    below: 100

# OR
condition: or
conditions:
  - condition: state
    entity_id: person.john
    state: "home"
  - condition: state
    entity_id: person.jane
    state: "home"

# NOT
condition: not
conditions:
  - condition: state
    entity_id: alarm_control_panel.home
    state: "armed_away"

# Shorthand syntax
conditions:
  - and:
      - condition: state
        entity_id: input_boolean.guest_mode
        state: "on"
      - condition: time
        after: "08:00:00"
```

### Template Condition Shorthand

When you must use a template, use the shorthand syntax:

```yaml
# Shorthand (preferred when template is necessary)
conditions:
  - "{{ trigger.to_state.attributes.brightness > 100 }}"

# Long form (equivalent)
conditions:
  - condition: template
    value_template: "{{ trigger.to_state.attributes.brightness > 100 }}"
```

### Trigger Condition

Matches _which_ trigger started the run, by trigger `id` — the clean way to branch on the trigger (see [Trigger IDs](#trigger-ids)) instead of templating `trigger.id`. Only true when the run was started by a trigger (false in scripts and manual runs).

```yaml
condition: trigger
id: motion_on # a single id, or a list (OR semantics):
# id:
#   - motion_on
#   - motion_off
```

---

## Trigger Types

### Purpose-Specific Triggers (2026.2+)

The HA visual automation editor offers **purpose-specific triggers and conditions** organized by real-world concepts (door opened, motion detected, temperature threshold, battery low, etc.) rather than by technical entity domain. These work across entity types — e.g., "when a door opens" fires whether the door is a contact sensor or a motorized cover.

**Key points:**

- **Most** are a UI-editor convenience — under the hood they generate standard `state`, `numeric_state`, etc., so the YAML output uses the same triggers documented below
- **Exceptions (2026.6, Labs):** the native zone trigger/condition family serializes as new YAML (see [Native Zone Triggers & Conditions](#native-zone-triggers--conditions-20266-labs)), and the step-level `note:` field is new additive syntax (see [Documenting Automations & Scripts](#documenting-automations--scripts)).
- Available concepts: door/window/gate open/close, motion/occupancy detection, temperature/humidity/illuminance thresholds, power consumption, battery status, air quality, climate, and more
- When creating automations via the API, use the standard YAML trigger types below (plus the zone family above where you want the native any-zone primitives)

### State Trigger

The workhorse trigger. Fires on entity state changes.

```yaml
# Basic state change to specific value
triggers:
  - trigger: state
    entity_id: binary_sensor.motion
    to: "on"

# From specific state
triggers:
  - trigger: state
    entity_id: light.bedroom
    from: "off"
    to: "on"

# Any state change (omit to/from)
triggers:
  - trigger: state
    entity_id: sensor.temperature

# Attribute change
triggers:
  - trigger: state
    entity_id: climate.thermostat
    attribute: current_temperature

# Duration trigger (entity has been in state for X)
triggers:
  - trigger: state
    entity_id: light.porch
    to: "on"
    for:
      minutes: 30

# Multiple entities
triggers:
  - trigger: state
    entity_id:
      - binary_sensor.motion_kitchen
      - binary_sensor.motion_hallway
    to: "on"
```

### Numeric State Trigger

Fires when crossing a threshold.

```yaml
triggers:
  - trigger: numeric_state
    entity_id: sensor.temperature
    above: 25
    for:
      minutes: 5
```

### Time Trigger

Fires at specific times.

```yaml
# Fixed time
triggers:
  - trigger: time
    at: "07:00:00"

# Input datetime helper
triggers:
  - trigger: time
    at: input_datetime.morning_alarm

# Time pattern (every 5 minutes)
triggers:
  - trigger: time_pattern
    minutes: "/5"

# Every hour at :30
triggers:
  - trigger: time_pattern
    minutes: 30
```

### Sun Trigger

Fires at sunrise/sunset with optional offset.

```yaml
triggers:
  - trigger: sun
    event: sunset
    offset: "-00:30:00" # 30 minutes before sunset
```

### Event Trigger

Fires on Home Assistant events.

```yaml
# ZHA button event
triggers:
  - trigger: event
    event_type: zha_event
    event_data:
      device_ieee: "00:11:22:33:44:55:66:77"
      command: "on"

# Custom event
triggers:
  - trigger: event
    event_type: my_custom_event
```

**Multi-trigger guard for `trigger.event`:** In automations mixing event and non-event triggers, `trigger.event` is `LoggingUndefined` for non-event triggers. Attribute access (`.data`, `.split()`) raises `UndefinedError`; `in` on a bare `LoggingUndefined` silently returns `False`. Use `trigger.platform == 'event'` as a short-circuit guard:

```yaml
# AVOID — trigger.event.data raises UndefinedError when a non-event trigger fires
conditions:
  - "{{ 'light.kitchen' in trigger.event.data.entity_id }}"

# CORRECT — guard prevents evaluating trigger.event on non-event triggers
conditions:
  - "{{ trigger.platform == 'event' and 'light.kitchen' in trigger.event.data.entity_id }}"
```

**Firing an event** (the action counterpart of this trigger) — useful for decoupling automations (one fires `my_event`, several others trigger on it):

```yaml
actions:
  - event: my_custom_event
    event_data:
      source: kitchen
      value: "{{ states('sensor.power') }}" # templates work directly; event_data_template is no longer needed
```

### MQTT Trigger

Fires on MQTT messages.

```yaml
triggers:
  - trigger: mqtt
    topic: "zigbee2mqtt/button/action"
    payload: "single"
```

### Device Trigger (Use Sparingly)

Device triggers key off an opaque `device_id` rather than a readable `entity_id`. Prefer entity-based `state` triggers where practical — they're self-documenting and easier to maintain.

```yaml
# Prefer a state trigger on a readable entity_id where practical
triggers:
  - trigger: device
    domain: mqtt
    device_id: abc123
    type: action
    subtype: single
```

A matching **`condition: device`** variant exists (`device_id`, `domain`, `entity_id`, `type`) with the same trade-off — prefer a `state`/`numeric_state` condition on a readable `entity_id` where practical.

### Zone Trigger

Fires when a person or device tracker enters or leaves a zone.

```yaml
triggers:
  - trigger: zone
    entity_id: person.john
    zone: zone.home
    event: enter # "enter" or "leave"
```

To _check_ zone membership in a condition rather than trigger on the transition, see [Zone Condition](#zone-condition).

### Native Zone Triggers & Conditions (2026.6, Labs)

2026.6 adds native, **any-zone** zone primitives (not just home). They are **Labs-gated** in the editor (Settings → System → Labs) but use the standard core config schema, so they round-trip through the config API regardless of the Labs toggle. Unlike most purpose-specific triggers, these serialize as a _new_ `<domain>.<subtype>` YAML shape with `options:` and `target:` blocks:

- **Triggers:** `zone.entered`, `zone.left`, `zone.occupancy_detected`, `zone.occupancy_cleared`
- **Conditions:** `zone.in_zone`, `zone.not_in_zone`, `zone.occupancy_is_detected`, `zone.occupancy_is_not_detected`

```yaml
# Trigger — fires when a tracked person/device enters a zone
triggers:
  - trigger: zone.entered
    target:
      entity_id: person.john # person or device_tracker
    options:
      zone: zone.office
      behavior: each # "each" (default), "all", or "first"
      # for: "00:05:00"           # optional dwell time
```

Triggers take `behavior: each` (default), `all`, or `first`; the matching **conditions** use a _different_ enum — `behavior: any` (default) or `all`. These are Labs keys and may change before they stabilize, so verify against the current schema. The classic flat [Zone Trigger](#zone-trigger) / [Zone Condition](#zone-condition) and plain `state` triggers remain the stable, non-Labs fallback.

> The `entered_home`/`left_home`/`is_home`/`is_not_home` device automations were removed in **2026.5** (see [below](#presence-and-person-triggers-and-conditions-removed-in-20265)) — 2026.6 only _adds_ these any-zone primitives as the native successor.

### Template Trigger

Fires when a Jinja template renders truthy. Prefer `state`/`numeric_state` whenever the change can be expressed natively — reach for the template trigger only for cross-entity or derived conditions.

```yaml
triggers:
  - trigger: template
    value_template: "{{ states('sensor.temp') | float > 25 and is_state('binary_sensor.window', 'on') }}"
    for: "00:05:00" # optional: template must stay true this long before firing
```

### Calendar Trigger

Fires at the start or end of a calendar event, with an optional offset. Prefer this over a `state` trigger on the calendar entity, which only tracks one event at a time.

```yaml
triggers:
  - trigger: calendar
    entity_id: calendar.work
    event: start # "start" or "end"
    offset: "-00:15:00" # optional: fire 15 min before the event
```

Exposes `trigger.calendar_event` with `.summary`, `.start`, `.end`, `.description`, `.location` for filtering by event details.

### Webhook Trigger

Fires when an HTTP request hits `/api/webhook/<webhook_id>`. The canonical way to trigger HA from external systems (scripts, IFTTT, other servers).

```yaml
triggers:
  - trigger: webhook
    webhook_id: "my-secret-hook-id"
    allowed_methods: [POST, PUT] # optional; default POST + PUT
    local_only: true # optional; default true — set false for external callers
```

Read the payload via `trigger.json`, `trigger.data` (form-encoded), `trigger.query`, or `trigger.headers`.

### Home Assistant Trigger

Fires when HA finishes starting or begins shutting down.

```yaml
triggers:
  - trigger: homeassistant
    event: start # "start" or "shutdown"
```

Use `start` for boot-time setup (restore state, resync devices). `shutdown` handlers get only ~20 seconds before HA stops — keep them short.

### Conversation Trigger

Fires when a voice/Assist sentence matches. The match syntax supports `[optional]` words, `(a|b)` alternatives, and `{slot}` wildcards.

```yaml
triggers:
  - trigger: conversation
    command:
      - "party time"
      - "play {album} by {artist}"
```

Captured wildcards are available as `trigger.slots.<name>`; the full text is `trigger.sentence`. To make the assistant speak a dynamic reply, use the `set_conversation_response` action (`- set_conversation_response: "Done"`; `~` clears a previously set response).

### Tag Trigger

Fires when an NFC/QR tag is scanned.

```yaml
triggers:
  - trigger: tag
    tag_id: "A7-6B-90-5F"
    device_id: 0e19cd3c... # optional: limit to one specific scanner device
```

### Geolocation Trigger

Fires when a transient entity from a geolocation feed (NWS alerts, GDACS, fire-service feeds, USGS quakes) enters or leaves a zone. Keyed by the feed `source`, not a fixed `entity_id`.

```yaml
triggers:
  - trigger: geo_location
    source: nsw_rural_fire_service_feed
    zone: zone.fire_alert
    event: enter # "enter" or "leave"
```

### Persistent Notification Trigger

Fires on persistent-notification lifecycle changes (e.g. react to an integration posting an error notice).

```yaml
triggers:
  - trigger: persistent_notification
    update_type: [added, removed] # any of: added, removed, updated, current; omit for all
    notification_id: invalid_config # optional: filter to one notification
```

### Presence and Person Triggers and Conditions (Removed in 2026.5)

The `entered_home`/`left_home` device trigger types and `is_home`/`is_not_home` device condition types for `person` and `device_tracker` domains were **removed in 2026.5**. Use state triggers and conditions instead.

```yaml
# AVOID (removed in 2026.5)
triggers:
  - trigger: device
    domain: person
    type: entered_home
    entity_id: person.john

# CORRECT — state trigger
triggers:
  - trigger: state
    entity_id: person.john
    to: "home"

# CORRECT — state condition
condition: state
entity_id: person.john
state: "home"
```

For non-home zone presence, see `#zone-condition`.

### Timer Entity Triggers (2026.5+)

Timer entities now expose lifecycle events as purpose-specific triggers in the UI editor. In YAML, use the `event` trigger with the corresponding `timer.*` event type.

```yaml
# Timer finished (also: timer.started, timer.paused, timer.restarted, timer.cancelled)
triggers:
  - trigger: event
    event_type: timer.finished
    event_data:
      entity_id: timer.cooking
```

### Media Player Triggers/Conditions (2026.5+)

Media player entities now have purpose-specific triggers and conditions in the UI editor covering play/pause state, mute status, and volume level. In YAML these map to standard state and numeric_state triggers.

```yaml
# Play/pause state — to: "playing", "paused", "idle", "off"
triggers:
  - trigger: state
    entity_id: media_player.living_room
    to: "playing"

# Volume threshold
triggers:
  - trigger: numeric_state
    entity_id: media_player.living_room
    attribute: volume_level
    above: 0.7
```

---

## Wait Actions

### wait_for_trigger (Preferred)

Event-driven wait. More efficient than polling.

```yaml
# Wait for door to close
- wait_for_trigger:
    - trigger: state
      entity_id: binary_sensor.door
      to: "off"
  timeout:
    minutes: 5
  continue_on_timeout: false # Stop automation if timeout

# Wait for any of multiple triggers
- wait_for_trigger:
    - trigger: state
      entity_id: binary_sensor.door
      to: "off"
    - trigger: event
      event_type: mobile_app_notification_action
      event_data:
        action: "CLOSE_DOOR"
```

### wait_template (Use Sparingly)

Polls until template is true. **Immediately continues if already true.**

```yaml
# Only use when wait_for_trigger cannot express the condition
- wait_template: "{{ states('sensor.temperature') | float > 25 }}"
  timeout:
    minutes: 10
```

**Key difference:**

- `wait_for_trigger` waits for a **change** to occur
- `wait_template` waits for a **condition** to be true (passes immediately if already true)

### Checking Wait Results

Both waits set `wait.completed` and `wait.remaining`:

```yaml
- wait_for_trigger:
    - trigger: state
      entity_id: binary_sensor.door
      to: "off"
  timeout:
    minutes: 5

- if:
    - "{{ not wait.completed }}"
  then:
    - action: notify.mobile_app
      data:
        message: "Door still open after 5 minutes!"
```

### delay

Pauses the sequence for a fixed time. Accepts a time string, a units dict, or a template. Prefer `wait_for_trigger` when waiting for an _event_ rather than a fixed duration.

```yaml
- delay: "00:01:30" # HH:MM:SS
- delay: { minutes: 1, seconds: 30 } # units dict (combinable)
- delay: "{{ states('input_number.delay_seconds') | int }}" # template → seconds
```

---

## Automation Modes

The `mode` determines what happens when an automation triggers while already running.

### single (Default)

New triggers are ignored while running. A warning is logged.

**Best for:** One-shot notifications, actions that shouldn't overlap.

```yaml
automation:
  - alias: "Doorbell notification"
    mode: single
    triggers:
      - trigger: state
        entity_id: binary_sensor.doorbell
        to: "on"
    actions:
      - action: notify.mobile_app
        data:
          message: "Someone at the door!"
```

### restart

Stops the current run and starts fresh. Timer-based actions are reset.

**Best for:** Motion-activated lights with timeout, retriggerable delays.

```yaml
mode: restart # Re-trigger resets the timer
```

See `references/examples.yaml` Example 1 for a complete motion-light automation using restart + wait_for_trigger.

### queued

Queues new triggers to run after current run completes.

**Best for:** Sequential actions, door locks, garage doors.

```yaml
automation:
  - alias: "Garage door controller"
    mode: queued
    max: 5 # Maximum queue size
    triggers:
      - trigger: state
        entity_id: input_boolean.garage_door_trigger
        to: "on"
    actions:
      - action: cover.toggle
        target:
          entity_id: cover.garage_door
      - delay:
          seconds: 20 # Wait for door to fully open/close
```

### parallel

Runs multiple instances simultaneously.

**Best for:** Per-entity actions with `trigger.entity_id`, notifications that shouldn't block.

```yaml
automation:
  - alias: "Window open too long"
    mode: parallel
    max: 10 # Maximum parallel runs
    triggers:
      - trigger: state
        entity_id:
          - binary_sensor.window_bedroom
          - binary_sensor.window_kitchen
          - binary_sensor.window_living
        to: "on"
        for:
          minutes: 30
    actions:
      - action: notify.mobile_app
        data:
          message: "{{ trigger.to_state.name }} has been open for 30 minutes"
```

### max_exceeded

Control logging when max runs are exceeded:

```yaml
automation:
  - alias: "Quiet automation"
    mode: single
    max_exceeded: silent # No warning logged
```

---

## Continue on Error

Any automation action can be set to continue execution even if it fails, using the `continue_on_error` key. Since 2026.3, this is also configurable in the visual editor (three-dots menu on any action).

```yaml
actions:
  - action: light.turn_on
    target:
      entity_id: light.patio
    continue_on_error: true # Automation proceeds even if this fails
  - action: notify.mobile_app
    data:
      message: "Light action attempted"
```

**Use sparingly** — silently swallowing errors makes debugging harder. Best for non-critical actions (e.g., logging, optional notifications) where a failure shouldn't block the rest of the automation.

---

## Stopping a Sequence

`stop:` halts the rest of the sequence cleanly — clearer than nesting everything inside a `choose`/`if` guard.

```yaml
- stop: "reason shown in the trace"

# Mark the run as failed (red in the trace, propagates to callers):
- stop: "unexpected state"
  error: true

# Return a value from a script and halt:
- stop: "done"
  response_variable: my_result
```

---

## Variables

Compute a value once and reuse it — keeps sequences DRY and avoids repeating long templates.

```yaml
# Mid-sequence (scoped to the remaining steps of this run):
- variables:
    brightness: 100
    targets:
      - light.kitchen
      - light.living_room

# Automation/script top level (full templates; usable in conditions and actions):
variables:
  threshold: 25
```

`trigger_variables:` is a separate top-level key evaluated **before** triggers fire — it supports **limited templates only** (no `states()`/`state_attr()`), mainly for passing a blueprint `!input` into trigger options. Don't put state-based templates there.

---

## Capturing Action Responses

`response_variable` captures the data a service returns (e.g. `weather.get_forecasts`, `calendar.get_events`, `todo.get_items`) into a variable for later steps — the only native mechanism for response-aware service calls.

```yaml
- action: weather.get_forecasts
  target:
    entity_id: weather.home
  data:
    type: daily
  response_variable: forecast
- action: notify.mobile_app
  data:
    message: "High today: {{ forecast['weather.home'].forecast[0].temperature }}°"
```

---

## Repeat Actions

Four repeat variants are available:

```yaml
# Repeat N times
- repeat:
    count: 3
    sequence:
      - action: light.toggle
        target:
          entity_id: light.bedroom

# Repeat while condition is true
- repeat:
    while:
      - condition: state
        entity_id: binary_sensor.door
        state: "on"
    sequence:
      - action: notify.mobile_app
        data:
          message: "Door still open"
      - delay:
          minutes: 5

# Repeat until condition is true
- repeat:
    until:
      - condition: numeric_state
        entity_id: sensor.temperature
        below: 25
    sequence:
      - delay:
          minutes: 1

# Repeat for each item in a list
- repeat:
    for_each:
      - "light.kitchen"
      - "light.bedroom"
      - "light.hallway"
    sequence:
      - action: light.turn_off
        target:
          entity_id: "{{ repeat.item }}"
```

Access `repeat.index` (1-based) and `repeat.item` (for `for_each`) inside the sequence.

---

## if/then vs choose

### if/then/else

Use for simple binary conditions:

```yaml
actions:
  - if:
      - condition: state
        entity_id: sun.sun
        state: "below_horizon"
    then:
      - action: light.turn_on
        target:
          entity_id: light.porch
    else:
      - action: light.turn_off
        target:
          entity_id: light.porch
```

### choose

Use for multiple branches (like switch/case):

```yaml
actions:
  - choose:
      - conditions:
          - condition: trigger
            id: "morning"
        sequence:
          - action: scene.turn_on
            target:
              entity_id: scene.morning

      - conditions:
          - condition: trigger
            id: "evening"
        sequence:
          - action: scene.turn_on
            target:
              entity_id: scene.evening

    default:
      - action: light.turn_off
        target:
          area_id: living_room
```

---

## Parallel Actions

The `parallel:` action runs a group of actions **concurrently** within one sequence. This is distinct from `mode: parallel` (see [Automation Modes](#automation-modes)), which controls concurrency of whole automation _runs_. Steps inside a nested `sequence:` still run in order.

```yaml
actions:
  - parallel:
      - action: notify.person1
        data:
          message: "Sent at the same time"
      - sequence:
          - wait_for_trigger:
              - trigger: state
                entity_id: binary_sensor.motion
                to: "on"
          - action: notify.person2
```

---

## Trigger IDs

Assign IDs to triggers for use in conditions and choose:

```yaml
automation:
  - alias: "Multi-trigger automation"
    triggers:
      - trigger: state
        entity_id: binary_sensor.motion
        to: "on"
        id: "motion_on"

      - trigger: state
        entity_id: binary_sensor.motion
        to: "off"
        for:
          minutes: 5
        id: "motion_off"

    actions:
      - choose:
          - conditions:
              - condition: trigger
                id: "motion_on"
            sequence:
              - action: light.turn_on
                target:
                  entity_id: light.hallway

          - conditions:
              - condition: trigger
                id: "motion_off"
            sequence:
              - action: light.turn_off
                target:
                  entity_id: light.hallway
```

Access trigger info in templates with `trigger.id`, `trigger.entity_id`, `trigger.to_state`, etc.

---

## Documenting Automations & Scripts

Two fields document _intent_ (the why, not the what):

- **`description:`** — a top-level automation/script field for the overall purpose.
- **`note:`** (2026.6) — a per-block annotation on any individual trigger, condition, or action (including `wait_*`, `choose` branches, `if`/`then`/`else`, `parallel`, `repeat`, and nested `sequence` steps). Scripts have no triggers, so notes there apply to sequence steps and conditions only. The YAML key is the **singular `note:`** (not `notes:`) — the editor surfaces it as a "Notes" field, but the docs never show the key string, so don't guess the plural.

```yaml
description: "Turn on the porch light at dusk; skip if already on."
triggers:
  - trigger: sun
    event: sunset
    note: "Dusk, not full dark — sunset event is ~civil twilight."
actions:
  - action: light.turn_on
    target:
      entity_id: light.porch
    note: "Brightness intentionally left at last value."
```

`note:` is **stored documentation only** — it persists in the saved/edited config but is stripped from the running automation object at runtime, so don't rely on reading it back. Keep notes concise (intent, assumptions, why a threshold/mode/entity was chosen).

---

## Disabling Automations

Home Assistant provides two distinct ways to disable an automation, with different persistence and behavior.

### Method 1: Turn Off (Temporary, State Machine)

`automation.turn_off` disables the automation's configured triggers — it will not fire automatically. The entity remains in the state machine with state `off` and can still be invoked via the `automation.trigger` service.

```yaml
- action: automation.turn_off
  target:
    entity_id: automation.my_automation
  data:
    stop_actions: true # default: true — stops currently running actions
```

| Attribute                | Value                                                                                                                                                                                                                |
| ------------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `stop_actions`           | Optional. Stops currently active action runs. **Defaults to `true`.**                                                                                                                                                |
| Survives reload?         | Yes — state is stored in `core.restore_state`                                                                                                                                                                        |
| Survives restart?        | Only if the automation has an `id:` field — `core.restore_state` matches by `entity_id`, which is derived from `alias:` without `id:` and is unstable if automations are added, removed, or have conflicting aliases |
| Entity in state machine? | Yes — state is `off`                                                                                                                                                                                                 |
| Re-enable via            | `automation.turn_on`                                                                                                                                                                                                 |

**`initial_state` override:** If the automation YAML contains an explicit `initial_state` value, it overrides the stored state after a restart (`true` forces on, `false` forces off regardless of stored state).

### Method 2: Registry Disable (Permanent, via Entity Registry)

Disabling an automation via _Settings → Automations → open automation → ⋮ → Settings → Enabled toggle_ sets `disabled_by: user` in `core.entity_registry`. The entity is removed from the state machine entirely.

| Attribute                | Value                                                                                                                                                                  |
| ------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Survives reload?         | Yes — stored in `core.entity_registry`                                                                                                                                 |
| Survives restart?        | Yes                                                                                                                                                                    |
| Entity in state machine? | **No** — `GET /api/states/<entity_id>` returns 404                                                                                                                     |
| Requires `id:` field?    | Yes — the `id:` field in `automations.yaml` becomes the automation's `unique_id`, which is required for an entity registry entry                                       |
| Re-enable via            | UI toggle (_Settings → Automations → open automation → ⋮ → Settings → Enabled toggle_) or WebSocket API (`config/entity_registry/update` with `{"disabled_by": null}`) |

**Note:** The list toggle on the Automations page (`/config/automation/dashboard`) calls `automation.turn_on`/`turn_off` (Method 1). The _Enabled toggle_ under _Settings → Automations → open automation → ⋮ → Settings → Enabled toggle_ modifies the entity registry (Method 2). Both can be active simultaneously — an automation can be registry-enabled but in state `off`, or registry-disabled but with a stored `on` state.

### AVOID: `enabled: false` in automations.yaml

```yaml
# AVOID — enabled: is not a valid top-level key
- alias: My Automation
  enabled: false # not a valid top-level key
  triggers: ...
```

`enabled:` is **not** a valid top-level key in `automations.yaml`. Home Assistant rejects unknown keys during schema validation, so the automation loads as `unavailable`.

```yaml
# CORRECT — disable temporarily via service (Method 1)
- action: automation.turn_off
  target:
    entity_id: automation.my_automation
# CORRECT — disable permanently via entity registry (Method 2)
# UI: Settings → Automations → open automation → ⋮ → Settings → Enabled toggle
# Or via WebSocket API: config/entity_registry/update (disabled_by: user)
```

### `enabled:` on individual triggers, conditions, and actions

While `enabled:` is not valid as a _top-level_ automation key (above), it **is** valid on any individual trigger, condition, or action — as a boolean or a blueprint `!input`. A disabled element is skipped without disabling the whole automation. It also accepts a **limited template** (variables / blueprint inputs only — no `states()`), evaluated **once when the automation loads**.

```yaml
triggers:
  - trigger: sun
    event: sunset
    enabled: false # statically disabled
  - trigger: time
    at: "15:30:00"
    enabled: "{{ enable_afternoon }}" # limited template over a variable/!input; evaluated once at load
actions:
  - action: notify.notify
    enabled: false
```
