| name | google-calendar |
| description | Read or write the user's Google Calendar via the LifeOS broker endpoint. Use any time the user asks to read events, check schedule, add/move/cancel a calendar event, prepare for a meeting, or anything that touches Google Calendar. NEVER read or write any local google-token.json — auth is centralised in LifeOS. |
| user-invocable | false |
| metadata | {"openclaw":{"emoji":"📅","requires":{"bins":[]}}} |
Google Calendar (via LifeOS broker)
You access the user's Google Calendar by getting a fresh access token from LifeOS on every call. Never read, write, or trust a local google-token.json file. That file (if present in the workspace) is stale by definition — the source of truth is LifeOS Secret Manager, and the dashboard reconnect flow propagates here automatically.
Where the LifeOS connection comes from
The pod is started with two env vars set by LifeOS at provisioning time:
LIFEOS_API_URL — base URL. Always read from $LIFEOS_API_URL at runtime. Never hardcode this URL into a script, config file, or workspace file. The host changes per environment (dev / prod / self-hosted) and per region, so any literal you bake in will break elsewhere.
LIFEOS_API_KEY — per-user Bearer token. Same rule: read from $LIFEOS_API_KEY at runtime, never hardcode.
A copy is also written by the init container to the user's home dir as ~/.lifeos/config.json (resolved as /home/node/.lifeos/config.json inside the running container — the PVC is mounted at /home/node, not /mnt/data, despite the init container writing to /mnt/data against its own mount). The file is { "api_url": "...", "api_key": "..." }. Use the env vars first, fall back to the file if env vars are missing.
Helper script rules
The pod has a canonical broker-config helper planted by the LifeOS init container at scripts/lifeos-broker.js. ALWAYS use it. It reads api_url and api_key from process.env first, falls back to /home/node/.lifeos/config.json, and returns a ready-made config object with the correct tokenUrl.
DO require it from any per-task script:
const { loadBrokerConfig } = require('./lifeos-broker');
const cfg = loadBrokerConfig();
const cfg2 = loadBrokerConfig({ calendarId: 'work@example.com' });
DO NOT build a tokenUrl string yourself. The host is environment-specific (it varies per region and per deployment) and any literal you bake in will silently 404 in another environment. If you find yourself typing https://...convex.site into a config object, stop and import the helper instead.
DO NOT write your own lifeos-broker.js, google-calendar-config.json, or any sibling helper that constructs broker URLs. The init container will overwrite the canonical helper on next boot regardless.
If scripts/lifeos-broker.js doesn't exist or the env vars are missing, the pod hasn't been re-provisioned yet. Tell the user "Calendar isn't connected to LifeOS in this workspace. Reconnect at app.lifeos.zone/settings, or restart the pod from the dashboard." Do not work around it by writing your own.
The one call that gets you a token
curl -sS -H "Authorization: Bearer $LIFEOS_API_KEY" \
"$LIFEOS_API_URL/api/v1/google-calendar/access-token"
Returns one of:
{ "access_token": "ya29...", "expires_at_ms": 1777200000000 }
{ "error": "calendar_disconnected", "reason": "invalid_grant" | "not_connected",
"reconnect_url": "https://app.lifeos.zone/settings" }
{ "error": "transient_upstream", "retry": true }
{ "error": "no_credentials" }
Cache the access token in memory until expires_at_ms - 60_000 ms before re-fetching. Never persist it.
On 409
Don't loop. Tell the user verbatim:
Your Google Calendar isn't connected. Reconnect at app.lifeos.zone/settings — once you do, I'll have access automatically (no need to send me anything).
Then stop trying calendar tools until they confirm.
On 401 from the broker (LifeOS auth bad)
Means the per-user LIFEOS_API_KEY is wrong or revoked. Tell the user:
Something is off with my LifeOS connection — the API key seems wrong. Tell Kemp / Michiel; you don't need to do anything yourself.
Don't ask the user for a new key — you should never accept calendar tokens by hand from the user.
On 502 (transient_upstream)
Google's token endpoint had a hiccup. Don't tell the user to reconnect. Just retry with backoff: 1s, 3s, 9s. If three retries all fail, surface a one-liner:
Google had a hiccup refreshing the calendar token. Try again in a minute.
On 503 (no_credentials)
Server-side misconfig (LifeOS missing GOOGLE_CLIENT_ID/SECRET). NOT the user's fault. Don't ask them to reconnect — that won't fix it. Surface:
LifeOS is missing the Google OAuth credentials on the server. Ping Kemp / Michiel.
Calendar API calls (after you have access_token)
Use the standard Google Calendar v3 REST API with Authorization: Bearer <access_token>. Common endpoints:
GET https://www.googleapis.com/calendar/v3/calendars/primary/events?timeMin=...&timeMax=...&singleEvents=true&orderBy=startTime
POST https://www.googleapis.com/calendar/v3/calendars/primary/events (create — gate on user approval)
PATCH https://www.googleapis.com/calendar/v3/calendars/primary/events/{id} (update — gate on user approval)
DELETE https://www.googleapis.com/calendar/v3/calendars/primary/events/{id} (delete — gate on user approval)
Default calendarId is "primary".
Wipe stale tokens
If you find a google-token.json lying around in the workspace (/home/node/.openclaw/workspace/google-token.json or similar), delete it. Don't read it. It's a leftover from a previous self-bootstrapped flow and will go stale.
What NOT to do
- Don't ask the user for OAuth client IDs, refresh tokens, or any kind of credential file. The LifeOS dashboard handles all of that.
- Don't write any token, refresh_token, or client secret to disk inside the pod.
- Don't use a different OAuth client than the one LifeOS owns — there isn't one.
- Don't tell the user to "paste a token here" or similar. Calendar auth is a one-click dashboard flow.
Approval
Calendar reads are low-risk. Creating, updating, or deleting events is mid-risk and requires explicit user approval before the API call (per the Operator AI approval framework).