Custom sentences and intents - the basics

To use intent scripts you have to install the intent script integration.

Custom sentences and intents come in pairs, linked by an intent name.

language: "en"
intents:
  CustomWhatTime:
    data:
      - sentences:
          - "what time is it"
          - "tell me the time"
CustomWhatTime:
  action:
    - action: tts.speak
      target:
        entity_id: tts.home_assistant_cloud
      data:
        cache: true
        media_player_entity_id: media_player.kitchen
        message: Too late

Custom sentences should be saved as yaml files in config/custom_sentences/<language_code>/ with a separate file for each sentence. Being in English, the examples here are saved in config/custom_sentences/en/. The name of the yaml file doesn't matter, but they should all begin with

language: "en"
intents:
  <IntentName>:

where <IntentName> is the name of the intent the sentence points to.

Intent scripts are stored together in configuration.yaml, under the key intent_script:. Their number will grow rapidly so it is good practice to keep them in a yaml file of their own, then add this to configuration.yaml with the line:

intent_script: !include intents.yaml

For more information, see splitting configuration.yaml.

Entities in intent scripts do not have to be exposed to Assist as they do in built-in sentences. On the other hand, any aliases you have set up will not be available - these have to be provided by the custom sentence. In this example radio station names are aliases for Sonos Favourites IDs:

language: "en"
intents:
  CustomRadioPlay:
    data:
      - sentences:
          - "Play {station}"
lists:
  station:
    values:
      - in: "radio one"
        out: "FV:2/87"
      - in: "radio 1"
        out: "FV:2/87"
      - in: "radio two"
        out: "FV:2/88"
      - in: "radio 2"
        out: "FV:2/88"
      - in: "radio too"      
        out: "FV:2/88"

Sentences

Syntax

Complete custom sentences can be listed

      - sentences:
          - "what time is it"
          - "what is the time"
          - "what's the time"
          - "tell me the time"

...or a single sentence can be marked to show (alternative words), and [words which can be omitted]:

      - sentences:
          - "(what | what's | what is | tell me) [the] time [is it]"

Documentation on custom sentence syntax can be found here.

Slots

Slots are placeholders in custom sentences containing values to be passed to the intent script. In the sentence

          - "play {album} by {artist}"

{album} and {artist} are slots. When a user says "Play Aqualung by Jethro Tull", the slot album captures "Aqualung" and the slot artist captures "Jethro Tull". The matched intent would be able to generate the TTS sentence

"OK. Playing {{album}} by {{artist}}"

If your slot value is a string, you can restrict the user to a list of valid options

language: en
intents:
  PlayAlbum:
    data:
      - sentences:
          - "play {album} by jethro tull"
lists:
  album:
    values:
      - "Stand Up"
      - "Aqualung"
      - "Songs from the Wood"
      - "Heavy Horses"

...or you can use a wildcard

lists:
  album:
    wildcard: true

Note: If you specify a list of options and the user provides a value not on the list, you will get the default error message from your voice assistant - "Sorry, I don't understand" or something similar. If you specify a wildcard, it's up to you to use the intent script to filter out invalid options and provide an error message.

If the user is likely to use a word that is different from the string you want to pass to the intent, you can specify "in" and "out" values. For example Start the { kitchen } timer could pass on the entity ID of the timer to be started. As mentioned earlier, it can also create aliases, allowing users to refer to the same timer entity with different names.

lists:
  timer:
    values:
      - in: "kitchen"
        out: "timer.kitchen"
      - in: "cooker"
        out: "timer.kitchen"
      - in: "egg"
        out: "timer.kitchen"

If your slot value is a number, you can specify the range.

lists:
  brightness:
    range:
      from: 1
      to: 255

"Collisions"

In theory custom sentences should take precedence over built-in sentences, but this doesn't always happen. It's a particular problem when the custom sentence contains the words "in" or "on" - "What's in the diary?" is likely to give the error "No area named diary".

You can reduce the chances of this by creating a unique sentence first, then adding the problem sentence as an alternative. For example:

language: "en"
intents:
  CustomCalendarToday:
    data:
      - sentences:
          - "(What's | What is) happening"
          - "(What's | What is) in the (calendar | diary)"

Intents

Intent scripts, like normal scripts, are series of actions to be executed. They are likely to contain more templating than normal scripts, and the last action will probably be a TTS statement of some kind.

If your custom sentence is a command, the first part of the intent will execute it and the second part will be TTS confirmation. For example:

language: "en"
intents:
  CustomSetTemperature:
    data:
      - sentences:
          - "(set|change) [the] (temperature | heating | thermostat) to {temp} [degrees]"
lists:
  temp:
    range:
      from: 0
      to: 35
CustomSetTemperature:
  action:
    - action: climate.set_temperature
      data:
        temperature: "{{ temp }}"
      target:
        entity_id: climate.hallway
    - action: script.tts_response
      data:
        tts_sentence: "Heating set to {{ temp }}"

When the sentence a is question it gets more complicated. In this example, the user has asked "Have I got coffee on the shopping list?" "Coffee" is held in the slot {item}.

QueryShoppingListItem:
  action:
    - action: todo.get_items
      target:
        entity_id: todo.shopping_list
      data:
        status: needs_action
      response_variable: shopping_list_data
    - variables:
        found: "{{ shopping_list_data['todo.shopping_list']['items']|selectattr('summary','search',item)|list|count > 0 }}"
    - action: script.tts_response
      data:
        tts_sentence: >-
          {% if found %}
              "Yes. {{item}} is already there."
          {% else %}
              "No. Can't find {{item}}"
          {% endif %}
  • The first action is a standard todo list command, which finds incomplete items in the shopping list and lists them in the variable shopping_list_data. More on response variables here.

  • The second action creates the variable found which will be true if coffee is in shopping_list_data, false if not.

  • The third action calls a script to deliver the TTS response, which will be "yes" if found is true, otherwise "no".

Note: in TTS statements and HA commands the slot value item is enclosed in {{curly brackets}}, marking the value as output. In the template the value is being processed, so there are no curly brackets. More details here.

In the previous example the if... else... structure was in the TTS statement. You can also use the choose: action to specify different paths with different actions.

DeleteShoppingListItem:
  action:
    - action: todo.get_items
      target:
        entity_id: todo.shopping_list
      data:
        status: needs_action
      response_variable: shopping_list_data
    - variables:
        found: "{{ shopping_list_data['todo.shopping_list']['items']|selectattr('summary','search',item)|list|count > 0 }}"
    - choose:
        - conditions: "{{ found }}"
          sequence:
            - action: shopping_list.remove_item
              data:
                name: "{{ item }}"
            - action: script.tts_response
              data:
                tts_sentence: "Removed {{ item }}"                
        - conditions: "{{ not found }}"
          sequence:
            - action: script.tts_response
              data:
                tts_sentence: "Sorry. Can't find {{ item }} in the list"

Variables

Variables in an intent script are local, so they don't exist outside it. If you want to pass a value on to another script, then that script will have to have a field defined to receive it.

fields:
  variable_field:
    selector:
      text: null
    name: tts_sentence

In the intent, call the second script with the variable as data:

    - action: script.tts_response
      data:
        tts_sentence: "{{ variable }}"

Slot values

Slot values passed from the custom sentence can be used as variables throughout the intent script. If you want to carry out mathematical operations on them, bear in mind that in Jinja2 they will be strings, even if they were defined by the custom sentence as a range of numbers. They will have to be converted to numerical values using either | float(0) or | int(0). In this example, hour was defined in the custom sentence as a number in the range 1-12:

        hour_val: >-
          {% if minute | int(0) > 30 %}
            {{ hour | int(0) -1 }}  
          {% else %}
            {{ hour | int(0) }}
          {% endif %}

Variables action

You can use the variables: action to create any variables you need in your intent script.

  action:
    - variables:
        hour_val: >-
          {% if minute | int(0) > 30 %}
            {{ hour | int(0) -1 }}  
          {% else %}
            {{ hour | int(0) }}
          {% endif %}
        time: >- 
          {% if now().strftime('%H') | int(0) > 12 %}
            {{ now().strftime('%H') | int(0) - 12 }}
          {% else %}
            {{ now().strftime('%H') | int(0) }}
          {% endif %}
        now_val: "{{ time | float(0) * 3600 + now().strftime('%M') | int(0) * 60 + now().strftime('%S') | int(0) }}"
        alarm_val: "{{ hour_val | int(0) * 3600 + minute | int(0) * 60 }}"
        alarm: >-
          {% if alarm_val | int(0) < now_val | int(0) %}
            {{ alarm_val | int(0) + 43200 - now_val | int(0) }}
          {% else %}
            {{ alarm_val | int(0) - now_val | int(0) }}
          {% endif %}
    - action: timer.start
      target:
        entity_id: timer.voice_alarm
      data:
        duration: "{{ alarm }}"

Response variables

response_variable: in an intent script holds the output of an action and can be given any name you lke. In this example, the todo.get_items action returns a list of incomplete items on a shopping list and stores it in shopping_list_data.

    - action: todo.get_items
      target:
        entity_id: todo.shopping_list
      data:
        status: needs_action
      response_variable: shopping_list_data

Action response variables

action_response: variables have a similar function, but they are only used in speech: templates. They are always referenced by action_response

speech:
      text: "{{ action_response['calendar.my_calendar'].events | length }}"