Script Syntax Reference
A script in WoowTech is an ordered list of steps that gets carried out one after another the moment you invoke it. Picture a short checklist — switch on the entrance lamp, pause briefly, then push a phone alert. Once a script exists you can fire it from a dashboard tile, from the Assist voice helper, from within an automation, or from anywhere else capable of calling an action.
Scripts and automations are siblings. The single meaningful distinction: an automation kicks off on its own when a trigger fires, whereas a script only runs when you ask it to.
When a script happens to be running as part of an automation, the trigger variable is available too.
The shape of a script
At its core, the syntax is a list of key/value maps holding actions. Should a script contain just one action, you can drop the surrounding list. Every action accepts an optional alias for labeling.
# A script integration entry demonstrating the syntax
script:
greet_arrival:
sequence:
- alias: "Open the garage door"
action: cover.open_cover
target:
entity_id: cover.garage
- alias: "Announce that the garage opened"
action: notify.notify
data:
message: "The garage door is now open!"
Performing an action
There are many ways to perform an action — the actions page covers every option.
- alias: "Brighten the hallway"
action: light.turn_on
target:
entity_id: group.hallway
data:
brightness: 180
Triggering a scene
Rather than calling scene.turn_on, scripts offer a compact shortcut for switching scenes on.
- scene: scene.evening_relax
Variables
The variables action lets you define or replace values that later templated actions can read. Script-wide variables can also be declared separately.
- alias: "Define variables"
variables:
targets:
- fan.bedroom
- fan.study
speed: 75
- alias: "Apply fan speed"
action: fan.set_percentage
target:
entity_id: "{{ targets }}"
data:
percentage: "{{ speed }}"
Variables can themselves be templated.
- alias: "Build a templated message"
variables:
door_report: "The front gate is {{ states('cover.front_gate') }}."
- alias: "Send the door report"
action: notify.send_message
target:
entity_id: notify.my_device
data:
message: "{{ door_report }}"
How variable scope works
When variables assigns to a name that already exists, it updates that existing variable. A name that hasn't been seen before is created at the top level (the script-run scope).
sequence:
# Seed the counter with a starting value
- variables:
visitors: 0
# Bump it if Anna is home
- if:
- condition: state
entity_id: device_tracker.anna
state: "home"
then:
- variables:
visitors: "{{ visitors + 1 }}"
anna_present: true
- action: notify.notify
data:
message: "There are {{ visitors }} people home" # "There are 1 people home"
# The updated value persists here
- action: notify.notify
data:
message: "There are {{ visitors }} people home {% if anna_present is defined %}(Anna among them){% endif %}"
# "There are 1 people home (Anna among them)"
Testing a condition
You can drop a condition into the main sequence to halt the rest of it. If the condition doesn't evaluate to true, execution stops there. The conditions page documents the full range.
Note: A
conditionaction only stops the current sequence block. Inside arepeat, just the present iteration ends; inside achoose, only thatchooseblock's actions stop.
# Carry on only when Anna is home
- alias: "Verify Anna is home"
condition: state
entity_id: device_tracker.anna
state: "home"
A condition may also be a list, in which case execution proceeds only when EVERY condition is true.
- alias: "Anna home AND it's chilly indoors"
conditions:
- condition: state
entity_id: "device_tracker.anna"
state: "home"
- condition: numeric_state
entity_id: "sensor.indoor_temperature"
below: 18
Pausing with a delay
A delay puts the script on hold for a while before resuming. Several syntaxes are accepted.
# Plain seconds — pause for 8 seconds
- alias: "Hold 8s"
delay: 8
# HH:MM — pause for 2 hours
- delay: "02:00"
# HH:MM:SS — pause for 2.5 minutes
- delay: "00:02:30"
# Named units: milliseconds, seconds, minutes, hours, days.
# Combine freely; at least one is required.
# Treat milliseconds as *at least* that long — not exact.
# Pause for 3 minutes
- delay:
minutes: 3
Every form accepts templates.
# Pause for whatever input_number.pause_minutes holds
- delay: "{{ states('input_number.pause_minutes') | multiply(60) | int }}"
Waiting
These actions let a script pause until either some entity reaches a state described by a template, or one or more triggers fire.
Waiting on a template
The template is checked; if it's true the script moves on, otherwise it waits until the template becomes true.
Re-evaluation happens whenever an entity referenced by the template changes. Non-deterministic functions like now() won't cause continuous re-checks. For periodic re-evaluation, reference a sensor from the Time and Date integration that updates each minute or each day.
# Hold until the speaker stops
- alias: "Wait for the speaker to stop"
wait_template: "{{ is_state('media_player.patio', 'idle') }}"
Waiting on a trigger
This accepts the same triggers an automation's trigger section uses. The script resumes when any one of them fires. All trigger variables, variables, and script variables defined earlier are carried into the trigger.
# Wait for a custom event, or for the lamp to stay on for 12 seconds
- alias: "Wait for DOORBELL_RANG or lamp on"
wait_for_trigger:
- trigger: event
event_type: DOORBELL_RANG
- trigger: state
entity_id: light.porch
to: "on"
for: 12
Adding a timeout
Both wait styles can take a timeout, after which the script proceeds even if the condition or event never occurred. Timeout uses the same syntax as delay and likewise accepts templates.
# Wait up to 90 seconds for the sensor to read 'on', then continue
- wait_template: "{{ is_state('binary_sensor.lobby', 'on') }}"
timeout: "00:01:30"
Add continue_on_timeout: false to abort instead of continuing once the timeout lapses.
# Wait for the webhook event, or give up after the timeout
- wait_for_trigger:
- trigger: event
event_type: ifttt_webhook_received
event_data:
action: joined_network
timeout:
minutes: "{{ timeout_minutes }}"
continue_on_timeout: false
Omit continue_on_timeout: false and the script always continues, since the default is true.
The wait variable
Each time a wait finishes — whether the condition held, the event arrived, or the timeout ran out — a wait variable is created or refreshed to report the outcome.
| Variable | Meaning |
|---|---|
wait.completed |
true if the condition was satisfied, otherwise false |
wait.remaining |
How much of the timeout is left, or none when no timeout was given |
wait.trigger |
Present only after wait_for_trigger; describes which trigger fired. none if the timeout expired with no trigger |
Use this to branch on whether the wait succeeded, or to chain multiple waits under a single shared timeout budget.
# Branch depending on whether the condition was met
- wait_template: "{{ is_state('binary_sensor.window', 'on') }}"
timeout: 12
- if:
- "{{ not wait.completed }}"
then:
- action: script.window_stayed_shut
else:
- action: script.turn_on
target:
entity_id:
- script.window_opened
- script.chime
# Share a total of 12 seconds across two waits
- wait_template: "{{ is_state('binary_sensor.window_a', 'on') }}"
timeout: 12
continue_on_timeout: false
- action: switch.turn_on
target:
entity_id: switch.corridor_lamp
- wait_for_trigger:
- trigger: state
entity_id: binary_sensor.window_b
to: "on"
for: 3
timeout: "{{ wait.remaining }}"
continue_on_timeout: false
- action: switch.turn_off
target:
entity_id: switch.corridor_lamp
Firing an event
This action emits an event. In the action GUI it appears as "Fire Manual Event." Events serve many ends — they can set off an automation or signal another integration. The example below writes an entry into the Activity panel.
- alias: "Emit LOGBOOK_ENTRY event"
event: LOGBOOK_ENTRY
event_data:
name: Anna
message: just got home
entity_id: device_tracker.anna
domain: light
Use event_data to attach custom data, handy for passing values to another script that waits on an event trigger. event_data accepts templates.
- event: ROOM_SCANNED
event_data:
name: scanEvent
payload: "{{ scanResult }}"
Emitting and catching custom events
This automation raises a custom event named event_switch_state_changed carrying state in its event data. The action portion could live in either a script or an automation.
- alias: "Emit Event"
triggers:
- trigger: state
entity_id: switch.pantry
to: "on"
actions:
- event: event_switch_state_changed
event_data:
state: "on"
This second automation listens for that custom event with an Event trigger and pulls out the state value it carried.
- alias: "Catch Event"
triggers:
- trigger: event
event_type: event_switch_state_changed
actions:
- action: notify.notify
data:
message: "pantry switch is now {{ trigger.event.data.state }}"
Repeating a block of actions
This action reruns a sequence of actions, with nesting fully supported. There are three ways to govern how many times it loops.
Counted repeat
Supply a count. It may be a template, rendered when the repeat step is reached.
script:
blink_lamp:
mode: restart
sequence:
- action: light.turn_on
target:
entity_id: "light.{{ lamp }}"
- alias: "Toggle the lamp 'cycles' times"
repeat:
count: "{{ cycles|int * 2 - 1 }}"
sequence:
- delay: 1
- action: light.toggle
target:
entity_id: "light.{{ lamp }}"
blink_office_lamp:
sequence:
- alias: "Blink the office lamp 4 times"
action: script.blink_lamp
data:
lamp: office
cycles: 4
For each
This form iterates over a list, which can be predefined or generated by a template. The sequence runs once per item, with the current item exposed as repeat.item.
repeat:
for_each:
- "patio"
- "garage"
- "attic"
sequence:
- action: switch.turn_off
target:
entity_id: "switch.{{ repeat.item }}_fan"
List items can be other types too — templates, or even key/value mappings.
repeat:
for_each:
- locale: French
text: Bonjour le monde
- locale: German
text: Hallo Welt
sequence:
- action: notify.tablet
data:
title: "Greeting in {{ repeat.item.locale }}"
message: "{{ repeat.item.text }}!"
While loop
This form takes a list of conditions checked before each pass. The sequence repeats as long as the conditions stay true.
script:
keep_going:
sequence:
- action: script.prepare_step
- alias: "Loop WHILE the conditions hold"
repeat:
while:
- condition: state
entity_id: input_boolean.keep_running
state: "on"
# Cap the iterations
- condition: template
value_template: "{{ repeat.index <= 25 }}"
sequence:
- action: script.do_work
A shorthand template condition is also accepted for while:
- repeat:
while: "{{ is_state('sensor.house_mode', 'Active') and repeat.index < 12 }}"
sequence:
- ...
Repeat until
This form's conditions are checked after each pass, so the sequence always runs at least once. It repeats until the conditions become true.
automation:
- triggers:
- trigger: state
entity_id: binary_sensor.trigger_input
to: "on"
conditions:
- condition: state
entity_id: binary_sensor.target_state
state: "off"
actions:
- alias: "Loop UNTIL the conditions hold"
repeat:
sequence:
# A command that occasionally misfires
- action: shell_command.activate_relay
# Let it settle
- delay:
milliseconds: 300
until:
# Confirm success
- condition: state
entity_id: binary_sensor.target_state
state: "on"
until also supports the shorthand template form:
- repeat:
until: "{{ is_state('device_tracker.phone', 'home') }}"
sequence:
- ...
The repeat loop variable
Within a repeat action — meaning inside sequence, while, and until — a repeat variable is defined:
| Field | Description |
|---|---|
first |
True on the first pass of the loop |
index |
The current pass number: 1, 2, 3, … |
last |
True on the final pass — valid only for counted loops |
If-then
This action runs a then sequence when one or more and-combined conditions pass, with an optional else sequence for when they don't.
script:
- if:
- alias: "If the house is empty"
condition: state
entity_id: zone.home
state: 0
then:
- alias: "Then begin vacuuming"
action: vacuum.start
target:
area_id: kitchen
# `else` is entirely optional
else:
- action: notify.notify
data:
message: "Skipped vacuuming — someone's home!"
Nesting works, but if you keep nesting if-then inside else, consider choose instead.
Choosing among action groups
This action picks one sequence from a list of sequences, with full nesting support. Each sequence pairs with a list of conditions. The first sequence whose conditions all pass runs. An optional default runs only when none of the listed sequences match. Each sequence (except default) may carry an optional alias.
choose behaves like an if/elif/else chain. The first conditions/sequence pair is the "if/then," extra pairs are "elif/then," and default is the "else."
# An "if", "elif", and "else" example
automation:
- triggers:
- trigger: state
entity_id: input_boolean.run_demo
to: "on"
mode: restart
actions:
- choose:
# IF early morning
- conditions:
- condition: template
value_template: "{{ now().hour < 8 }}"
sequence:
- action: script.demo_dawn
# ELIF daytime
- conditions:
- condition: template
value_template: "{{ now().hour < 17 }}"
sequence:
- action: light.turn_off
target:
entity_id: light.lounge
- action: script.demo_midday
# ELSE evening
default:
- action: light.turn_off
target:
entity_id: light.den
- delay:
minutes: "{{ range(1, 11)|random }}"
- action: light.turn_off
target:
entity_id: all
conditions also takes a shorthand template form:
automation:
- triggers:
- trigger: state
entity_id: input_select.house_status
actions:
- choose:
- conditions: >
{{ trigger.to_state.state == 'Home' and
is_state('binary_sensor.all_secure', 'on') }}
sequence:
- action: script.welcome_home
data:
ok: true
- conditions: >
{{ trigger.to_state.state == 'Home' and
is_state('binary_sensor.all_secure', 'off') }}
sequence:
- action: script.turn_on
target:
entity_id: script.alert_blink
- action: script.welcome_home
data:
ok: false
- conditions: "{{ trigger.to_state.state == 'Away' }}"
sequence:
- action: script.depart_home
Multiple choose blocks can sit together to form an IF-IF pattern. The example below lets a single automation drive unrelated entities off one shared trigger. When the sun drops low, the porch and garden lights come on. If the living-room TV is on, someone's likely there, so those lights come on too — and the same logic applies to the studio.
# An "if" plus "if" example
automation:
- alias: "Light rooms as dusk falls if occupied"
triggers:
- trigger: numeric_state
entity_id: sun.sun
attribute: elevation
below: 4
actions:
# Always applies
- action: light.turn_on
data:
brightness: 255
color_temp: 366
target:
entity_id:
- light.porch
- light.garden
# IF an entity is ON
- choose:
- conditions:
- condition: state
entity_id: binary_sensor.livingroom_tv
state: "on"
sequence:
- action: light.turn_on
data:
brightness: 255
color_temp: 366
target:
entity_id: light.livingroom
# IF an unrelated entity is ON
- choose:
- conditions:
- condition: state
entity_id: binary_sensor.studio_pc
state: "on"
sequence:
- action: light.turn_on
data:
brightness: 255
color_temp: 366
target:
entity_id: light.studio
Grouping actions
The sequence action bundles several actions together. They run in order — each one waits for the previous to finish.
Grouping is handy when you want to collapse related sets of actions in the UI for tidiness. Paired with parallel, it also lets you run several ordered groups concurrently.
In the example below two distinct groups run in sequence — one turns devices on, the other sends notifications. Order is preserved both within each group and between the groups, for four actions total, executed back-to-back.
automation:
- triggers:
- trigger: state
entity_id: binary_sensor.driveway_motion
to: "on"
actions:
- alias: "Turn on devices"
sequence:
- action: light.turn_on
target:
entity_id: light.floodlight
- action: siren.turn_on
target:
entity_id: siren.alarm
- alias: "Send notifications"
sequence:
- action: notify.person1
data:
message: "Driveway motion detected!"
- action: notify.person2
data:
message: "Heads up — driveway sensor tripped..."
Running actions in parallel
Normally every action sequence in WoowTech runs one step at a time, each starting only after the last finishes.
That isn't always necessary — when actions don't depend on each other and order is irrelevant, the parallel action launches them all at once.
This example sends two messages simultaneously:
automation:
- triggers:
- trigger: state
entity_id: binary_sensor.driveway_motion
to: "on"
actions:
- parallel:
- action: notify.person1
data:
message: "These go out together!"
- action: notify.person2
data:
message: "These go out together!"
You can also run an ordered group inside a parallel block:
script:
parallel_demo:
sequence:
- parallel:
- sequence:
- wait_for_trigger:
- trigger: state
entity_id: binary_sensor.driveway_motion
to: "on"
- action: notify.person1
data:
message: "This one waited for the motion trigger"
- action: notify.person2
data:
message: "I fire instantly and don't wait for the above!"
Caution: Parallel execution helps in plenty of situations, but use it deliberately and only when you need it. Most of the time plain sequential actions are perfectly fine.
A few caveats of parallel actions:
- No order guarantee. They start together but may finish in any order.
- If one fails or errors, the rest keep running until they finish or error themselves.
- Variables created or changed in one parallel branch can collide with another branch's variables. Give them distinct names to avoid that.
Stopping a script sequence
You can break out of a script at any point — and optionally return a response — with the stop action.
stop takes a text explaining why the sequence is halting. That text is logged and shows up in automation and script traces.
It's useful for bailing out partway through, for instance when a condition isn't met.
- stop: "Halt the remainder of the sequence"
To return a response, use the response_variable option, naming a variable that holds the data to return. The response must be a mapping of key/value pairs.
- stop: "Halt the remainder of the sequence"
response_variable: "my_response_variable"
There's also an error option for stopping due to an unexpected problem. It halts the sequence and marks the automation or script as failed.
- stop: "That wasn't supposed to happen!"
error: true
Continuing after an error
By default a sequence halts the moment any action errors — the run stops, an error is logged, and the run is marked as errored.
Sometimes the error is expected and harmless. For those, set continue_on_error on the action.
continue_on_error is available on every action and defaults to false. Set it to true to keep the sequence going regardless of whether that action errors.
Below, the first action carries continue_on_error. If it errors, the script moves to the next action.
- alias: "If this one fails..."
continue_on_error: true
action: notify.flaky_provider
data:
message: "I might blow up..."
- alias: "This one still runs!"
action: persistent_notification.create
data:
title: "Hello!"
message: "All good here..."
Note that continue_on_error won't mask misconfiguration or errors that WoowTech doesn't handle.
Disabling an action
Any action in a sequence can be switched off without deleting it — add enabled: false.
# A script with one disabled action
script:
greet_arrival:
sequence:
# Disabled, so this won't run and no message is sent
- enabled: false
alias: "Announce the garage opening"
action: notify.notify
data:
message: "Opening the garage door!"
# Enabled, so this runs
- alias: "Open the garage door"
action: cover.open_cover
target:
entity_id: cover.garage
Actions can also be turned off via limited templates or blueprint inputs.
blueprint:
input:
toggle_action:
name: Toggle
selector:
boolean:
actions:
- delay: 0:45
enabled: !input toggle_action
Responding to a conversation
The set_conversation_response action returns a custom reply when an automation is started by a conversation engine such as a voice assistant. The response can be templated.
# A templated response producing "Checking 456"
- variables:
code: "456"
- set_conversation_response: "{{ 'Checking ' ~ code }}"
The response is delivered to the conversation engine once the automation finishes. If set_conversation_response runs more than once, the last value wins. Clear it by setting it to None:
# Clearing the conversation response
set_conversation_response: ~
If a conversation engine didn't start the automation, the response goes unused.
Script Syntax Reference
A script in WoowTech is an ordered list of steps that gets carried out one after another the moment you invoke it. Picture a short checklist — switch on the entrance lamp, pause briefly, then push a phone alert. Once a script exists you can fire it from a dashboard tile, from the Assist voice helper, from within an automation, or from anywhere else capable of calling an action.
Scripts and automations are siblings. The single meaningful distinction: an automation kicks off on its own when a trigger fires, whereas a script only runs when you ask it to.
When a script happens to be running as part of an automation, the trigger variable is available too.
The shape of a script
At its core, the syntax is a list of key/value maps holding actions. Should a script contain just one action, you can drop the surrounding list. Every action accepts an optional alias for labeling.
# A script integration entry demonstrating the syntax
script:
greet_arrival:
sequence:
- alias: "Open the garage door"
action: cover.open_cover
target:
entity_id: cover.garage
- alias: "Announce that the garage opened"
action: notify.notify
data:
message: "The garage door is now open!"
Performing an action
There are many ways to perform an action — the actions page covers every option.
- alias: "Brighten the hallway"
action: light.turn_on
target:
entity_id: group.hallway
data:
brightness: 180
Triggering a scene
Rather than calling scene.turn_on, scripts offer a compact shortcut for switching scenes on.
- scene: scene.evening_relax
Variables
The variables action lets you define or replace values that later templated actions can read. Script-wide variables can also be declared separately.
- alias: "Define variables"
variables:
targets:
- fan.bedroom
- fan.study
speed: 75
- alias: "Apply fan speed"
action: fan.set_percentage
target:
entity_id: "{{ targets }}"
data:
percentage: "{{ speed }}"
Variables can themselves be templated.
- alias: "Build a templated message"
variables:
door_report: "The front gate is {{ states('cover.front_gate') }}."
- alias: "Send the door report"
action: notify.send_message
target:
entity_id: notify.my_device
data:
message: "{{ door_report }}"
How variable scope works
When variables assigns to a name that already exists, it updates that existing variable. A name that hasn't been seen before is created at the top level (the script-run scope).
sequence:
# Seed the counter with a starting value
- variables:
visitors: 0
# Bump it if Anna is home
- if:
- condition: state
entity_id: device_tracker.anna
state: "home"
then:
- variables:
visitors: "{{ visitors + 1 }}"
anna_present: true
- action: notify.notify
data:
message: "There are {{ visitors }} people home" # "There are 1 people home"
# The updated value persists here
- action: notify.notify
data:
message: "There are {{ visitors }} people home {% if anna_present is defined %}(Anna among them){% endif %}"
# "There are 1 people home (Anna among them)"
Testing a condition
You can drop a condition into the main sequence to halt the rest of it. If the condition doesn't evaluate to true, execution stops there. The conditions page documents the full range.
Note: A
conditionaction only stops the current sequence block. Inside arepeat, just the present iteration ends; inside achoose, only thatchooseblock's actions stop.
# Carry on only when Anna is home
- alias: "Verify Anna is home"
condition: state
entity_id: device_tracker.anna
state: "home"
A condition may also be a list, in which case execution proceeds only when EVERY condition is true.
- alias: "Anna home AND it's chilly indoors"
conditions:
- condition: state
entity_id: "device_tracker.anna"
state: "home"
- condition: numeric_state
entity_id: "sensor.indoor_temperature"
below: 18
Pausing with a delay
A delay puts the script on hold for a while before resuming. Several syntaxes are accepted.
# Plain seconds — pause for 8 seconds
- alias: "Hold 8s"
delay: 8
# HH:MM — pause for 2 hours
- delay: "02:00"
# HH:MM:SS — pause for 2.5 minutes
- delay: "00:02:30"
# Named units: milliseconds, seconds, minutes, hours, days.
# Combine freely; at least one is required.
# Treat milliseconds as *at least* that long — not exact.
# Pause for 3 minutes
- delay:
minutes: 3
Every form accepts templates.
# Pause for whatever input_number.pause_minutes holds
- delay: "{{ states('input_number.pause_minutes') | multiply(60) | int }}"
Waiting
These actions let a script pause until either some entity reaches a state described by a template, or one or more triggers fire.
Waiting on a template
The template is checked; if it's true the script moves on, otherwise it waits until the template becomes true.
Re-evaluation happens whenever an entity referenced by the template changes. Non-deterministic functions like now() won't cause continuous re-checks. For periodic re-evaluation, reference a sensor from the Time and Date integration that updates each minute or each day.
# Hold until the speaker stops
- alias: "Wait for the speaker to stop"
wait_template: "{{ is_state('media_player.patio', 'idle') }}"
Waiting on a trigger
This accepts the same triggers an automation's trigger section uses. The script resumes when any one of them fires. All trigger variables, variables, and script variables defined earlier are carried into the trigger.
# Wait for a custom event, or for the lamp to stay on for 12 seconds
- alias: "Wait for DOORBELL_RANG or lamp on"
wait_for_trigger:
- trigger: event
event_type: DOORBELL_RANG
- trigger: state
entity_id: light.porch
to: "on"
for: 12
Adding a timeout
Both wait styles can take a timeout, after which the script proceeds even if the condition or event never occurred. Timeout uses the same syntax as delay and likewise accepts templates.
# Wait up to 90 seconds for the sensor to read 'on', then continue
- wait_template: "{{ is_state('binary_sensor.lobby', 'on') }}"
timeout: "00:01:30"
Add continue_on_timeout: false to abort instead of continuing once the timeout lapses.
# Wait for the webhook event, or give up after the timeout
- wait_for_trigger:
- trigger: event
event_type: ifttt_webhook_received
event_data:
action: joined_network
timeout:
minutes: "{{ timeout_minutes }}"
continue_on_timeout: false
Omit continue_on_timeout: false and the script always continues, since the default is true.
The wait variable
Each time a wait finishes — whether the condition held, the event arrived, or the timeout ran out — a wait variable is created or refreshed to report the outcome.
| Variable | Meaning |
|---|---|
wait.completed |
true if the condition was satisfied, otherwise false |
wait.remaining |
How much of the timeout is left, or none when no timeout was given |
wait.trigger |
Present only after wait_for_trigger; describes which trigger fired. none if the timeout expired with no trigger |
Use this to branch on whether the wait succeeded, or to chain multiple waits under a single shared timeout budget.
# Branch depending on whether the condition was met
- wait_template: "{{ is_state('binary_sensor.window', 'on') }}"
timeout: 12
- if:
- "{{ not wait.completed }}"
then:
- action: script.window_stayed_shut
else:
- action: script.turn_on
target:
entity_id:
- script.window_opened
- script.chime
# Share a total of 12 seconds across two waits
- wait_template: "{{ is_state('binary_sensor.window_a', 'on') }}"
timeout: 12
continue_on_timeout: false
- action: switch.turn_on
target:
entity_id: switch.corridor_lamp
- wait_for_trigger:
- trigger: state
entity_id: binary_sensor.window_b
to: "on"
for: 3
timeout: "{{ wait.remaining }}"
continue_on_timeout: false
- action: switch.turn_off
target:
entity_id: switch.corridor_lamp
Firing an event
This action emits an event. In the action GUI it appears as "Fire Manual Event." Events serve many ends — they can set off an automation or signal another integration. The example below writes an entry into the Activity panel.
- alias: "Emit LOGBOOK_ENTRY event"
event: LOGBOOK_ENTRY
event_data:
name: Anna
message: just got home
entity_id: device_tracker.anna
domain: light
Use event_data to attach custom data, handy for passing values to another script that waits on an event trigger. event_data accepts templates.
- event: ROOM_SCANNED
event_data:
name: scanEvent
payload: "{{ scanResult }}"
Emitting and catching custom events
This automation raises a custom event named event_switch_state_changed carrying state in its event data. The action portion could live in either a script or an automation.
- alias: "Emit Event"
triggers:
- trigger: state
entity_id: switch.pantry
to: "on"
actions:
- event: event_switch_state_changed
event_data:
state: "on"
This second automation listens for that custom event with an Event trigger and pulls out the state value it carried.
- alias: "Catch Event"
triggers:
- trigger: event
event_type: event_switch_state_changed
actions:
- action: notify.notify
data:
message: "pantry switch is now {{ trigger.event.data.state }}"
Repeating a block of actions
This action reruns a sequence of actions, with nesting fully supported. There are three ways to govern how many times it loops.
Counted repeat
Supply a count. It may be a template, rendered when the repeat step is reached.
script:
blink_lamp:
mode: restart
sequence:
- action: light.turn_on
target:
entity_id: "light.{{ lamp }}"
- alias: "Toggle the lamp 'cycles' times"
repeat:
count: "{{ cycles|int * 2 - 1 }}"
sequence:
- delay: 1
- action: light.toggle
target:
entity_id: "light.{{ lamp }}"
blink_office_lamp:
sequence:
- alias: "Blink the office lamp 4 times"
action: script.blink_lamp
data:
lamp: office
cycles: 4
For each
This form iterates over a list, which can be predefined or generated by a template. The sequence runs once per item, with the current item exposed as repeat.item.
repeat:
for_each:
- "patio"
- "garage"
- "attic"
sequence:
- action: switch.turn_off
target:
entity_id: "switch.{{ repeat.item }}_fan"
List items can be other types too — templates, or even key/value mappings.
repeat:
for_each:
- locale: French
text: Bonjour le monde
- locale: German
text: Hallo Welt
sequence:
- action: notify.tablet
data:
title: "Greeting in {{ repeat.item.locale }}"
message: "{{ repeat.item.text }}!"
While loop
This form takes a list of conditions checked before each pass. The sequence repeats as long as the conditions stay true.
script:
keep_going:
sequence:
- action: script.prepare_step
- alias: "Loop WHILE the conditions hold"
repeat:
while:
- condition: state
entity_id: input_boolean.keep_running
state: "on"
# Cap the iterations
- condition: template
value_template: "{{ repeat.index <= 25 }}"
sequence:
- action: script.do_work
A shorthand template condition is also accepted for while:
- repeat:
while: "{{ is_state('sensor.house_mode', 'Active') and repeat.index < 12 }}"
sequence:
- ...
Repeat until
This form's conditions are checked after each pass, so the sequence always runs at least once. It repeats until the conditions become true.
automation:
- triggers:
- trigger: state
entity_id: binary_sensor.trigger_input
to: "on"
conditions:
- condition: state
entity_id: binary_sensor.target_state
state: "off"
actions:
- alias: "Loop UNTIL the conditions hold"
repeat:
sequence:
# A command that occasionally misfires
- action: shell_command.activate_relay
# Let it settle
- delay:
milliseconds: 300
until:
# Confirm success
- condition: state
entity_id: binary_sensor.target_state
state: "on"
until also supports the shorthand template form:
- repeat:
until: "{{ is_state('device_tracker.phone', 'home') }}"
sequence:
- ...
The repeat loop variable
Within a repeat action — meaning inside sequence, while, and until — a repeat variable is defined:
| Field | Description |
|---|---|
first |
True on the first pass of the loop |
index |
The current pass number: 1, 2, 3, … |
last |
True on the final pass — valid only for counted loops |
If-then
This action runs a then sequence when one or more and-combined conditions pass, with an optional else sequence for when they don't.
script:
- if:
- alias: "If the house is empty"
condition: state
entity_id: zone.home
state: 0
then:
- alias: "Then begin vacuuming"
action: vacuum.start
target:
area_id: kitchen
# `else` is entirely optional
else:
- action: notify.notify
data:
message: "Skipped vacuuming — someone's home!"
Nesting works, but if you keep nesting if-then inside else, consider choose instead.
Choosing among action groups
This action picks one sequence from a list of sequences, with full nesting support. Each sequence pairs with a list of conditions. The first sequence whose conditions all pass runs. An optional default runs only when none of the listed sequences match. Each sequence (except default) may carry an optional alias.
choose behaves like an if/elif/else chain. The first conditions/sequence pair is the "if/then," extra pairs are "elif/then," and default is the "else."
# An "if", "elif", and "else" example
automation:
- triggers:
- trigger: state
entity_id: input_boolean.run_demo
to: "on"
mode: restart
actions:
- choose:
# IF early morning
- conditions:
- condition: template
value_template: "{{ now().hour < 8 }}"
sequence:
- action: script.demo_dawn
# ELIF daytime
- conditions:
- condition: template
value_template: "{{ now().hour < 17 }}"
sequence:
- action: light.turn_off
target:
entity_id: light.lounge
- action: script.demo_midday
# ELSE evening
default:
- action: light.turn_off
target:
entity_id: light.den
- delay:
minutes: "{{ range(1, 11)|random }}"
- action: light.turn_off
target:
entity_id: all
conditions also takes a shorthand template form:
automation:
- triggers:
- trigger: state
entity_id: input_select.house_status
actions:
- choose:
- conditions: >
{{ trigger.to_state.state == 'Home' and
is_state('binary_sensor.all_secure', 'on') }}
sequence:
- action: script.welcome_home
data:
ok: true
- conditions: >
{{ trigger.to_state.state == 'Home' and
is_state('binary_sensor.all_secure', 'off') }}
sequence:
- action: script.turn_on
target:
entity_id: script.alert_blink
- action: script.welcome_home
data:
ok: false
- conditions: "{{ trigger.to_state.state == 'Away' }}"
sequence:
- action: script.depart_home
Multiple choose blocks can sit together to form an IF-IF pattern. The example below lets a single automation drive unrelated entities off one shared trigger. When the sun drops low, the porch and garden lights come on. If the living-room TV is on, someone's likely there, so those lights come on too — and the same logic applies to the studio.
# An "if" plus "if" example
automation:
- alias: "Light rooms as dusk falls if occupied"
triggers:
- trigger: numeric_state
entity_id: sun.sun
attribute: elevation
below: 4
actions:
# Always applies
- action: light.turn_on
data:
brightness: 255
color_temp: 366
target:
entity_id:
- light.porch
- light.garden
# IF an entity is ON
- choose:
- conditions:
- condition: state
entity_id: binary_sensor.livingroom_tv
state: "on"
sequence:
- action: light.turn_on
data:
brightness: 255
color_temp: 366
target:
entity_id: light.livingroom
# IF an unrelated entity is ON
- choose:
- conditions:
- condition: state
entity_id: binary_sensor.studio_pc
state: "on"
sequence:
- action: light.turn_on
data:
brightness: 255
color_temp: 366
target:
entity_id: light.studio
Grouping actions
The sequence action bundles several actions together. They run in order — each one waits for the previous to finish.
Grouping is handy when you want to collapse related sets of actions in the UI for tidiness. Paired with parallel, it also lets you run several ordered groups concurrently.
In the example below two distinct groups run in sequence — one turns devices on, the other sends notifications. Order is preserved both within each group and between the groups, for four actions total, executed back-to-back.
automation:
- triggers:
- trigger: state
entity_id: binary_sensor.driveway_motion
to: "on"
actions:
- alias: "Turn on devices"
sequence:
- action: light.turn_on
target:
entity_id: light.floodlight
- action: siren.turn_on
target:
entity_id: siren.alarm
- alias: "Send notifications"
sequence:
- action: notify.person1
data:
message: "Driveway motion detected!"
- action: notify.person2
data:
message: "Heads up — driveway sensor tripped..."
Running actions in parallel
Normally every action sequence in WoowTech runs one step at a time, each starting only after the last finishes.
That isn't always necessary — when actions don't depend on each other and order is irrelevant, the parallel action launches them all at once.
This example sends two messages simultaneously:
automation:
- triggers:
- trigger: state
entity_id: binary_sensor.driveway_motion
to: "on"
actions:
- parallel:
- action: notify.person1
data:
message: "These go out together!"
- action: notify.person2
data:
message: "These go out together!"
You can also run an ordered group inside a parallel block:
script:
parallel_demo:
sequence:
- parallel:
- sequence:
- wait_for_trigger:
- trigger: state
entity_id: binary_sensor.driveway_motion
to: "on"
- action: notify.person1
data:
message: "This one waited for the motion trigger"
- action: notify.person2
data:
message: "I fire instantly and don't wait for the above!"
Caution: Parallel execution helps in plenty of situations, but use it deliberately and only when you need it. Most of the time plain sequential actions are perfectly fine.
A few caveats of parallel actions:
- No order guarantee. They start together but may finish in any order.
- If one fails or errors, the rest keep running until they finish or error themselves.
- Variables created or changed in one parallel branch can collide with another branch's variables. Give them distinct names to avoid that.
Stopping a script sequence
You can break out of a script at any point — and optionally return a response — with the stop action.
stop takes a text explaining why the sequence is halting. That text is logged and shows up in automation and script traces.
It's useful for bailing out partway through, for instance when a condition isn't met.
- stop: "Halt the remainder of the sequence"
To return a response, use the response_variable option, naming a variable that holds the data to return. The response must be a mapping of key/value pairs.
- stop: "Halt the remainder of the sequence"
response_variable: "my_response_variable"
There's also an error option for stopping due to an unexpected problem. It halts the sequence and marks the automation or script as failed.
- stop: "That wasn't supposed to happen!"
error: true
Continuing after an error
By default a sequence halts the moment any action errors — the run stops, an error is logged, and the run is marked as errored.
Sometimes the error is expected and harmless. For those, set continue_on_error on the action.
continue_on_error is available on every action and defaults to false. Set it to true to keep the sequence going regardless of whether that action errors.
Below, the first action carries continue_on_error. If it errors, the script moves to the next action.
- alias: "If this one fails..."
continue_on_error: true
action: notify.flaky_provider
data:
message: "I might blow up..."
- alias: "This one still runs!"
action: persistent_notification.create
data:
title: "Hello!"
message: "All good here..."
Note that continue_on_error won't mask misconfiguration or errors that WoowTech doesn't handle.
Disabling an action
Any action in a sequence can be switched off without deleting it — add enabled: false.
# A script with one disabled action
script:
greet_arrival:
sequence:
# Disabled, so this won't run and no message is sent
- enabled: false
alias: "Announce the garage opening"
action: notify.notify
data:
message: "Opening the garage door!"
# Enabled, so this runs
- alias: "Open the garage door"
action: cover.open_cover
target:
entity_id: cover.garage
Actions can also be turned off via limited templates or blueprint inputs.
blueprint:
input:
toggle_action:
name: Toggle
selector:
boolean:
actions:
- delay: 0:45
enabled: !input toggle_action
Responding to a conversation
The set_conversation_response action returns a custom reply when an automation is started by a conversation engine such as a voice assistant. The response can be templated.
# A templated response producing "Checking 456"
- variables:
code: "456"
- set_conversation_response: "{{ 'Checking ' ~ code }}"
The response is delivered to the conversation engine once the automation finishes. If set_conversation_response runs more than once, the last value wins. Clear it by setting it to None:
# Clearing the conversation response
set_conversation_response: ~
If a conversation engine didn't start the automation, the response goes unused.
Start writing here...