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.
- Family: Shared family events. The default calendar. Use this one for searching everything event / date related.
- 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).