Hobby projects → Instructions for later
Guide · Home Assistant · Assist · running daily since August 2026

“Turn the aircon off in twenty minutes” — instructions for later, by voice

Households run on “later”, not “now”: fall asleep to the fan, curtains at sunrise, lights off when the film ends. Home Assistant’s built-in voice timers only chime. This guide adds real scheduled actions to a voice assistant — lights, aircon or curtains, any room, at a time or after a delay, in English, Thai or Mandarin — using the community Scheduler component and three small scripts that never claim more than they have verified. Complete YAML, ready to adapt.

The design, before the code

One primitive, three verbs. The Scheduler component can create a one-shot schedule that runs a service once and then deletes itself. Everything else is three scripts the language model can call: schedule, list, cancel. Each returns a spoken message in the language the person used, and the assistant is told to speak it verbatim.

Verify before speaking. “Sure, at six” is worthless if nothing was actually created. The schedule script waits two seconds, finds the schedule entity it just asked for, checks it is on and has a real next-trigger time, and only then says the time — the verified time, read back from the entity, not the time it hoped for. Cancelling works the same way in reverse: it counts what is left afterwards and admits it if anything survived.

The rules live in the tool descriptions. A language model reads a tool’s description at exactly the moment it decides whether to use it. So the description is where the routing lives: do not use the timer tool for this, convert “six a.m.” to 06:00, treat “don’t open the curtains at six” as a cancellation, never say something is cancelled without calling this. In our testing this is far more reliable than putting the same rules in the system prompt.

Fail closed. An impossible combination — curtains in a room that has none, a time that does not parse — returns a message that says nothing was scheduled. The model cannot invent a success it was never handed.

Schedules clean themselves up. repeat_type: single with the start and end date set to the fire date means the entity fires once and is gone. Nothing accumulates; “what’s scheduled?” is always the truth.

One law, paid for in full. Never cancel a schedule the user set on the strength of an inference. If a new request seems to contradict one, say what is pending and ask. The cancel script is deliberately only reachable by an explicit request.

What you need

Script 1 — schedule an action

Exactly one of at_time or in_minutes. A clock time already past today rolls to tomorrow. Aircon-on forces cool mode, because climate.turn_on restores whatever mode the unit remembers — and “heat” at six in the morning in Bangkok is not what anyone asked for. Edit entity_map and the room options to match your house.
Settings → Automations & scenes → Scripts → new → Edit in YAML
alias: Schedule a device action (at a time or after a delay)
icon: mdi:calendar-clock
mode: queued
description: >-
  Schedules a device action to happen later — either AT a clock time or IN a number of minutes. Use this
  for ANY request of the form 'open/close/turn on/turn off <device> at <time>' or '... in <N> minutes/hours'
  (e.g. 'open the curtains at 6 a.m. tomorrow', 'turn off the bedroom lights in 20 minutes', 'ปิดแอร์อีกครึ่งชั่วโมง').
  Do NOT use the timer tool for these — a timer only chimes, it cannot perform a device action. Convert
  spoken times to 24-hour HH:MM ('six a.m.' → '06:00', 'half past nine tonight' → '21:30'); if the time
  has already passed today it is scheduled for tomorrow automatically. Convert durations to minutes ('an
  hour' → 60). Pass exactly one of at_time or in_minutes. Returns a 'message' field in the language you
  pass as 'language' — speak that message back to the user verbatim; it states the VERIFIED time the action
  will run. If the message says nothing was scheduled, tell the user that.
fields:
  action:
    name: Action
    description: 'What to do: ''open'' or ''close'' (curtains), ''turn_on'' or ''turn_off'' (lights, aircon).'
    required: true
    example: open
    selector:
      select:
        options:
        - open
        - close
        - turn_on
        - turn_off
  device:
    name: Device
    description: 'Which device class: ''curtains'', ''lights'' or ''aircon''.'
    required: true
    example: curtains
    selector:
      select:
        options:
        - curtains
        - lights
        - aircon
  room:
    name: Room
    description: >-
      The room. If the user names a room, pass it exactly as one of the options. If they don't, pass the
      room the user is speaking from.
    required: true
    example: Living Room
    selector:
      select:
        options:
        - Living Room
        - Bedroom
        - Stu Room
        - Cooking Room
        - Shower Room
        - Bath Room
  at_time:
    name: At time
    description: >-
      Clock time to run the action, 24-hour HH:MM (e.g. '06:00', '21:30'). Use this OR in_minutes, not
      both.
    required: false
    example: 06:00
    selector:
      text: null
  in_minutes:
    name: In minutes
    description: Delay in whole minutes from now (e.g. 20, 90). Use this OR at_time, not both.
    required: false
    example: 20
    selector:
      number:
        min: 1
        max: 1440
        mode: box
  language:
    name: Language
    description: >-
      The language the user spoke in: 'th' Thai, 'zh' Chinese, 'en' English. The message is returned in
      this language. Defaults to 'en'.
    required: false
    example: th
    selector:
      select:
        options:
        - en
        - th
        - zh
sequence:
- alias: Resolve inputs
  variables:
    lang: '{{ language | default(''en'') }}'
    act: '{{ action | default('''') }}'
    dev: '{{ device | default('''') }}'
    rm: '{{ room | default('''') }}'
    entity_map:
      curtains:
        Living Room: cover.living_room_curtains
      lights:
        Living Room: light.living_room
        Bedroom: light.bedroom
        Stu Room: light.stu_room
        Cooking Room: light.cooking_room
        Shower Room: light.shower_room
        Bath Room: light.bath_room
      aircon:
        Living Room: climate.living_room
        Bedroom: climate.bedroom
        Stu Room: climate.stu_room
    target_entity: '{{ entity_map.get(dev, {}).get(rm, '''') }}'
    svc: >-
      {% if dev == 'curtains' and act == 'open' %}cover.open_cover{% elif dev == 'curtains' and act ==
      'close' %}cover.close_cover{% elif dev == 'lights' and act in ['turn_on','open'] %}light.turn_on{%
      elif dev == 'lights' and act in ['turn_off','close'] %}light.turn_off{% elif dev == 'aircon' and
      act in ['turn_on','open'] %}climate.set_hvac_mode{% elif dev == 'aircon' and act in ['turn_off','close']
      %}climate.set_hvac_mode{% else %}{% endif %}
    svc_data: >-
      {% if dev == 'aircon' and act in ['turn_on','open'] %}{{ {'hvac_mode': 'cool'} }}{% elif dev ==
      'aircon' and act in ['turn_off','close'] %}{{ {'hvac_mode': 'off'} }}{% else %}{{ {} }}{% endif
      %}
    has_delay: '{{ in_minutes is defined and in_minutes is not none and (in_minutes | int(0)) > 0 }}'
    has_time: >-
      {{ at_time is defined and at_time is not none and (at_time | string) is match('^([01]?\d|2[0-3]):[0-5]\d$')
      }}
    fire_at: >-
      {% set n = now() %}{% if in_minutes is defined and in_minutes is not none and (in_minutes | int(0))
      > 0 %}{{ (n + timedelta(minutes=(in_minutes | int))).isoformat() }}{% elif at_time is defined and
      at_time is not none and (at_time | string) is match('^([01]?\d|2[0-3]):[0-5]\d$') %}{% set hh =
      (at_time | string).split(':')[0] | int %}{% set mm = (at_time | string).split(':')[1] | int %}{%
      set t = n.replace(hour=hh, minute=mm, second=0, microsecond=0) %}{% if t <= n %}{% set t = t + timedelta(days=1)
      %}{% endif %}{{ t.isoformat() }}{% else %}{{ '' }}{% endif %}
- alias: Validate
  choose:
  - conditions:
    - condition: template
      value_template: '{{ target_entity == '''' or svc == '''' or fire_at == '''' }}'
    sequence:
    - variables:
        message: >-
          {% if lang == 'th' %}ขอโทษค่ะ ตั้งเวลาไม่ได้ ไม่ได้ตั้งอะไรไว้เลย{% elif lang == 'zh' %}抱歉,无法设置定时,什么也没有安排。{%
          else %}Sorry — I couldn't set that schedule. Nothing was scheduled.{% endif %}
        reason: target={{ target_entity }} svc={{ svc }} fire_at={{ fire_at }}
    - variables:
        result_vars:
          message: '{{ message }}'
          scheduled: false
          reason: '{{ reason }}'
    - stop: invalid request
      response_variable: result_vars
- alias: Compute names and times
  variables:
    fire_dt: '{{ as_datetime(fire_at) }}'
    fire_date: '{{ as_datetime(fire_at).strftime(''%Y-%m-%d'') }}'
    fire_hhmm: '{{ as_datetime(fire_at).strftime(''%H:%M'') }}'
    is_tomorrow: '{{ as_datetime(fire_at).date() != now().date() }}'
    sched_name: >-
      Voice: {{ act }} {{ rm }} {{ dev }} @ {{ as_datetime(fire_at).strftime('%d/%m %H:%M') }} [{{ now().strftime('%H%M%S')
      }}]
- alias: Create the schedule
  action: scheduler.add
  continue_on_error: true
  data:
    name: '{{ sched_name }}'
    weekdays:
    - daily
    start_date: '{{ fire_date }}'
    end_date: '{{ fire_date }}'
    repeat_type: single
    timeslots:
    - start: '{{ fire_hhmm }}'
      actions:
      - service: '{{ svc }}'
        entity_id: '{{ target_entity }}'
        service_data: '{{ svc_data }}'
- delay:
    seconds: 2
- alias: Verify the schedule entity exists
  variables:
    sched_entity: >-
      {{ states.switch | selectattr('attributes.friendly_name', 'eq', 'Scheduler ' ~ sched_name) | map(attribute='entity_id')
      | list | first | default('') }}
    sched_ok: >-
      {{ sched_entity != '' and is_state(sched_entity, 'on') and state_attr(sched_entity, 'next_trigger')
      is not none }}
- alias: Build the spoken message
  variables:
    room_th: >-
      {{ {'Living Room': 'ห้องนั่งเล่น', 'Bedroom': 'ห้องนอน', 'Stu Room': 'ห้องสตู', 'Cooking Room':
      'ห้องครัว', 'Shower Room': 'ห้องอาบน้ำ', 'Bath Room': 'ห้องน้ำ'}.get(rm, rm) }}
    room_zh: >-
      {{ {'Living Room': '客厅', 'Bedroom': '卧室', 'Stu Room': 'Stu的房间', 'Cooking Room': '厨房', 'Shower Room':
      '淋浴间', 'Bath Room': '浴室'}.get(rm, rm) }}
    dev_en: '{{ {''curtains'': ''curtains'', ''lights'': ''lights'', ''aircon'': ''aircon''}.get(dev,
      dev) }}'
    dev_th: '{{ {''curtains'': ''ม่าน'', ''lights'': ''ไฟ'', ''aircon'': ''แอร์''}.get(dev, dev) }}'
    dev_zh: '{{ {''curtains'': ''窗帘'', ''lights'': ''灯'', ''aircon'': ''空调''}.get(dev, dev) }}'
    verb_en: >-
      {{ {'open': 'open', 'close': 'close', 'turn_on': 'turn on', 'turn_off': 'turn off'}.get(act, act)
      }}
    verb_th: '{{ {''open'': ''เปิด'', ''close'': ''ปิด'', ''turn_on'': ''เปิด'', ''turn_off'': ''ปิด''}.get(act,
      act) }}'
    verb_zh: '{{ {''open'': ''打开'', ''close'': ''关闭'', ''turn_on'': ''打开'', ''turn_off'': ''关闭''}.get(act,
      act) }}'
    when_en: >-
      {% if has_delay %}in {{ in_minutes | int }} minutes, at {{ fire_hhmm }}{% else %}at {{ fire_hhmm
      }}{% if is_tomorrow %} tomorrow{% endif %}{% endif %}
    when_th: >-
      {% if has_delay %}อีก {{ in_minutes | int }} นาที เวลา {{ fire_hhmm }} น.{% else %}เวลา {{ fire_hhmm
      }} น.{% if is_tomorrow %} พรุ่งนี้{% endif %}{% endif %}
    when_zh: >-
      {% if has_delay %}{{ in_minutes | int }}分钟后,{{ fire_hhmm }}{% else %}{% if is_tomorrow %}明天{% endif
      %}{{ fire_hhmm }}{% endif %}
    message: >-
      {% if sched_ok %}{% if lang == 'th' %}จะ{{ verb_th }}{{ dev_th }}{{ room_th }}{{ when_th }}ค่ะ{%
      elif lang == 'zh' %}将于{{ when_zh }}{{ verb_zh }}{{ room_zh }}{{ dev_zh }}。{% else %}I'll {{ verb_en
      }} the {{ rm | lower }} {{ dev_en }} {{ when_en }}.{% endif %}{% else %}{% if lang == 'th' %}ขอโทษค่ะ
      ตั้งเวลาไม่ได้ ไม่ได้ตั้งอะไรไว้เลย{% elif lang == 'zh' %}抱歉,无法设置定时,什么也没有安排。{% else %}Sorry — I
      couldn't set that schedule. Nothing was scheduled.{% endif %}{% endif %}
    result_vars:
      message: '{{ message }}'
      scheduled: '{{ sched_ok }}'
      schedule_entity: '{{ sched_entity }}'
      fires_at: '{{ fire_at }}'
      target: '{{ target_entity }}'
      service: '{{ svc }}'
- alias: Return
  stop: done
  response_variable: result_vars

Script 2 — what is pending, and how long until

Reads the Scheduler’s own switch entities back — the ones named Voice: … by script 1 — and reports them sorted by fire time, with a verified “in 7 hours 28 minutes” per item so the model never does the arithmetic itself.
Script → Edit in YAML
alias: List scheduled actions
icon: mdi:calendar-search
mode: queued
description: >-
  Reports pending scheduled device actions (set with schedule_action: 'at <time>' or 'in <N> minutes').
  Use this whenever the user asks what is scheduled, what timers/schedules are set, WHEN something will
  happen, or HOW LONG UNTIL something happens ('when will the curtains open', 'how long until the aircon
  turns off', 'อีกนานแค่ไหนม่านจะเปิด', '还有多久窗帘会打开'). Do NOT use the built-in timer status — it only knows
  chime timers. If the user names a device or room, pass it as a filter so the answer is about that device
  only. ALWAYS call this tool before answering, even if you answered a similar question earlier in this
  conversation — schedules change and only this tool knows the current state. Returns 'message' in the
  language you pass as 'language'; speak it verbatim. Each item carries 'relative' (verified time remaining,
  e.g. 'in 7 hours 28 minutes') — read it out for 'how long until' questions, never calculate it yourself.
  If the message says nothing is scheduled for the device, say exactly that.
fields:
  language:
    name: Language
    description: 'The language the user spoke in: ''th'' Thai, ''zh'' Chinese, ''en'' English. Defaults
      to ''en''.'
    required: false
    example: th
    selector:
      select:
        options:
        - en
        - th
        - zh
  device:
    name: Device
    description: >-
      Optional filter: 'curtains', 'lights' or 'aircon'. Pass it when the user asks about a specific device.
    required: false
    example: curtains
    selector:
      select:
        options:
        - curtains
        - lights
        - aircon
  room:
    name: Room
    description: 'Optional filter: the room, exactly as one of the options.'
    required: false
    example: Living Room
    selector:
      select:
        options:
        - Living Room
        - Bedroom
        - Stu Room
        - Cooking Room
        - Shower Room
        - Bath Room
sequence:
- variables:
    lang: '{{ language | default(''en'') }}'
    dev: '{{ device | default('''') }}'
    rm: '{{ room | default('''') }}'
    items: >-
      {% set ns = namespace(rows=[]) %}{% for s in states.switch if s.entity_id.startswith('switch.schedule_voice')
      and s.state == 'on' and s.attributes.next_trigger is not none %}{% set fn = s.attributes.friendly_name
      | replace('Scheduler Voice: ', '') %}{% set parts = fn.split(' @ ')[0].split(' ') %}{% if (not dev
      or dev == parts[-1]) and (not rm or rm == (parts[1:-1] | join(' '))) %}{% set ns.rows = ns.rows
      + [{'entity': s.entity_id, 'action': parts[0], 'room': parts[1:-1] | join(' '), 'device': parts[-1],
      'at': s.attributes.next_trigger | string}] %}{% endif %}{% endfor %}{{ ns.rows | sort(attribute='at')
      }}
    count: '{{ items | count }}'
- variables:
    room_th:
      Living Room: ห้องนั่งเล่น
      Bedroom: ห้องนอน
      Stu Room: ห้องสตู
      Cooking Room: ห้องครัว
      Shower Room: ห้องอาบน้ำ
      Bath Room: ห้องน้ำ
    room_zh:
      Living Room: 客厅
      Bedroom: 卧室
      Stu Room: Stu的房间
      Cooking Room: 厨房
      Shower Room: 淋浴间
      Bath Room: 浴室
    dev_th:
      curtains: ม่าน
      lights: ไฟ
      aircon: แอร์
    dev_zh:
      curtains: 窗帘
      lights: 灯
      aircon: 空调
    verb_en:
      open: open
      close: close
      turn_on: turn on
      turn_off: turn off
    verb_th:
      open: เปิด
      close: ปิด
      turn_on: เปิด
      turn_off: ปิด
    verb_zh:
      open: 打开
      close: 关闭
      turn_on: 打开
      turn_off: 关闭
- variables:
    dev_lbl_en: >-
      {% if dev and rm %}the {{ rm | lower }} {{ dev }}{% elif dev %}the {{ dev }}{% elif rm %}the {{
      rm | lower }}{% else %}{% endif %}
    dev_lbl_th: >-
      {% set d = {'curtains': 'ม่าน', 'lights': 'ไฟ', 'aircon': 'แอร์'}.get(dev, '') %}{% set r = {'Living
      Room': 'ห้องนั่งเล่น', 'Bedroom': 'ห้องนอน', 'Stu Room': 'ห้องสตู', 'Cooking Room': 'ห้องครัว',
      'Shower Room': 'ห้องอาบน้ำ', 'Bath Room': 'ห้องน้ำ'}.get(rm, '') %}{{ d ~ r }}
    dev_lbl_zh: >-
      {% set d = {'curtains': '窗帘', 'lights': '灯', 'aircon': '空调'}.get(dev, '') %}{% set r = {'Living
      Room': '客厅', 'Bedroom': '卧室', 'Stu Room': 'Stu的房间', 'Cooking Room': '厨房', 'Shower Room': '淋浴间',
      'Bath Room': '浴室'}.get(rm, '') %}{{ r ~ d }}
    message: >-
      {% if count | int == 0 %}{% if lang == 'th' %}{% if dev_lbl_th %}ไม่มีรายการตั้งเวลาสำหรับ{{ dev_lbl_th
      }}ค่ะ{% else %}ไม่มีรายการตั้งเวลาค่ะ{% endif %}{% elif lang == 'zh' %}{% if dev_lbl_zh %}{{ dev_lbl_zh
      }}没有任何定时安排。{% else %}没有任何定时安排。{% endif %}{% else %}{% if dev_lbl_en %}Nothing is scheduled for {{
      dev_lbl_en }}.{% else %}Nothing is scheduled.{% endif %}{% endif %}{% else %}{% set ns = namespace(txt=[])
      %}{% for r in items %}{% set secs = ((as_datetime(r.at) - now()).total_seconds()) | int %}{% set
      h = secs // 3600 %}{% set m = (secs % 3600) // 60 %}{% set rel = ('in ' ~ (h ~ ' hour' ~ ('s' if
      h != 1 else '') ~ ' ' if h > 0 else '') ~ (m ~ ' minute' ~ ('s' if m != 1 else '') if (m > 0 or
      h == 0) else '')) | trim %}{% set rel_th = ('อีก ' ~ (h ~ ' ชั่วโมง ' if h > 0 else '') ~ (m ~ '
      นาที' if (m > 0 or h == 0) else '')) | trim %}{% set rel_zh = ((h ~ '小时' if h > 0 else '') ~ (m
      ~ '分钟' if (m > 0 or h == 0) else '') ~ '后') %}{% set day = '' if as_datetime(r.at).date() == now().date()
      else (' พรุ่งนี้' if lang == 'th' else ('明天' if lang == 'zh' else ' tomorrow')) %}{% if lang ==
      'th' %}{% set ns.txt = ns.txt + [verb_th.get(r.action, r.action) ~ dev_th.get(r.device, r.device)
      ~ room_th.get(r.room, r.room) ~ ' เวลา ' ~ as_datetime(r.at).strftime('%H:%M') ~ ' น.' ~ day ~ '
      (' ~ rel_th ~ ')'] %}{% elif lang == 'zh' %}{% set ns.txt = ns.txt + [day ~ as_datetime(r.at).strftime('%H:%M')
      ~ verb_zh.get(r.action, r.action) ~ room_zh.get(r.room, r.room) ~ dev_zh.get(r.device, r.device)
      ~ '(' ~ rel_zh ~ ')'] %}{% else %}{% set ns.txt = ns.txt + [verb_en.get(r.action, r.action) ~ '
      the ' ~ (r.room | lower) ~ ' ' ~ r.device ~ ' at ' ~ as_datetime(r.at).strftime('%H:%M') ~ day ~
      ' (' ~ rel ~ ')'] %}{% endif %}{% endfor %}{% if lang == 'th' %}มี {{ count }} รายการค่ะ: {{ ns.txt
      | join(', ') }}{% elif lang == 'zh' %}共有{{ count }}项安排:{{ ns.txt | join(';') }}。{% else %}{{ count
      }} scheduled: {{ ns.txt | join('; ') }}.{% endif %}{% endif %}
    result_vars:
      message: '{{ message }}'
      count: '{{ count }}'
      items: >-
        {% set ns = namespace(l=[]) %}{% for r in items %}{% set secs = ((as_datetime(r.at) - now()).total_seconds())
        | int %}{% set h = secs // 3600 %}{% set m = (secs % 3600) // 60 %}{% set rel = ('in ' ~ (h ~
        ' hour' ~ ('s' if h != 1 else '') ~ ' ' if h > 0 else '') ~ (m ~ ' minute' ~ ('s' if m != 1 else
        '') if (m > 0 or h == 0) else '')) | trim %}{% set ns.l = ns.l + [{'entity': r.entity, 'action':
        r.action, 'room': r.room, 'device': r.device, 'at': r.at, 'relative': rel, 'minutes_until': secs
        // 60}] %}{% endfor %}{{ ns.l }}
- stop: done
  response_variable: result_vars

Script 3 — cancel

Filters by device and room if given, removes what matches, waits a second, then counts what is still there before choosing its sentence.
Script → Edit in YAML
alias: Cancel scheduled actions
icon: mdi:calendar-remove
mode: queued
description: >-
  Cancels pending scheduled device actions that were set with schedule_action. Use this for ANY request
  to undo, stop or prevent a scheduled action, however it is phrased: 'cancel the curtain schedule', 'don't
  open the curtains at six', 'never mind the aircon later', 'stop the lights turning off', 'the curtains
  have been cancelled' (treat a statement that something is cancelled as a request to cancel it), 'ยกเลิกที่ตั้งเวลาไว้',
  'อย่าเปิดม่านตอนหกโมง', '取消窗帘的定时'. Never reply that something is cancelled or will not happen without
  calling this tool — only its message is the truth. Filter by device and/or room if the user names them;
  pass nothing to cancel ALL scheduled actions (confirm first if more than one would be cancelled and
  they didn't say 'all'). Returns a 'message' field in the language you pass as 'language' — speak it
  back verbatim.
fields:
  device:
    name: Device
    description: 'Optional filter: ''curtains'', ''lights'' or ''aircon''.'
    required: false
    example: curtains
    selector:
      select:
        options:
        - curtains
        - lights
        - aircon
  room:
    name: Room
    description: 'Optional filter: the room, exactly as one of the options.'
    required: false
    example: Living Room
    selector:
      select:
        options:
        - Living Room
        - Bedroom
        - Stu Room
        - Cooking Room
        - Shower Room
        - Bath Room
  language:
    name: Language
    description: 'The language the user spoke in: ''th'', ''zh'' or ''en''. Defaults to ''en''.'
    required: false
    example: th
    selector:
      select:
        options:
        - en
        - th
        - zh
sequence:
- variables:
    lang: '{{ language | default(''en'') }}'
    dev: '{{ device | default('''') }}'
    rm: '{{ room | default('''') }}'
    targets: >-
      {% set ns = namespace(l=[]) %}{% for s in states.switch if s.entity_id.startswith('switch.schedule_voice')
      %}{% set fn = s.attributes.friendly_name | replace('Scheduler Voice: ', '') %}{% set parts = fn.split('
      @ ')[0].split(' ') %}{% set r_room = parts[1:-1] | join(' ') %}{% set r_dev = parts[-1] %}{% if
      (dev == '' or dev == r_dev) and (rm == '' or rm == r_room) %}{% set ns.l = ns.l + [s.entity_id]
      %}{% endif %}{% endfor %}{{ ns.l }}
    n: '{{ targets | count }}'
- repeat:
    for_each: '{{ targets }}'
    sequence:
    - action: scheduler.remove
      continue_on_error: true
      data:
        entity_id: '{{ repeat.item }}'
- delay:
    seconds: 1
- variables:
    left: '{{ states.switch | selectattr(''entity_id'', ''in'', targets) | list | count }}'
    message: >-
      {% if n | int == 0 %}{% if lang == 'th' %}ไม่มีรายการตั้งเวลาที่ตรงกันค่ะ{% elif lang == 'zh' %}没有匹配的定时安排。{%
      else %}There was nothing scheduled that matches.{% endif %}{% elif left | int == 0 %}{% if lang
      == 'th' %}ยกเลิกแล้ว {{ n }} รายการค่ะ{% elif lang == 'zh' %}已取消{{ n }}项安排。{% else %}Cancelled {{
      n }} scheduled action{{ 's' if n | int > 1 else '' }}.{% endif %}{% else %}{% if lang == 'th' %}ขอโทษค่ะ
      ยกเลิกไม่สำเร็จ ยัง{{ left }}รายการค้างอยู่{% elif lang == 'zh' %}抱歉,取消失败,仍有{{ left }}项未取消。{% else
      %}Sorry — {{ left }} of {{ n }} could not be cancelled and are still scheduled.{% endif %}{% endif
      %}
    result_vars:
      message: '{{ message }}'
      cancelled: '{{ (n | int) - (left | int) }}'
      remaining: '{{ left }}'
- stop: done
  response_variable: result_vars

Expose, then try it

Notes from daily use

Part of a bigger picture. These scripts are one tool among many in a voice assistant that speaks the whole family’s languages; the same verify-before-claiming rule runs through the film playback chain and the music path.