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.

  • namegoogle-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.

~/.claude/skills/google-workspace-apis/SKILL.md

--- name: google-workspace-apis description: Builds integrations with Google Workspace APIs - Drive, Calendar, Gmail, and Sheets - choosing between user OAuth, service accounts with… license: MIT ---

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 dataUseNotes
End users of a multi-tenant app (any Google account)OAuth 2.0 authorization code flow, access_type=offlineStore per-user refresh token; see oauth-oidc for the flow itself
All users in one Workspace domain, admin-approved, background jobsService 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 identityService account without delegationShare 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/IPNever 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.

NeedPreferAvoid unless required
Files the app creates or the user picksdrive.file (non-sensitive)drive, drive.readonly (restricted)
Read calendarscalendar.readonly or calendar.events.readonlycalendar
Create eventscalendar.eventscalendar
Send mail onlygmail.send (sensitive)gmail.modify, mail.google.com (restricted)
Read mailgmail.metadata if headers sufficegmail.readonly (restricted)
Sheets the user picksdrive.file + Sheets APIspreadsheets 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 call events.list with syncToken only. You cannot combine syncToken with timeMin, q, or orderBy. 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.query for availability instead of listing everyone’s events.

Drive

  • Shared drives need supportsAllDrives=true and, for lists, includeItemsFromAllDrives=true plus corpora. Missing these silently hides files.
  • Query with q, e.g. "'<folderId>' in parents and trashed = false".
  • Changes feed: changes.getStartPageToken, then changes.list(pageToken) until newStartPageToken; persist it.
  • Uploads over ~5 MB use resumable upload. Google Docs/Sheets/Slides are exported with files.export (size-limited) rather than downloaded with alt=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; call users.history.list(startHistoryId). A 404 means the history is too old: do a full sync of the window you care about.
  • Fetch with format=metadata and metadataHeaders when you only need headers.

Sheets

  • Read many ranges with values.batchGet, write with values.batchUpdate; never loop per cell. valueInputOption=USER_ENTERED parses formulas and dates like the UI; RAW stores 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

ResponseMeaningAction
401Access token expired or revokedLet the client refresh; on invalid_grant mark disconnected
403 insufficientPermissions / ACCESS_TOKEN_SCOPE_INSUFFICIENTScope missingRe-consent with the scope, or fix the delegation scope list
403 rateLimitExceeded / userRateLimitExceeded, 429QuotaExponential backoff with jitter (1, 2, 4, 8… s, cap ~64 s), honor Retry-After
403 domainPolicy / unauthorized_clientAdmin has not allowed the app or scopeAdmin console API controls
410Sync token invalidFull resync
500/503TransientRetry 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_client when 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) vs start.dateTime (timed) are different shapes; always send a timeZone for 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.watch without labelIds notifies 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_client means 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.

esc