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.

Setting up Google Calendar sync for OpenClaw

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:

  1. Sync the calendar locally so you’re not fetching 25,000 lines over HTTP every time you check
  2. 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:

  1. Calendar sync at 6:00 AM - downloads fresh copies of all ICS files
  2. 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

  1. Open Google Calendar
  2. Click the gear icon > Settings
  3. Select the calendar you want to sync from the left sidebar
  4. Scroll to “Integrate calendar”
  5. 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.