Google Workspace APIs skill for Codex

Right principal, smallest scope, no stray keys. Codex picks the right identity for a Google Workspace integration before writing code: user OAuth, a plain service account, or domain-wide delegation, including keyless signing when key creation is blocked. It chooses the narrowest scopes, explains verification costs for restricted scopes, adds backoff for quota errors and decodes invalid_grant, unauthorized_client and accessNotConfigured into concrete fixes.

  • Integrations
  • $google-workspace-apis
  • 183 lines
  • 2 files
  • ~2,200 tokens
  • MIT
  • Updated

Install

curl -fsSL codexskills.dev/install.sh \  | sh -s -- google-workspace-apis

Installs to ~/.agents/skills/google-workspace-apis/ for every project on this machine.

104-line POSIX script. No sudo, no telemetry. Read install.sh

Use it in Codex

Start a prompt with $google-workspace-apis, or pick it from /skills. You can also just describe the task: Codex reads the description below and loads the playbook when your request matches.

What Codex reads before loading it

Sets up authentication for Google Workspace APIs (Drive, Gmail, Calendar, Sheets, Docs, Admin SDK, Chat) - choosing between user OAuth, service accounts, domain-wide delegation and keyless signing, picking least-privilege scopes, consent screen verification, quotas, batching and backoff with google-auth, google-api-python-client and googleapis for Node. Use when a Google API call fails with invalid_grant, unauthorized_client, accessNotConfigured or insufficientPermissions, or when starting any integration that acts on Workspace users' data.

Use it when

  • Starting any integration with Drive, Gmail, Calendar, Sheets, Docs or Admin SDK
  • Deciding between user OAuth, a service account and domain-wide delegation
  • A Google API call fails with invalid_grant, unauthorized_client or a 403
  • Your organisation blocks service account key creation

Use something else when

  • Auth already works and you need Drive file operations: use google-drive-api
  • Auth already works and you need mailbox sync or sending: use gmail-api
  • You are integrating a generic third-party OAuth provider: use oauth-integrations

How to check the result

Inspect the granted scopes on a refreshed token, make one read call that reports which user it ran as, then expire the cached token and confirm the job still runs unattended.

The mistake it prevents

Leaving an External OAuth app in Testing status, which silently expires refresh tokens after seven days.

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

--- name: google-workspace-apis description: Sets up authentication for Google Workspace APIs (Drive, Gmail, Calendar, Sheets, Docs, Admin SDK, Chat) - choosing between user OAuth, service… license: MIT ---

Done well means: the integration authenticates as the right principal for the job, requests the narrowest scopes that work, holds no long-lived key file if it can avoid one, and survives quota errors with backoff instead of crashing.

Choose the principal

SituationPrincipalNotes
A person runs a tool on their own dataUser OAuth (installed app or web flow)Refresh token per user; consent screen required
SaaS product acting for many customers’ usersUser OAuth, web flow, access_type=offlineSensitive/restricted scopes need Google verification
Backend job on files/calendars it owns or that are shared to itService account, no delegationLives outside any user; share resources to its email
Backend job acting as users of one Workspace domainService account + domain-wide delegation (DWD)A super admin authorises client ID + scopes
Admin SDK (users, groups, devices)DWD impersonating an admin, or admin OAuthDirectory API needs an admin identity
Public data only (no user data)API keyNever for Workspace user data

Ask the user which of these applies when it is not obvious. DWD is powerful: it can read every mailbox for the granted scopes. Do not propose it when per-user OAuth would do.

Before writing code

- [ ] Read AGENTS.md for secret handling and the existing Google client library
- [ ] Identify the Cloud project; enable each API (gcloud services enable drive.googleapis.com gmail.googleapis.com)
- [ ] Pick the principal (table above) and the exact scopes (table below)
- [ ] OAuth: configure the consent screen (Internal for single-domain tools, External otherwise)
- [ ] DWD: get the service account's numeric client ID for the admin to authorise
- [ ] Decide where tokens/keys live (secret manager, not the repo)

gcloud and every API call need network; in a Codex sandbox with network off, prepare code and commands and ask the user to run or approve them.

Scopes: narrowest that works

NeedPreferAvoid unless required
Files the app creates or the user picksdrive.file (non-sensitive)drive (restricted)
Read Drive metadatadrive.metadata.readonlydrive.readonly
Send mail onlygmail.send (sensitive)gmail.modify, mail.google.com
Read mailgmail.readonly (restricted)mail.google.com (full, incl. delete)
Calendar eventscalendar.eventscalendar
Sheets the app opens via pickerdrive.file + Sheets APIspreadsheets on all sheets
Directory users readadmin.directory.user.readonlyadmin.directory.user

All scopes are prefixed https://www.googleapis.com/auth/. Restricted scopes (most of Gmail, full Drive) require an annual third-party security assessment for External apps. In External apps left in “Testing” status, refresh tokens expire after 7 days: that, not a bug, is the usual cause of weekly invalid_grant.

User OAuth (Python)

from pathlib import Path
from google.auth.transport.requests import Request
from google.oauth2.credentials import Credentials
from google_auth_oauthlib.flow import InstalledAppFlow
from googleapiclient.discovery import build

SCOPES = ["https://www.googleapis.com/auth/drive.file"]
TOKEN = Path("~/.config/mytool/token.json").expanduser()   # outside the repo

def creds():
    c = Credentials.from_authorized_user_file(TOKEN, SCOPES) if TOKEN.exists() else None
    if c and c.expired and c.refresh_token:
        c.refresh(Request())
    elif not c or not c.valid:
        c = InstalledAppFlow.from_client_secrets_file("client_secret.json", SCOPES).run_local_server(port=0)
    TOKEN.parent.mkdir(parents=True, exist_ok=True)
    TOKEN.write_text(c.to_json())
    return c

drive = build("drive", "v3", credentials=creds(), cache_discovery=False)

Web apps: use the authorization-code flow with access_type=offline and prompt=consent only on first connect (forcing consent every time issues new refresh tokens and hits the per-client token cap). Store refresh tokens encrypted per user; handle revocation by asking the user to reconnect.

Service account with domain-wide delegation

from google.oauth2 import service_account
from googleapiclient.discovery import build

SCOPES = ["https://www.googleapis.com/auth/calendar.events"]
base = service_account.Credentials.from_service_account_file("/run/secrets/sa.json", scopes=SCOPES)
as_user = base.with_subject("[email protected]")            # impersonate this Workspace user
cal = build("calendar", "v3", credentials=as_user, cache_discovery=False)

Admin side (super admin, Admin console -> Security -> Access and data control -> API controls -> Domain-wide delegation): add the service account’s numeric client ID and the exact scope list. The scopes in code must be a subset of the authorised list or token exchange fails with unauthorized_client. Changes can take minutes to propagate.

Keyless: no JSON key at all

Many organisations block key creation (iam.disableServiceAccountKeyCreation). On GCP or with workload identity federation, sign the DWD assertion through the IAM Credentials API instead:

import google.auth
from google.auth import iam
from google.auth.transport.requests import Request
from google.oauth2 import service_account

SA = "[email protected]"
source, _ = google.auth.default(scopes=["https://www.googleapis.com/auth/cloud-platform"])
signer = iam.Signer(Request(), source, SA)
creds = service_account.Credentials(
    signer=signer, service_account_email=SA,
    token_uri="https://oauth2.googleapis.com/token",
    scopes=SCOPES, subject="[email protected]",
)

The runtime identity needs roles/iam.serviceAccountTokenCreator on that service account.

Node (googleapis)

import { google } from "googleapis";

const auth = new google.auth.JWT({
  keyFile: process.env.GOOGLE_SA_KEY_FILE,
  scopes: ["https://www.googleapis.com/auth/admin.directory.user.readonly"],
  subject: "[email protected]",
});
const admin = google.admin({ version: "directory_v1", auth });
const { data } = await admin.users.list({ customer: "my_customer", maxResults: 200 });

Quotas, batching, partial responses

  • Errors to retry with exponential backoff and jitter: 429, 500, 503, and 403 with reason rateLimitExceeded or userRateLimitExceeded. Do not retry 403 insufficientPermissions or accessNotConfigured.
  • Quotas are per project per minute and often per user per minute; spreading load across impersonated users does not raise the project limit.
  • Request only needed fields: fields="nextPageToken, files(id, name, modifiedTime)".
  • Batch HTTP requests (up to 100 calls per batch in most APIs, fewer for Gmail in practice) cut round trips but each inner call still counts against quota.
  • Python’s HttpRequest.execute(num_retries=5) adds backoff for retryable errors.

Error decoder

ErrorUsual causeFix
invalid_grantRefresh token revoked or expired (Testing status, user removed access, password change on Gmail scopes); clock skew for JWTRe-consent; publish the app; sync time
unauthorized_clientDWD not set or scopes in code exceed the authorised listMatch scopes exactly in Admin console
403 accessNotConfiguredAPI not enabled in the project that owns the credentialsEnable the API, wait a few minutes
403 insufficientPermissionsToken lacks the scopeRe-consent with the scope
404 on a known file/calendarService account (not impersonating) has no accessShare to the SA email or use DWD
admin_policy_enforcedWorkspace admin blocks the app or scopeAdmin must trust the app in API controls

Gotchas

  • A service account is not a Workspace user: it has no Gmail mailbox and, without DWD, sees nothing in users’ Drives or calendars.
  • with_subject must be a real user in the domain; groups and aliases fail.
  • Consumer @gmail.com accounts cannot be impersonated; DWD is Workspace-only.
  • Do not commit client_secret.json, token.json or service account keys; add them to .gitignore and scan before committing.
  • Apps Script is often the simpler answer for in-domain automations owned by one team.

Verification

  • Print the token’s granted scopes (creds.scopes after refresh, or call https://oauth2.googleapis.com/tokeninfo?access_token=...) and compare with intent.
  • Make one read call as the intended principal and log who it ran as (e.g. Drive about.get with fields="user").
  • Force a refresh (expire the cached token) and confirm the job still runs unattended.

For Drive specifics use $google-drive-api; for Gmail sync and push use $gmail-api.

What the zip contains

SKILL.md plus the interface file below. Nothing runs on install.

agents/openai.yamlgenerated from this page

Gives the Codex app a display name, a one-line description and a starter prompt for this skill.

interface:
  display_name: "Google Workspace APIs"
  short_description: "Right principal, smallest scope, no stray keys"
  default_prompt: "$google-workspace-apis Set up a service account that can create calendar events for anyone in our domain"

About this skill

Written and maintained by the Codex Skills editors to the same editorial standard as every skill here: a selective description, a real workflow, known pitfalls and a way to verify the work. First published , last updated . MIT licensed. Something wrong or out of date? Tell us.

esc
Just launched

Codex Skills Pro is live

79 premium skills for Codex, each with scripts, references and an evidence report that proves the result.

Bought one by one$882.21
All 79 skills$49
94% off

That's $0.62 a skill, paid once.

Get all 79 for $49Or pick single skills from $4.99 →

Secure checkout by Creem