Google Workspace Skill
July 27, 2026 · View on GitHub
CLI tool for Google Docs, Gmail, and Calendar automation via the official Python SDK. All output is JSON — built for both direct use and AI agent consumption.
What it does
- Create, search, modify, share, and delete Google Docs
- Publish Markdown files to Google Docs with full formatting (headings, bold, italic, lists, tables, blockquotes, inline code, links, horizontal rules)
- Idempotent
synccommand: Markdown files carry their own Google Doc binding via YAML front matter, so repeated updates land on the same document - Insert local images into documents
- List, reply to, and resolve comments
- Full tab management: list, add, rename, replace content
- Download, search, read, export, inspect cached headers, draft, send, reply to, archive, trash, and label Gmail messages, including reply-all drafts in existing threads
- Create Calendar events with invites and list upcoming Calendar events
Quick install
git clone https://github.com/grapeot/gdocs-skill.git
cd gdocs-skill
uv venv
source .venv/bin/activate
uv pip install -e .
Dependencies: google-api-python-client, google-auth, google-auth-oauthlib, and pyyaml.
OAuth setup (one-time)
This repo uses a bring-your-own OAuth client model. You create a Google Cloud project and download your own credentials. No shared client secrets are distributed.
Step 1: Create a Google Cloud project
- Go to Google Cloud Console
- Create a new project (name it e.g.
gdocs-skill) - Wait a few seconds for the project to be created, then confirm the project selector shows the new name
Step 2: Enable APIs
- Search for "Google Docs API" in the console search bar and click ENABLE
- Search for "Google Drive API" and click ENABLE
- Search for "Gmail API" and click ENABLE
Verify at the API Dashboard — all enabled APIs should appear there.
Step 3: Configure OAuth consent screen
- Navigate to "OAuth consent screen" in the left sidebar
- Choose External as the user type (unless you have Google Workspace and only need internal use)
- Fill in required fields: App name (any), user support email, developer contact email
- On the Scopes page, click ADD OR REMOVE SCOPES and add:
https://www.googleapis.com/auth/documentshttps://www.googleapis.com/auth/drive.filehttps://www.googleapis.com/auth/gmail.modifyhttps://www.googleapis.com/auth/calendar.events
- On the Test users page, click ADD USERS and add your own Gmail address
- Critical: skipping this step causes
Error 403: access_deniedinstead of the authorization prompt. The app runs in testing mode, and only listed test users can authorize.
- Critical: skipping this step causes
- Save and return to dashboard
Step 4: Create OAuth credentials
- Go to Credentials
- Click + CREATE CREDENTIALS → OAuth client ID
- Choose Desktop app as the application type
- Name it anything (e.g.
gdocs-skill-desktop) - Click CREATE, then click DOWNLOAD JSON in the dialog
Step 5: Install the credentials file
mv ~/Downloads/client_secret_*.json secrets/credentials.json
chmod 600 secrets/credentials.json
If you closed the download dialog, go back to the Credentials page, find your OAuth client in the list, and click the download icon on the right.
Step 6: Verify
python -m gdocs create --title "Smoke test"
On first run, a browser window opens for Google authorization. After authorizing, secrets/token.json is created and subsequent runs require no interaction.
If you previously authorized the tool before Gmail or Calendar support existed, delete secrets/token.json and run python -m gdocs gmail profile or python -m gdocs calendar list-events --time-min 2026-01-01T00:00:00Z to reauthorize with the new scopes.
Usage examples
# Publish a Markdown file as a new Google Doc
python -m gdocs publish report.md --title "Q4 Report"
# Idempotent sync — first run creates the doc, later runs update it
python -m gdocs sync report.md --title "Q4 Report"
# Search your docs
python -m gdocs search "quarterly review"
# Share a document
python -m gdocs share DOC_ID --email colleague@example.com --role writer
# List unresolved comments
python -m gdocs comment list DOC_ID --unresolved-only
# Insert an image
python -m gdocs image DOC_ID chart.png
# Check Gmail profile
python -m gdocs gmail profile
# Download recent inbox messages to data/mail/
python -m gdocs gmail download --days 7 --limit 100 --label INBOX
# Search Gmail server-side with native Gmail query syntax
python -m gdocs gmail search "from:user@example.com newer_than:7d"
# Inspect locally cached RFC headers for threading experiments
python -m gdocs gmail inspect --gmail-id MSG_ID --thread
# Send an email; use --dry-run first when testing
python -m gdocs gmail draft --subject "Hello" --body-file body.md
python -m gdocs gmail send --to user@example.com --subject "Hello" --body-file body.md --dry-run
# Save a reply-all draft in an existing Gmail thread without sending
python -m gdocs gmail reply --gmail-id MSG_ID --body-file reply.md --reply-all --draft
# Create a Calendar event and invite attendees
python -m gdocs calendar create-event --summary "Planning" --start "2026-05-20T10:00:00-07:00" --end "2026-05-20T10:30:00-07:00" --attendee user@example.com
# List Calendar events in a time window
python -m gdocs calendar list-events --time-min "2026-05-20T00:00:00-07:00" --time-max "2026-05-21T00:00:00-07:00"
Command reference
| Command | Purpose |
|---|---|
publish <file> --title TITLE | Publish a Markdown file as a new Google Doc (one-shot, no binding) |
sync <file> [--title TITLE] | Idempotent sync: creates on first run, updates on subsequent runs via front matter |
create --title TITLE | Create an empty document |
delete DOC_ID [--permanent] | Move to trash (default, recoverable 30 days) or permanently delete |
search QUERY [--max-results N] | Full-text search across accessible Google Docs |
share DOC_ID --email EMAIL [--role ROLE] [--message MSG] | Share a document with a user |
title DOC_ID NEW_TITLE | Rename a document |
link DOC_ID [--public] | Get the shareable link, optionally enabling public access |
tab list DOC_ID | List all tabs in a document |
tab add DOC_ID TITLE [FILE] [--format FMT] | Add a new tab, optionally with content from a file |
tab delete DOC_ID TAB_ID | Delete a tab |
tab rename DOC_ID TAB_ID NEW_TITLE | Rename a tab |
tab replace DOC_ID TAB_ID FILE [--format FMT] | Replace a tab's content from a file |
image DOC_ID IMAGE_PATH [--index N] [--width W] [--tab-id ID] | Insert a local image |
comment list DOC_ID [--unresolved-only] | List document comments |
comment reply DOC_ID COMMENT_ID TEXT | Reply to a comment |
comment resolve DOC_ID COMMENT_ID | Mark a comment as resolved |
gmail profile | Show authenticated Gmail address and mailbox stats |
gmail download [--days N] [--limit N] [--label L] [--query Q] | Download messages to the local .eml cache |
gmail search QUERY [--limit N] [--label L] | Search Gmail server-side with native Gmail query syntax |
gmail list-local [--limit N] | List locally cached messages |
gmail read [--gmail-id ID] [--subject S] [--from F] [--latest] [--index N] [--full] | Read a cached message body |
gmail inspect --gmail-id ID [--thread] | Inspect cached raw .eml RFC headers for threading experiments without Gmail API calls |
gmail export-md [--limit N] [--subject S] [--from F] [--output-dir DIR] [--unsafe-output-dir] [--force] | Export cached messages as Markdown |
gmail draft [--to ADDR] --subject S --body-file F [--cc ADDR] [--bcc ADDR] [--body-format FMT] | Create a Gmail draft without sending |
gmail send --to ADDR --subject S --body-file F [--cc ADDR] [--bcc ADDR] [--body-format FMT] [--dry-run] | Send an email |
gmail reply --gmail-id ID --body-file F [--reply-all] [--draft/--dry-run] [--to ADDR] [--cc ADDR] [--body-format FMT] | Send, preview, or save a draft reply in an existing Gmail thread |
gmail archive GMAIL_ID [--dry-run] | Remove the INBOX label |
gmail trash GMAIL_ID [--dry-run] | Move a message to trash |
gmail mark-read GMAIL_ID [--dry-run] | Mark a message as read |
gmail mark-unread GMAIL_ID [--dry-run] | Mark a message as unread |
gmail label list | List Gmail labels |
gmail label apply GMAIL_ID --label LABEL [--dry-run] | Apply a label |
gmail label remove GMAIL_ID --label LABEL [--dry-run] | Remove a label |
calendar create-event --summary S --start ISO --end ISO [--attendee EMAIL] [--calendar-id ID] [--description D] [--location L] [--timezone TZ] | Create a Calendar event and send attendee updates |
calendar list-events --time-min ISO [--time-max ISO] [--calendar-id ID] [--max-results N] | List Calendar events ordered by start time |
All commands output JSON to stdout. Errors go to stderr as {"error": ..., "status_code": ..., "response": ...}. Exit code 0 on success, 1 on error.
Markdown support
Headings (H1–H3), bold, italic, bold+italic, inline code, hyperlinks, unordered lists, ordered lists, horizontal rules, blockquotes, and native tables. See docs/prd.md for the full format table.
Safety
- OAuth credentials stored in
secrets/— excluded from git via.gitignore, files set to mode600 - OAuth scopes:
documents(Docs content read/write),drive.file(file-level access only),gmail.modify(Gmail read/send/label operations), andcalendar.events(Calendar event create/list operations) - Token auto-refresh; automatic re-authorization if refresh fails
- Retries on transient API errors (HTTP 429 and 5xx) with exponential backoff (1s, 2s, 4s)
- Gmail cache stored in
data/mail/by default — excluded from git because it contains private email data
The global --mail-data-dir flag overrides the Gmail cache location. The default layout is data/mail/messages/ for raw .eml, data/mail/markdown/ for exports, and data/mail/mail.db for SQLite metadata. gmail inspect reads only this cache, including thread summaries only for messages already present locally. gmail export-md --output-dir refuses paths outside data/mail/ unless --unsafe-output-dir is set, because exported Markdown contains private email bodies.
Documentation
- Feature reference — what the tool can do
- Architecture decisions — why it works the way it does
- Changelog — history and lessons learned
- AI agent skill file — complete CLI reference with troubleshooting