Setting up Google Calendar sync for OpenClaw
How I set up read-only Google Calendar sync for my personal AI assistant running on a home server VM.
I’ve been running OpenClaw (clawdbot/moltbot) as a personal assistant on a virtual machine on my home server. It handles daily briefs, automations, and various tasks. One thing I wanted working properly: calendar awareness.
My AI assistant kept missing calendar events. Not all of them. Just the recurring ones. The weekly gymnastics class for my daughter at 8:30 on Saturdays? Invisible. The birthday that repeats every year? Gone.
Here’s how I fixed it.
The problem with ICS feeds
Google Calendar gives you an ICS URL you can use to fetch your calendar data. Sounds simple. Fetch the URL, parse the events, show them to the user.
The catch: recurring events aren’t stored as individual events. They’re stored once, with a recurrence rule (RRULE) that says “repeat every Saturday” or “repeat yearly on August 17th.” If you just scan for events matching today’s date, you’ll miss anything that recurs.
My setup was fetching the raw ICS file and doing naive text parsing. It worked for one-off events. It completely ignored recurring ones.
A 10-year-old family calendar has a lot of recurring events.
The fix: sync locally, query properly
Two parts to this:
- Sync the calendar locally so you’re not fetching 25,000 lines over HTTP every time you check
- Use a real ICS parser that expands recurring events into actual occurrences
OpenClaw has Node.js available, so I used ical.js for the parsing. The library handles RRULEs properly, expanding them into concrete event instances within a date range.
The sync script
Simple shell script that downloads the ICS files daily:
#!/bin/bash
CALENDAR_DIR="/home/clawd/calendars" # Adjust path as needed
mkdir -p "$CALENDAR_DIR"
sync_calendar() {
local name="$1"
local url="$2"
local file="$CALENDAR_DIR/$name.ics"
if [ -f "$file" ]; then
cp "$file" "$file.bak"
fi
if curl -sL --connect-timeout 15 --max-time 30 "$url" -o "$file.tmp"; then
if head -1 "$file.tmp" | grep -q "BEGIN:VCALENDAR"; then
mv "$file.tmp" "$file"
echo "$name: $(wc -l < "$file") lines"
else
echo "Error: $name - not a valid ICS" >&2
rm -f "$file.tmp"
return 1
fi
else
echo "Error: $name - download failed" >&2
return 1
fi
}
# Add your calendars here
sync_calendar "family" "YOUR_ICS_URL_HERE"
sync_calendar "work" "YOUR_OTHER_ICS_URL_HERE"
The query tool
A Node.js script that reads the local ICS files and outputs events for a date range. The important part is using ical.js to properly iterate through recurring events:
if (event.isRecurring()) {
const iterator = event.iterator()
let next
while ((next = iterator.next())) {
if (next.compare(endDate) > 0) break
if (next.compare(startDate) < 0) continue
// This occurrence falls within our range
events.push({
summary: event.summary,
start: next.toJSDate(),
recurring: true,
})
}
}
The iterator walks through every occurrence of the recurring event. You give it a date range, it gives you back concrete instances. Saturday gymnastics at 8:30? Now it shows up.
Cron jobs
Two cron jobs tie it together:
- Calendar sync at 6:00 AM - downloads fresh copies of all ICS files
- Morning brief at 6:58 AM (weekdays) or 8:58 AM (weekends) - queries the local files and includes events in the daily summary
The morning brief now runs node bin/cal-query.mjs --days 2 instead of trying to parse raw ICS text inline.
Results
Before:
No events today.
After:
Saturday, Jan 31
08:30 - 10:00 Gymnastics (recurring)
Both of these were invisible before.
Getting your ICS URL
Google Calendar
- Open Google Calendar
- Click the gear icon > Settings
- Select the calendar you want to sync from the left sidebar
- Scroll to “Integrate calendar”
- Copy the “Secret address in iCal format”
Use the secret address, not the public one - unless you’ve made your calendar publicly visible.
Other calendars
Any calendar that exports ICS works with this setup. The format is standardized; only the URL location differs:
- Outlook/Microsoft 365: Calendar settings > Shared calendars > Publish a calendar
- Apple iCloud: Calendar sharing settings > Public calendar link
- Fastmail: Calendar settings > Export > ICS URL
The catch
Don’t just fetch and text-parse the ICS file. Recurring events are stored as rules (RRULE), not individual entries. A naive parser will miss your weekly meetings and annual birthdays. That’s why we use ical.js to expand them into actual occurrences.
Set this up yourself
If you’re running the OpenClaw AI assistant and want this same setup, here’s a prompt you can use.
Setup prompt for OpenClaw
Set up local calendar sync and querying for my daily briefs.
**What I need:**
1. Create a calendars directory at ~/calendars (or in your workspace)
2. Create a sync script (bin/cal-sync.sh) that downloads these ICS feeds:
- "personal" from: [YOUR PERSONAL CALENDAR ICS URL]
- "family" from: [YOUR FAMILY CALENDAR ICS URL]
(add more as needed)
3. Create a query tool (bin/cal-query.mjs) using ical.js that:
- Reads all .ics files from the calendars directory
- Properly expands recurring events using RRULE
- Accepts --days N to look ahead N days
- Accepts --date YYYY-MM-DD for a specific date
- Accepts --json for machine-readable output
- Outputs human-readable event list by default
- Marks recurring events
- Handles timezone conversion to my local timezone
4. Set up cron jobs:
- calendar-sync: runs daily at 6:00 AM to refresh the local copies
- Update my existing morning brief cron to use the local query tool
instead of fetching ICS directly
5. Document the setup in TOOLS.md
The query tool should merge events from all calendars, dedupe by
title+time, and sort chronologically.
Install ical.js via npm if not already installed.