Your calendar is a mess — meetings stacked back-to-back, double-bookings spotted too late, timezone confusion that made you join a call at 3am. OpenClaw + Google Calendar fixes this. Your AI agent can check availability, propose times, detect conflicts, handle timezones, and prep you for upcoming meetings.
This guide walks you through wiring OpenClaw into Google Calendar via the official API so you end up with a scheduling agent that actually understands your calendar.
Why Calendar Integration Matters
The average knowledge worker spends ~30 min/day on calendar admin — 30 × 5 × 50 ≈ 125 hours/year. At $50/hr that’s $6,250 spent just managing your calendar. What OpenClaw can do once connected:
- ●“When am I free next week?” — instant answer
- ●“Schedule a 1-hour meeting with John next Tuesday” — checks availability, sends invite
- ●“What’s on my calendar today?” — morning briefing with context
- ●“Reschedule my 3pm to tomorrow” — checks conflicts, updates the event
- ●“Prep me for my next meeting” — pulls docs and previous notes
Step 1: Enable Google Calendar API
- 1.Go to console.cloud.google.com and sign in
- 2.Create or select a project (e.g. OpenClaw Calendar)
- 3.APIs & Services → Library → search Google Calendar API → Enable
- 4.APIs & Services → Credentials → Create Credentials → OAuth client ID
- 5.Configure the consent screen (External user type, app name OpenClaw, support email)
- 6.Application type: Desktop app (or Web for server deploy), name OpenClaw Calendar Client
- 7.Copy Client ID and Client Secret
Step 2: Configure OpenClaw
Add credentials to .env:
GOOGLE_CALENDAR_ENABLED=true GOOGLE_CALENDAR_CLIENT_ID=abc123...apps.googleusercontent.com GOOGLE_CALENDAR_CLIENT_SECRET=xyz789... GOOGLE_CALENDAR_REDIRECT_URI=http://localhost:3000/auth/google/calendar/callback
Install the official client: npm install googleapis. Then create skills/calendar.js:
const { google } = require('googleapis');
const fs = require('fs');
const path = require('path');
class GoogleCalendarSkill {
constructor(config) {
this.config = config;
this.oauth2Client = null;
this.calendar = null;
}
async initialize() {
this.oauth2Client = new google.auth.OAuth2(
this.config.clientId,
this.config.clientSecret,
this.config.redirectUri,
);
const tokenPath = path.join(__dirname, '../.calendar-tokens.json');
if (fs.existsSync(tokenPath)) {
this.oauth2Client.setCredentials(JSON.parse(fs.readFileSync(tokenPath, 'utf8')));
}
this.calendar = google.calendar({ version: 'v3', auth: this.oauth2Client });
}
getAuthUrl() {
return this.oauth2Client.generateAuthUrl({
access_type: 'offline',
scope: ['https://www.googleapis.com/auth/calendar'],
});
}
async getFreeBusy(timeMin, timeMax, calendarIds = ['primary']) {
const res = await this.calendar.freebusy.query({
requestBody: {
timeMin: timeMin.toISOString(),
timeMax: timeMax.toISOString(),
items: calendarIds.map(id => ({ id })),
},
});
return res.data.calendars;
}
async findFreeSlots(start, end, durationMinutes = 60) {
const busy = (await this.getFreeBusy(start, end)).primary.busy;
const slots = [];
let cursor = new Date(start);
while (cursor < end) {
const slotEnd = new Date(cursor.getTime() + durationMinutes * 60000);
const overlap = busy.some(b =>
cursor < new Date(b.end) && slotEnd > new Date(b.start));
if (!overlap) slots.push({ start: new Date(cursor), end: slotEnd });
cursor = new Date(cursor.getTime() + 15 * 60000);
}
return slots;
}
async createEvent(summary, start, end, description = '', attendees = []) {
const tz = Intl.DateTimeFormat().resolvedOptions().timeZone;
return (await this.calendar.events.insert({
calendarId: 'primary',
requestBody: {
summary, description,
start: { dateTime: start.toISOString(), timeZone: tz },
end: { dateTime: end.toISOString(), timeZone: tz },
attendees: attendees.map(email => ({ email })),
},
})).data;
}
}
module.exports = GoogleCalendarSkill;Step 3: First-Time Authorization
Generate the auth URL, visit it in your browser, click Allow, then copy the code param from the redirect URL and exchange it for tokens:
const url = skill.getAuthUrl();
// Visit url, approve, copy ?code=... from the redirect, then:
await skill.getTokensFromCode('4/0AY0e...');
// Tokens saved to .calendar-tokens.json.calendar-tokens.json to your .gitignore — it contains refreshable credentials for your calendar.Step 4: Natural Language Calendar Queries
Wrap the skill in a lightweight agent that maps human queries to intents:
class CalendarAgent {
constructor(skill) { this.calendar = skill; }
async processQuery(query) {
const intent = this.detectIntent(query);
switch (intent.type) {
case 'check_availability': return this.handleAvailability(intent);
case 'list_events': return this.handleListEvents(intent);
case 'create_event': return this.handleCreateEvent(intent);
case 'reschedule': return this.handleReschedule(intent);
case 'prep_meeting': return this.handleMeetingPrep(intent);
default: return "I didn't understand that calendar request.";
}
}
detectIntent(query) {
const q = query.toLowerCase();
if (q.includes('when am i free') || q.includes('availability')) return { type: 'check_availability' };
if (q.includes('on my calendar') || q.includes('schedule today')) return { type: 'list_events' };
if (q.includes('schedule') || q.includes('book a meeting')) return { type: 'create_event' };
if (q.includes('reschedule') || q.includes('move my meeting')) return { type: 'reschedule' };
if (q.includes('prep me') || q.includes('next meeting')) return { type: 'prep_meeting' };
return { type: 'unknown' };
}
async handleAvailability() {
const now = new Date();
const week = new Date(now.getTime() + 7 * 86400000);
const slots = await this.calendar.findFreeSlots(now, week, 60);
if (!slots.length) return "No free 1-hour slots in the next week.";
return slots.slice(0, 5).map(s => this.fmt(s.start) + ' – ' + this.fmtT(s.end)).join('n');
}
}Step 5: Conflict Resolution Logic
Detect conflicts by listing events that overlap a candidate window, then apply a resolution strategy:
async detectConflicts(newStart, newEnd) {
const events = await this.calendar.getEvents(newStart, newEnd);
return events
.map(e => ({
id: e.id, summary: e.summary,
start: new Date(e.start.dateTime || e.start.date),
end: new Date(e.end.dateTime || e.end.date),
canReschedule: e.organizer?.self && !/important/i.test(e.summary),
}))
.filter(e => newStart < e.end && newEnd > e.start);
}Resolution Strategies
- ●Reschedule the conflicting event to the next free slot
- ●Shorten the conflicting meeting if you’re the organizer
- ●Propose an alternative time for the new event
Always confirm with the user before applying a resolution — show the proposed change and require an explicit ✅ before mutating the calendar.
Step 6: Timezone Handling (The Silent Killer)
A user in NYC asks for “3pm tomorrow”. Without timezone awareness the event becomes 8pm GMT for a London colleague — who misses the call. The fix: detect each participant’s timezone, then schedule in a window that’s inside everyone’s working hours.
async scheduleWithTimezones(details, attendees) {
const userTz = Intl.DateTimeFormat().resolvedOptions().timeZone;
const attendeeTzs = await Promise.all(
attendees.map(email => this.getAttendeeTimezone(email)));
const time = this.findBestTime(details.preferredTime, userTz, attendeeTzs);
return this.calendar.createEvent(
details.summary, time.start, time.end, details.description, attendees);
}
formatTimeWithTimezone(date, tz) {
return new Intl.DateTimeFormat('en-US', {
timeZone: tz, dateStyle: 'short', timeStyle: 'short',
}).format(date) + ' ' + tz;
}timeZone alongside dateTime when creating events. ISO strings without an explicit zone are a recipe for off-by-many-hours bugs.Meeting-Prep Agent Recipe
Run a small agent every few minutes that scans the next 30 minutes of events, pulls related Drive docs, summarizes previous meeting notes with the same attendees, and posts a briefing to wherever you live (Slack DM, email, etc.):
class MeetingPrepAgent {
async prepareMeeting(eventId) {
const event = await calendar.getEvent(eventId);
const docs = await drive.search({
query: `"${event.summary}" OR "${event.attendees[0].email}"`,
type: 'document',
});
const history = await calendar.getEvents(
new Date(Date.now() - 90 * 86400000), new Date(),
{ attendees: event.attendees.map(a => a.email) });
return `**Meeting Prep: ${event.summary}**
Time: ${formatDateTime(event.start.dateTime)}
Attendees: ${event.attendees.map(a => a.email).join(', ')}
Agenda: ${event.description || 'No agenda provided'}
Related Documents:
${docs.map(d => '- ' + d.name + ': ' + d.webViewLink).join('n')}
Previous meetings: ${history.length} in last 90 days.`;
}
}
setInterval(async () => {
const now = new Date();
const soon = new Date(now.getTime() + 30 * 60 * 1000);
for (const e of await calendar.getEvents(now, soon)) {
await sendNotification(await meetingPrepAgent.prepareMeeting(e.id));
}
}, 5 * 60 * 1000);Common Calendar Integration Issues
- ●Insufficient permissions — OAuth scope missing calendar; add https://www.googleapis.com/auth/calendar and re-authorize
- ●Token expired — listen to the oauth2Client ‘tokens’ event and persist refresh tokens
- ●Events not appearing — you may be querying the wrong calendarId; list calendarList.list() and verify
- ●Timezone mismatches — always pass an explicit timeZone in start and end blocks
The PaioClaw Alternative
DIY: 60–90 minutes for OAuth, conflict logic, and timezone plumbing — plus ongoing maintenance (token refresh bugs, timezone edge cases, conflict-resolution refinement). PaioClaw bundles a Calendar integration where OAuth is handled in the UI and you get pre-built conflict resolution, timezone intelligence, meeting prep, and Drive/Docs/Sheets cross-integration in about five minutes.
Pick DIY for custom scheduling algorithms or learning the API. Pick PaioClaw if you want standard scheduling working today and you also need calendar + drive + email in one place. Starts FREE, Smart $15/month, Genius $25/month.
The Bottom Line
Google Calendar integration turns OpenClaw into a scheduling assistant that saves real hours per week. The critical pieces are free/busy queries (availability), conflict resolution (overlap detection), and timezone handling (the silent killer). Get those three right and the rest — natural-language queries, conflict UX, meeting prep — falls out of the API surface. DIY gives you full control; PaioClaw gives you the same outcome with zero setup.

