Google Workspace APIs skill for Claude Code
Drive, Calendar, Gmail and Sheets with minimal scopes. Claude picks the right Google auth model for who owns the data, requests the narrowest scopes so verification stays cheap, and replaces repeated full scans with sync tokens, history IDs and change feeds. Push notifications are authenticated by channel token or Pub/Sub OIDC, and quota errors get backoff instead of retry storms.
- name
google-workspace-apis - size229 lines · ≈2,750 tokens
- files1
- licenseMIT
- updated
What Claude sees before loading it
This description is the only part of the skill that sits in context all the time. Claude reads it to decide when to load the full playbook below.
description: Builds integrations with Google Workspace APIs - Drive, Calendar, Gmail, and Sheets - choosing between user OAuth, service accounts with domain-wide delegation, and keyless credentials, requesting the narrowest scopes, incremental sync with page and sync tokens, push notifications through watch channels and Pub/Sub, quota-aware batching and backoff, and app verification for sensitive scopes. Use when reading or writing Google Drive files, syncing Google Calendar events, sending or watching Gmail, using Sheets as a data source, or debugging 401, 403 insufficient scopes, 410 Gone, or rate limit errors. Try asking
- Sync each user's Google Calendar into our app and keep it updated without polling
- Our nightly job uses a service account to read Drive files across the company - set up domain-wide delegation properly
- We get 403 userRateLimitExceeded writing to Google Sheets - fix the integration
Use it when
- Reading or writing Google Drive, Calendar, Gmail or Sheets data from an app or job
- Choosing between user OAuth, a service account, and domain-wide delegation
- Replacing polling with sync tokens and push notifications
- Hitting scope, verification, 410 Gone, or rate limit errors
Skip it when
- You only need Sign in with Google for identity - use oauth-oidc instead
- You are building booking pages, availability rules, or ICS invites - use calendar-scheduling instead
How to check the result
tokeninfo shows only the intended scopes, an edit made in the Google UI arrives through push and is applied by one incremental sync, and a corrupted sync token triggers a clean full resync.
The common mistake it prevents
Requesting broad restricted scopes like full drive or gmail.readonly when drive.file or gmail.send would do, which forces a lengthy verification and security assessment before launch.
Done well means: the auth model matches who owns the data, scopes are the narrowest that work (so verification is cheap or unnecessary), sync is incremental with tokens rather than repeated full scans, push notifications are authenticated, and every call survives 429/403-rate-limit responses with backoff.
Ground rules
- No credentials in the repo. OAuth client secrets, refresh tokens, and service account keys live in a secret manager or env vars; refresh tokens are encrypted at rest per user.
- Prefer keyless credentials (attached service account on Cloud Run/GKE/Functions, Workload Identity Federation from AWS, Azure, or GitHub Actions) over JSON key files. Newer Google Cloud organizations block service account key creation by default; do not ask the user to disable that policy without saying why.
- Always send
fields=(partial response) on list and get calls; it cuts payload, latency, and sometimes quota. - Use the official clients:
googleapis(Node),google-api-python-client+google-auth(Python), or the per-API Go/Java libraries. They handle discovery, pagination helpers, media upload, and token refresh.
Choose the auth model
| Whose data | Use | Notes |
|---|---|---|
| End users of a multi-tenant app (any Google account) | OAuth 2.0 authorization code flow, access_type=offline | Store per-user refresh token; see oauth-oidc for the flow itself |
| All users in one Workspace domain, admin-approved, background jobs | Service account + domain-wide delegation, impersonating each user (subject) | Workspace admin authorizes the client ID and exact scopes in Admin console > Security > API controls |
| Files or calendars explicitly shared with a robot identity | Service account without delegation | Share the Sheet/Drive folder/Calendar with the SA email; it has no mailbox and no own Gmail |
| Public data (public calendars, some Sheets) | API key restricted by API and referrer/IP | Never for private data |
Decision rule: if a human must click “Allow”, it is OAuth. If an admin approves once for the whole domain, it is domain-wide delegation. Delegation is powerful (the app can act as any user for the granted scopes); scope it tightly and log every impersonation.
Scopes
Pick the narrowest scope; sensitive and restricted scopes trigger Google’s OAuth app verification for external apps, and restricted scopes also require a recurring third-party security assessment.
| Need | Prefer | Avoid unless required |
|---|---|---|
| Files the app creates or the user picks | drive.file (non-sensitive) | drive, drive.readonly (restricted) |
| Read calendars | calendar.readonly or calendar.events.readonly | calendar |
| Create events | calendar.events | calendar |
| Send mail only | gmail.send (sensitive) | gmail.modify, mail.google.com (restricted) |
| Read mail | gmail.metadata if headers suffice | gmail.readonly (restricted) |
| Sheets the user picks | drive.file + Sheets API | spreadsheets on all sheets |
Combine drive.file with the Google Picker so users grant per-file access without a
restricted scope. Request scopes incrementally (include_granted_scopes=true) at the
moment a feature needs them.
Workflow
- [ ] 1. Identify data owner and pick the auth model
- [ ] 2. Enable each API in the Cloud project; configure the OAuth consent screen
- [ ] 3. Pick minimal scopes; check sensitive/restricted status
- [ ] 4. Build the client with credentials from the environment
- [ ] 5. Implement initial sync, then incremental sync with tokens
- [ ] 6. Add push notifications (watch channels or Gmail Pub/Sub) with authentication
- [ ] 7. Add backoff, batching, and quota monitoring
- [ ] 8. Verify with a test user/domain; check granted scopes and quota usage
4. Clients
Node, domain-wide delegation (key from a secret, never a committed file):
import { google } from 'googleapis';
const creds = JSON.parse(process.env.GOOGLE_SA_JSON!); // injected from secret manager
function clientFor(userEmail: string) {
const auth = new google.auth.JWT({
email: creds.client_email,
key: creds.private_key,
scopes: ['https://www.googleapis.com/auth/calendar.events'],
subject: userEmail, // impersonated user
});
return google.calendar({ version: 'v3', auth });
}
Node, keyless on Google Cloud (attached service account, no delegation):
const auth = new google.auth.GoogleAuth({ scopes: ['https://www.googleapis.com/auth/drive.file'] });
const drive = google.drive({ version: 'v3', auth });
Python, user OAuth with stored refresh token:
from google.oauth2.credentials import Credentials
from googleapiclient.discovery import build
creds = Credentials(
token=None,
refresh_token=user.decrypted_refresh_token,
token_uri="https://oauth2.googleapis.com/token",
client_id=os.environ["GOOGLE_CLIENT_ID"],
client_secret=os.environ["GOOGLE_CLIENT_SECRET"],
scopes=["https://www.googleapis.com/auth/calendar.events"],
)
calendar = build("calendar", "v3", credentials=creds, cache_discovery=False)
Refresh token rules: Google returns one only on first consent unless you pass
prompt=consent; External apps left in “Testing” status get refresh tokens that expire
after 7 days; tokens can be revoked by the user, by password changes (for Gmail scopes),
or when a user exceeds the per-client token limit. Handle invalid_grant by marking the
connection broken and asking the user to reconnect.
5. Sync per API
Calendar
- Expand recurrences for display:
events.list(singleEvents=true, orderBy='startTime', timeMin, timeMax)with RFC 3339 timestamps. - Incremental sync: do one full list, keep
nextSyncToken, then callevents.listwithsyncTokenonly. You cannot combinesyncTokenwithtimeMin,q, ororderBy. HTTP 410 Gone means the token is invalid: clear local state for that calendar and do a full sync. - Deleted events come back with
status: 'cancelled'; handle them. - Use
freebusy.queryfor availability instead of listing everyone’s events.
Drive
- Shared drives need
supportsAllDrives=trueand, for lists,includeItemsFromAllDrives=truepluscorpora. Missing these silently hides files. - Query with
q, e.g."'<folderId>' in parents and trashed = false". - Changes feed:
changes.getStartPageToken, thenchanges.list(pageToken)untilnewStartPageToken; persist it. - Uploads over ~5 MB use resumable upload. Google Docs/Sheets/Slides are exported with
files.export(size-limited) rather than downloaded withalt=media.
Gmail
- Send: base64url-encode an RFC 2822 message (build it with a MIME library) into
users.messages.send({ requestBody: { raw } }). Never hand-concatenate headers from user input (header injection). - Incremental sync: store the latest
historyId; callusers.history.list(startHistoryId). A 404 means the history is too old: do a full sync of the window you care about. - Fetch with
format=metadataandmetadataHeaderswhen you only need headers.
Sheets
- Read many ranges with
values.batchGet, write withvalues.batchUpdate; never loop per cell.valueInputOption=USER_ENTEREDparses formulas and dates like the UI;RAWstores strings as typed. - Per-minute read/write quotas are low per user and per project; a Sheet is a collaboration surface, not a database. Cache reads and batch writes.
6. Push notifications
Calendar and Drive use watch channels:
await calendar.events.watch({
calendarId: 'primary',
requestBody: {
id: crypto.randomUUID(), // channel ID, store it
type: 'web_hook',
address: `${process.env.PUBLIC_URL}/hooks/gcal`,
token: channelToken, // random per channel, stored server-side
},
});
On receipt: compare X-Goog-Channel-Token (constant-time) and X-Goog-Channel-ID
against stored values; reject otherwise. The first message has
X-Goog-Resource-State: sync and can be ignored. Notifications carry no event data, so
enqueue an incremental sync for that calendar and return 200. Channels expire (see the
expiration in the response); renew before expiry and call channels.stop for old ones.
Gmail uses Pub/Sub: create a topic, grant [email protected]
the Publisher role on it, call users.watch({ topicName, labelIds }), and renew the
watch at least every 7 days (daily is safer). For a push subscription, enable
authentication so Pub/Sub sends an OIDC token; verify its signature, aud, and service
account email on every request. The message carries emailAddress and historyId;
run history.list from your stored ID.
7. Quotas, errors, backoff
| Response | Meaning | Action |
|---|---|---|
| 401 | Access token expired or revoked | Let the client refresh; on invalid_grant mark disconnected |
403 insufficientPermissions / ACCESS_TOKEN_SCOPE_INSUFFICIENT | Scope missing | Re-consent with the scope, or fix the delegation scope list |
403 rateLimitExceeded / userRateLimitExceeded, 429 | Quota | Exponential backoff with jitter (1, 2, 4, 8… s, cap ~64 s), honor Retry-After |
403 domainPolicy / unauthorized_client | Admin has not allowed the app or scope | Admin console API controls |
| 410 | Sync token invalid | Full resync |
| 500/503 | Transient | Retry with backoff; make writes idempotent first |
Batch requests (multipart/mixed, up to 100 calls) save round trips but each inner call
still counts against quota. Check usage under APIs & Services > Quotas, and request
increases before launch, not during an outage.
Gotchas
- Domain-wide delegation fails with
unauthorized_clientwhen the scopes in code are not an exact subset of those the admin authorized for that client ID. - Service accounts have their own empty Drive with limited storage; files they create are owned by the SA. Write into a shared drive or a user-owned folder instead.
- Calendar
start.date(all-day) vsstart.dateTime(timed) are different shapes; always send atimeZonefor recurring events or DST shifts move them. - Watch channel addresses must be HTTPS with a valid certificate; localhost tunnels work for development only.
- Gmail
users.watchwithoutlabelIdsnotifies on every mailbox change, including your own label edits, which can loop if your handler modifies messages.
Verification
- Inspect a token’s granted scopes:
curl -s "https://oauth2.googleapis.com/tokeninfo?access_token=$TOKEN". - For delegation, run a one-off script that impersonates a test user and lists one item;
unauthorized_clientmeans Admin console scope mismatch. - Run full sync, change one item in the UI, confirm the push arrives and incremental sync applies exactly that change; then corrupt the stored sync token and confirm 410 triggers a clean resync.
- Load-test with realistic concurrency and confirm backoff keeps the 429 rate flat instead of spiraling.
About this skill
Written and maintained by the Claude Skills editors to the same editorial standard as every skill in the directory: a selective trigger, a real workflow, known pitfalls and a verification step. First published , last updated . MIT licensed.