About making inexpensive models smarter by providing tools and context. (local models, gpt-5-mini, gpt-4.1-mini, gpt-4o-mini ...)

And another script: Calendar Query Tool

Allows you to ask things like:

  • Search for events with the term birthday in the next two months.
  • When’s the next recycling pickup?
  • Search for dentist in the calendar.

You can configure which calendars it has access to in this part of the script:

variables:
  calendars_config:
    - name: Family
      entity_id: calendar.family
    - name: Garbage
      entity_id: calendar.garbage
  default_days_ahead: 365

Here’s the complete script:

alias: Calendar Query Tool
icon: mdi:calendar-search
description: >
  LLM tool to search calendar events  All input and returned dates use LOCAL
  TIME. Format: "YYYY-MM-DD HH:mm:SS".  Results are sorted (oldest → newest) and
  include total `count`.

  Functions:
    1) search_keyword
    2) list_next (next N upcoming events)
    3) list_range (all events in time range)

  Hint:
    - search keyword doesn't support fuzzy search. If you have multiple search terms try one of them.
      If you still can't find anything, try all upcoming events. As last strategy (only if the first steps failed) try to ask the user about the rough time range to search.

  Available calender names:
    - Family     # Shared family events
    - Garbage    # Collection dates

  Parameters:
    - operation           [required]: one of "search_keyword", "list_next", "list_range"
    - calendar_names_json [optional]: JSON calendar name array like '["Calendar_Name_1","Calendar_Name_2"]'.
                                      If omitted all calendars are used.
    - all_day             [optional]: true/false — filter for all-day events only.
    - keyword             [required for search_keyword]: string to match in
                                      title/description/location (case-insensitive).
    - limit               [required for list_next]: integer ≥ 1
    - start_date_time     [required for list_range]: "YYYY-MM-DD HH:mm:SS"
    - end_date_time       [required for list_range]: "YYYY-MM-DD HH:mm:SS"

  Output on success:
    result = {
      events: [
        {
          calendar_name: string,
          calendar_entity_id: string,
          title: string,
          description: string|null,
          location: string|null,
          all_day: boolean,
          start: "YYYY-MM-DD HH:mm:SS",   # local time
          end:   "YYYY-MM-DD HH:mm:SS"    # local time
        }, ...
      ],
      count: number,
      used_time_range: { start: "YYYY-MM-DD HH:mm:SS", end: "YYYY-MM-DD HH:mm:SS" },
      used_calendars: [string, ...]
    }
    error = null

  Output on error:
    result = null
    error  = {
      error: string,
      (optional) allowed_operations: [...],
      (optional) allowed: [calendar names...],
      (optional) unknown: [invalid names...],
      (optional) received: any
    }
mode: single
fields:
  operation:
    name: Operation
    required: true
    selector:
      select:
        options:
          - search_keyword
          - list_next
          - list_range
  calendar_names_json:
    name: Calendars (JSON array of names)
    description: >
      Optional. JSON array of configured calendar names, e.g.
      '["Calendar_Name_1","Calendar_Name_2"]'. Leave empty to use all configured
      calendars.
    required: false
    selector:
      text: null
  all_day:
    name: All-day only?
    required: false
    selector:
      boolean: {}
  keyword:
    name: Keyword (only for search_keyword)
    required: false
    selector:
      text: null
  limit:
    name: Limit (only for list_next)
    required: false
    selector:
      number:
        min: 0
        max: 200
  start_date_time:
    name: Start (only for list_range)
    description: Provide local time like "2025-09-23 00:00:00" (TEXT)
    required: false
    selector:
      text: null
  end_date_time:
    name: End (only for list_range)
    description: Provide local time like "2025-09-30 23:59:59" (TEXT)
    required: false
    selector:
      text: null
variables:
  calendars_config:
    - name: Family
      entity_id: calendar.gemeinsam
    - name: Garbage
      entity_id: calendar.abfall
  default_days_ahead: 365
sequence:
  - action: logbook.log
    data:
      name: "LLM CALENDAR TOOL (RAW):"
      message: >
        operation={{ operation }}, calendar_names_json={{ calendar_names_json }}, all_day={{ all_day }}, keyword={{ keyword }}, limit={{ limit }}, start={{ start_date_time }}, end={{ end_date_time }}
      entity_id: "{{ this.entity_id }}"
  - choose:
      - conditions:
          - condition: template
            value_template: |
              {{ operation in ['search_keyword','list_next','list_range'] }}
        sequence: []
    default:
      - variables:
          error_result: |
            {{ {
              'error': 'unsupported operation',
              'allowed_operations': ['search_keyword','list_next','list_range'],
              'received': operation
            } }}
      - stop: Unsupported operation
        response_variable: error_result
  - variables:
      allowed_names: |
        {{ calendars_config | map(attribute='name') | list }}
      name_to_entity: |
        {%- set ns = namespace(m={}) -%}
        {%- for c in calendars_config -%}
          {%- set ns.m = ns.m | combine({ c.name: c.entity_id }) -%}
        {%- endfor -%}
        {{ ns.m }}
  - variables:
      input_names: >
        {%- set parsed = [] -%}
        {%- if calendar_names_json is string and calendar_names_json|trim != '' -%}
          {%- set parsed = calendar_names_json | from_json -%}
        {%- endif -%}
        {{ parsed }}
      unknown_names: |
        {%- set ns = namespace(bad=[]) -%}
        {%- for n in input_names -%}
          {%- if n not in allowed_names -%}
            {%- set ns.bad = ns.bad + [n] -%}
          {%- endif -%}
        {%- endfor -%}
        {{ ns.bad }}
  - choose:
      - conditions:
          - condition: template
            value_template: "{{ unknown_names | length > 0 }}"
        sequence:
          - variables:
              error_result: |
                {{ {
                  'error': 'Unknown calendar name(s). Use one of the configured names.',
                  'unknown': unknown_names,
                  'allowed': allowed_names
                } }}
          - stop: Invalid calendar name(s)
            response_variable: error_result
  - variables:
      selected_names: |
        {{ input_names if (input_names | length > 0) else allowed_names }}
      selected_entity_ids: |
        {%- set ns = namespace(arr=[]) -%}
        {%- for n in selected_names -%}
          {%- set ns.arr = ns.arr + [ name_to_entity[n] ] -%}
        {%- endfor -%}
        {{ ns.arr }}
  - choose:
      - conditions:
          - condition: template
            value_template: "{{ operation == 'list_next' }}"
        sequence:
          - choose:
              - conditions:
                  - condition: template
                    value_template: "{{ (limit | default(0)) | int(0) >= 1 }}"
                sequence: []
            default:
              - variables:
                  error_result: >
                    {{ { 'error': 'Missing or invalid parameter: limit (>=1 required) for list_next.' } }}
              - stop: Missing limit
                response_variable: error_result
      - conditions:
          - condition: template
            value_template: "{{ operation == 'list_range' }}"
        sequence:
          - choose:
              - conditions:
                  - condition: template
                    value_template: >
                      {{ (start_date_time is string and start_date_time|trim != '') and (end_date_time is string and end_date_time|trim != '') }}
                sequence: []
            default:
              - variables:
                  error_result: >
                    {{ { 'error': 'Missing parameter(s) for list_range. Provide start_date_time and end_date_time in local time.' } }}
              - stop: Missing date range
                response_variable: error_result
      - conditions:
          - condition: template
            value_template: "{{ operation == 'search_keyword' }}"
        sequence:
          - choose:
              - conditions:
                  - condition: template
                    value_template: >-
                      {{ keyword is defined and (keyword | string | trim) != '' }}
                sequence: []
            default:
              - variables:
                  error_result: >
                    {{ { 'error': 'Missing parameter: keyword (string) for search_keyword.' } }}
              - stop: Missing keyword
                response_variable: error_result
  - variables:
      window_start: |
        {%- if operation == 'list_range' -%}
          {{ start_date_time }}
        {%- else -%}
          {{ now().strftime('%Y-%m-%d %H:%M:%S') }}
        {%- endif -%}
      window_end: |
        {%- if operation == 'list_range' -%}
          {{ end_date_time }}
        {%- else -%}
          {{ (now() + timedelta(days=default_days_ahead)).strftime('%Y-%m-%d %H:%M:%S') }}
        {%- endif -%}
  - action: calendar.get_events
    data:
      start_date_time: "{{ window_start }}"
      end_date_time: "{{ window_end }}"
    target:
      entity_id: "{{ selected_entity_ids }}"
    response_variable: cal_resp
  - variables:
      combined_events: >
        {%- set ns = namespace(list=[]) -%}
        {%- for eid, payload in cal_resp.items() -%}
          {%- set cal_name = (calendars_config | selectattr('entity_id','equalto',eid) | map(attribute='name') | list | first) -%}
          {%- for e in (payload.get('events', []) if payload is mapping else []) -%}
            {%- set ev_title = e.get('summary') -%}
            {%- set ev_desc  = e.get('description') -%}
            {%- set ev_loc   = e.get('location') -%}
            {%- set ev_start = e.get('start') -%}
            {%- set ev_end   = e.get('end') -%}

            {# infer all_day when start/end are DATE strings (YYYY-MM-DD); prefer explicit boolean when provided #}
            {%- set start_raw = (ev_start|string) -%}
            {%- set end_raw   = (ev_end|string) -%}
            {%- set inferred_all_day = (start_raw|length == 10) or (end_raw|length == 10) -%}
            {%- set ev_all_day = e.get('all_day') if (e.get('all_day') is boolean) else inferred_all_day -%}

            {# Localize & normalize end for all-day (exclusive → inclusive) #}
            {%- set start_dt = as_local(as_datetime(ev_start)) -%}
            {%- set end_dt   = as_local(as_datetime(ev_end)) -%}
            {%- if ev_all_day -%}
              {%- set inclusive_end_dt = (end_dt - timedelta(seconds=1)) -%}
            {%- else -%}
              {%- set inclusive_end_dt = end_dt -%}
            {%- endif -%}

            {%- if operation == 'list_range' -%}
              {%- set wsdt = as_local(as_datetime(window_start)) -%}
              {%- set wedt = as_local(as_datetime(window_end)) -%}
              {%- if (start_dt > wedt) or (inclusive_end_dt < wsdt) -%}
                {%- continue -%}
              {%- endif -%}
            {%- endif -%}

            {# Optional all-day filter #}
            {%- if (all_day is boolean) and (ev_all_day != all_day) -%}
              {%- continue -%}
            {%- endif -%}

            {# Optional keyword filter (search_keyword only) #}
            {%- if operation == 'search_keyword' -%}
              {%- set hay = ((ev_title or '') ~ ' ' ~ (ev_desc or '') ~ ' ' ~ (ev_loc or '')) | lower -%}
              {%- if (keyword | lower) not in hay -%}
                {%- continue -%}
              {%- endif -%}
            {%- endif -%}

            {%- set item = {
              'calendar_name': cal_name,
              'calendar_entity_id': eid,
              'title': ev_title,
              'description': ev_desc if ev_desc is string and ev_desc|length > 0 else none,
              'location': ev_loc if ev_loc is string and ev_loc|length > 0 else none,
              'all_day': ev_all_day,
              'start': start_dt.strftime('%Y-%m-%d %H:%M:%S'),
              'end': inclusive_end_dt.strftime('%Y-%m-%d %H:%M:%S')
            } -%}

            {%- set ns.list = ns.list + [ item ] -%}
          {%- endfor -%}
        {%- endfor -%}
        {{ ns.list }}
  - variables:
      sorted_events: |
        {{ combined_events | sort(attribute='start') }}
  - variables:
      final_events: |
        {%- if operation == 'list_next' -%}
          {{ sorted_events[: (limit | int(0)) ] }}
        {%- else -%}
          {{ sorted_events }}
        {%- endif -%}
  - variables:
      result: |
        {{ {
          'events': final_events,
          'count': (final_events | length),
          'used_time_range': { 'start': window_start, 'end': window_end },
          'used_calendars': selected_names
        } }}
  - stop: ""
    response_variable: result

And here’s what I added to my prompt:
Calendars:

Use the tool ‘Calendar Query Tool’ to find calendar entries.
There are two calendars in the system.

  1. Family: Shared family events. The default calendar. Use this one for searching everything event / date related.
  2. Garbage: Has only garbage collection dates.
    Only use this one if we ask for the next garbage collection of a specific type, which can be “landfill bin”, “recycling”, “green waste” (use one of these exact search terms in this case when needed).