Skip to Content

Scripts Overview

June 2, 2026 by
Scripts Overview
渥屋科技股份有限公司, 系統管理者
Script Syntax Reference

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 condition action only stops the current sequence block. Inside a repeat, just the present iteration ends; inside a choose, only that choose block'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...

Share this post