We build custom applications 5x faster and cheaper 🚀
Book a Free Consultation
Stuck on an error? Book a 30-minute call with an engineer and get a direct fix + next steps. No pressure, no commitment.
To integrate Google Calendar with OpenClaw reliably, you must treat Google authentication and API access as explicit external pieces: create OAuth or service-account credentials in Google Cloud, implement the OAuth 2.0 flow (or JWT assertion for service accounts) in an external web service that you control, securely persist access and refresh tokens outside the agent runtime, configure the OpenClaw skill via ClawHub to call your external service or Google APIs using those stored tokens, and add optional webhook handling for push notifications. Do not assume the agent runtime will hold long-lived secrets or state — keep credentials and token refresh logic in a web service or secret store, validate webhook requests, and debug by inspecting Google API responses, token scopes/expiry, redirect URI settings, and ClawHub/agent invocation logs.
https://www.googleapis.com/auth/calendar (full access) or more narrow such as https://www.googleapis.com/auth/calendar.events.https://accounts.google.com/o/oauth2/v2/auth?client_id=YOUR_CLIENT_ID&redirect_uri=YOUR_REDIRECT_URI&response_type=code&scope=https%3A%2F%2Fwww.googleapis.com%2Fauth%2Fcalendar&access_type=offline&prompt=consent
https://oauth2.googleapis.com/tokenUse a POST with form-encoded body:
client_id, client_secret, code, grant_type=authorization_code, and redirect\_uri.grant_type=refresh_token to obtain new access tokens when expired.POST /api/calendar/list — list events for a calendarId and time range.POST /api/calendar/create — create an event with provided details.POST /api/calendar/webhook-ack — (optional) to manage webhook validation and channel state.POST https://www.googleapis.com/calendar/v3/calendars/{calendarId}/events/watch from your external service, providing a channel object with an address (your webhook URL).channelId and resourceId returned by Google and validate incoming notifications against stored channel info; on notification, call the Calendar API to fetch the updated resource or events list because notifications are lightweight.access\_type=offline and prompt=consent in the auth URL to get refresh tokens for long-lived access.
prompt=consent is used).
const authUrl = new URL('https://accounts.google.com/o/oauth2/v2/auth');
authUrl.searchParams.set('client_id', process.env.GOOGLE_CLIENT\_ID);
authUrl.searchParams.set('redirect_uri', process.env.OAUTH_REDIRECT\_URI);
authUrl.searchParams.set('response\_type', 'code');
authUrl.searchParams.set('scope', 'https://www.googleapis.com/auth/calendar');
authUrl.searchParams.set('access\_type', 'offline');
authUrl.searchParams.set('prompt', 'consent');
// send user to authUrl.toString()
const express = require('express');
const fetch = require('node-fetch');
const app = express();
app.get('/oauth2/callback', async (req, res) => {
const code = req.query.code;
try {
const tokenResp = await fetch('https://oauth2.googleapis.com/token', {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body: new URLSearchParams({
client_id: process.env.GOOGLE_CLIENT_ID,
client_secret: process.env.GOOGLE_CLIENT_SECRET,
code: code,
grant_type: 'authorization_code',
redirect_uri: process.env.OAUTH_REDIRECT_URI
})
});
const tokens = await tokenResp.json();
// Persist tokens.access_token, tokens.refresh_token, tokens.expires_in, and account id securely
// saveTokensForUser(userId, tokens);
res.send('OK - tokens saved');
} catch (err) {
console.error('token exchange error', err);
res.status(500).send('token exchange failed');
}
});
app.post('/api/calendar/list', express.json(), async (req, res) => {
const { userId, calendarId, timeMin, timeMax } = req.body;
// loadTokensForUser is your secure DB call
const tokens = await loadTokensForUser(userId);
// If access token expired, refresh it first (see below)
try {
const eventsResp = await fetch(`https://www.googleapis.com/calendar/v3/calendars/${encodeURIComponent(calendarId)}/events?timeMin=${encodeURIComponent(timeMin)}&timeMax=${encodeURIComponent(timeMax)}`, {
headers: { Authorization: `Bearer ${tokens.access_token}` }
});
if (eventsResp.status === 401) {
// access token may be expired — refresh and retry (implement refreshTokenForUser)
}
const data = await eventsResp.json();
res.json(data);
} catch (err) {
console.error(err);
res.status(500).send('calendar call failed');
}
});
async function refreshTokenForUser(userId) {
const tokens = await loadTokensForUser(userId);
const resp = await fetch('https://oauth2.googleapis.com/token', {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body: new URLSearchParams({
client_id: process.env.GOOGLE_CLIENT\_ID,
client_secret: process.env.GOOGLE_CLIENT\_SECRET,
refresh_token: tokens.refresh_token,
grant_type: 'refresh_token'
})
});
const newTokens = await resp.json();
// update DB with newTokens.access_token and expiry (refresh_token typically not returned again)
return newTokens;
}
Speak one‑on‑one with a senior engineer about your no‑code app, migration goals, and budget. In just half an hour you’ll leave with clear, actionable next steps—no strings attached.
1
You configure OAuth 2.0 client credentials by creating a Google OAuth client, placing the client_id and client_secret into OpenClaw (ClawHub) secrets or environment variables, pointing the OAuth redirect to the connector’s callback URL from ClawHub, running the auth flow to obtain a refresh_token, and storing that refresh_token securely for the Google Calendar connector to use at runtime.
Do this:
2
Cause: duplicates happen because ClawSync deduplication is running against the wrong identifier or runs after multiple create attempts — Google Calendar exposes both a stable iCalUID and a changeable id, webhook retries or concurrent syncs can create records before dedupe merges them, or dedupe is configured only to annotate not to upsert/merge.
// lookup by iCalUID then upsert
const existing = await eventStore.findByField('iCalUID', gcal.iCalUID);
if (existing) {
await eventStore.update(existing.id, { ...gcalFields }); // merge
} else {
await eventStore.create({ iCalUID: gcal.iCalUID, ...gcalFields });
}
3
Map Google Calendar RRULE into a recurrence string array, preserve IANA timeZone on start/end, and keep attendees as an array of objects (email, displayName, responseStatus). Store the original RRULE text so you can reconstruct series and validate with a recurrence library at runtime.
skills:
calendar:
schema:
event:
type: object
properties:
id: { type: string }
summary: { type: string }
recurrence:
type: array
items: { type: string } // original RRULE strings
start:
type: object
properties:
dateTime: { type: string } // ISO-8601
timeZone: { type: string } // IANA tz name
end:
type: object
properties:
dateTime: { type: string }
timeZone: { type: string }
attendees:
type: array
items:
type: object
properties:
email: { type: string }
displayName: { type: string }
responseStatus: { type: string }
4
Direct answer: Enforce per-user and global QPS ceilings for the Google Calendar connector, honor 429 and 5xx responses (use Retry‑After when present), and configure ClawRetries to exponential backoff with jitter: initial 500–1000ms, multiplier 2, max delay 60s, max attempts 4–6; retry on 429/500–599 and network errors, avoid retrying most 4xx except idempotent conflict cases.
// Example pattern for retries + backoff
const base = 700; // ms
const maxDelay = 60000;
const maxAttempts = 5;
async function retryable(fn){
for(let i=0;i<maxAttempts;i++){
try { return await fn(); } // success
catch(e){
if(!shouldRetry(e) || i+1===maxAttempts) throw e;
const jitter = Math.random()*base*(2**i);
const delay = Math.min(maxDelay, base*(2**i)) + jitter;
await sleep(delay);
}
}
}
This prompt helps an AI assistant understand your setup and guide you through the fix step by step, without assuming technical knowledge.
From startups to enterprises and everything in between, see for yourself our incredible impact.
Need a dedicated strategic tech and growth partner? Discover what RapidDev can do for your business! Book a call with our team to schedule a free, no-obligation consultation. We’ll discuss your project and provide a custom quote at no cost.Â