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.

More Articles

Generate Commit Messages with Ollama in Neovim

Generate Commit Messages with Ollama in Neovim

Generate conventional commit messages from staged diffs using Ollama. Run it locally for privacy and offline, or use cloud models for speed.

Adding Umami analytics to the OpenClaw morning brief

Adding Umami analytics to the OpenClaw morning brief

An agent skill that fetches traffic data from Umami, and how it fits into a daily automated briefing.

Better Clipboard Handling in Claude Code

Better Clipboard Handling in Claude Code

A plugin that makes clipboard operations in Claude Code more reliable and natural to use.

Get notified when Claude Code needs your input

Get notified when Claude Code needs your input

Stop constantly checking your terminal. Set up notifications that alert you when Claude Code is ready for your input.

Auto-format generated Code with Claude Code Hooks

Auto-format generated Code with Claude Code Hooks

How to set up a PostToolUse hook in Claude Code to automatically run prettier after every file edit or write operation.

PWA Web Share Target on Android: The Absolute URL Fix

PWA Web Share Target on Android: The Absolute URL Fix

Getting the Web Share Target API to work on Android PWAs can be frustrating. Your manifest looks correct, but the app never appears in the share sheet. Here is what finally worked.

Standard function keys on external keyboards in MacOS

Standard function keys on external keyboards in MacOS

Configure Karabiner Elements to use standard function keys on external keyboards while keeping media keys on your MacBook.

How to Clear Cloudflare Cache using a webhook

How to Clear Cloudflare Cache using a webhook

Automatically purge Cloudflare cache using webhooks and API tokens.

Monitoring Cron Jobs with Notifery

Monitoring Cron Jobs with Notifery

Stop silent cron job failures from catching you off guard.