Google Docs Skill

July 27, 2026 · View on GitHub

Purpose

This CLI tool enables AI agents and scripts to operate on Google Docs and Gmail through the official Google Python SDK. Every operation produces JSON output for programmatic consumption. The tool is designed for consumption by AI agents within a workspace, but works equally well from any terminal.

Document Lifecycle

Create — Produce a new Google Doc with optional tab structures and Markdown-formatted content.

Delete — Move to trash (default, recoverable for 30 days) or permanently remove. Intended for cleaning up test and scaffold documents.

Update title — Rename the Google Doc via Drive metadata (not Docs content API).

Search — Full-text search across accessible documents via Drive API. Returns doc ID, title, link, and modification time. Search scope is limited to files the app created or the user explicitly opened with it, due to the drive.file scope.

Markdown Publishing

Markdown content is converted to Google Docs native formatting through a three-phase pipeline: parse (identify blocks and inline segments), flatten (strip markup to plain text), and generate (produce API formatting requests).

Supported Markdown to Google Docs mappings:

MarkdownGoogle Docs rendering
# HeadingHeading 1
## HeadingHeading 2
### HeadingHeading 3
**bold**Bold
*italic*Italic
***bold italic***Bold + Italic
`code`Courier New monospace
[text](url)Hyperlink
- item / * itemUnordered list
1. itemOrdered list
--- / *** / `___$\text{Horizontal} \text{rule} (\text{gray} \text{centered} \text{line}, 30 \times ━)
$> quote`Blockquote (left indent 36pt + gray left border)
`col | col |`

Internet links and inline code are supported within all block types. Unsupported by design: code blocks with syntax highlighting, merged table cells, column widths, alignment control, and images embedded within Markdown. Images must be inserted as a separate step.

Idempotent Sync

The sync command solves the problem of repeatedly updating the same Markdown file and needing changes reflected in the same Google Doc. Without sync, each publish creates a new document, and finding the right document to update requires searching through past sessions.

Design: The Markdown file carries its own Google Doc binding via YAML front matter:

---
gdoc_id: <Google Doc ID>
gdoc_tab_id: <tab ID, defaults to t.0>
gdoc_last_synced: <ISO 8601 timestamp>
---

# Document body

This means the binding stays with the file — moving or renaming it preserves the mapping. The mapping is git-trackable and human-readable. The document body (everything after the front matter) is what gets written to Google Docs, so readers see clean content.

Behavior: python -m gdocs sync file.md resolves the target document by checking, in order: the --gdoc-id CLI argument, the file's front matter gdoc_id field, or neither (triggers creation). If a doc ID is found, the tool replaces the relevant tab's content. If not, it creates a new document and writes the ID back to the file's front matter. The --dry-run flag previews what would happen without side effects.

See docs/rfc.md for the full decision tree and architecture rationale.

Tab Management

Google Docs supports native document tabs since approximately October 2024. This tool works with real tabs, not heading-based simulations. Supported operations:

  • List all tabs in a document (ID and title)
  • Add a new tab, optionally with content from a file
  • Rename a tab
  • Replace a tab's entire content from a file

Sharing

Share documents with specific users via email address. Supported roles: reader (view only), commenter (view and comment), writer (full edit). An optional notification message can be included.

Generate public "anyone with the link" access by setting --public on the link command. Public links set allowFileDiscovery=False to prevent search engine indexing.

Comments

Read and manage document comments via Drive API v3:

  • List all comments (with optional filter for unresolved only)
  • Reply to a comment
  • Resolve a comment (mark it as closed)

Each comment entry includes author, content, the quoted text it references, creation time, resolution status, and any replies.

Image Insertion

Insert local images (PNG, JPEG, GIF) into a document at a specific character index, or appended to the end of a tab. Images are uploaded to Google Drive and made publicly readable for inline display within the document. Width defaults to 468 points (full width of a standard Google Doc body). When inserting images with Markdown content that references them (e.g., ![alt](path.png)), insert images after publishing the Markdown, working from bottom to top of the document to avoid index drift from each insertion.

Gmail Integration

Implementation status: Complete for the first Gmail milestone. The implemented surface covers download, search, local list/read/export, standalone and in-thread drafts, send, reply, archive, trash, read/unread state, and label operations.

The Gmail pipeline covers server-side search, local download, local read/export, sending, replying, and message state changes. Gmail commands are grouped under python -m gdocs gmail .... Mutating operations support --dry-run where a preview is useful.

CommandDescription
gmail profileShow authenticated Gmail address and mailbox stats
gmail downloadSearch Gmail and download raw .eml to local cache
gmail searchServer-side search with native Gmail query syntax
gmail list-localList messages in the local SQLite cache
gmail readRead a cached message body, truncated at 10,000 chars unless --full is used
gmail inspectInspect cached raw .eml headers for threading experiments without Gmail API calls
gmail export-mdExport cached messages as Markdown files with YAML front matter
gmail sendSend an email via the Gmail API
gmail replySend, preview, or save an in-thread draft using In-Reply-To, References, and Gmail threadId; optional reply-all excludes the authenticated account and deduplicates recipients
gmail archiveRemove the INBOX label
gmail trashMove a message to Gmail Trash
gmail mark-readRemove the UNREAD label
gmail mark-unreadAdd the UNREAD label
gmail label listList system and user-defined labels
gmail label applyAdd a label to a message
gmail label removeRemove a label from a message

Downloaded messages are stored under data/mail/ by default:

data/mail/
├── messages/          # Raw .eml files
├── markdown/          # Markdown exports
└── mail.db            # SQLite metadata index

The SQLite index supports local filtering by Gmail ID, subject, sender, and thread ID. Raw .eml files preserve MIME content. gmail inspect --gmail-id ID [--thread] parses the cached raw headers and, when requested, reports only same-thread messages already present locally. Messages are deduplicated by gmail_id during download. The global --mail-data-dir flag overrides the cache location. Markdown exports default to data/mail/markdown/; export paths outside data/mail/ require --unsafe-output-dir because they contain private email bodies.

Gmail uses labels rather than folders. Archive removes the INBOX label rather than moving the message. User labels and system labels such as INBOX, UNREAD, STARRED, IMPORTANT, SENT, DRAFT, TRASH, SPAM, and CATEGORY_* can be resolved by name or ID.

Output Contract

All commands write a single JSON object to stdout on success. On error, a JSON error object is written to stderr:

{
  "error": "Failed to create document 'Title': HTTP 503 — <response body>",
  "status_code": 503,
  "response": "<API response body or null>"
}

Exit code is 0 on success, 1 on error. Transient API errors (HTTP 429 and 5xx) are retried up to 4 times with exponential backoff (1s, 2s, 4s) before surfacing the error.

Architecture

gdocs/
├── __main__.py      # CLI entry point: argparse, subcommand routing, sync orchestrator
├── client.py        # GoogleDocsClient — wraps Docs and Drive APIs
├── gmail_client.py  # GmailClient — wraps Gmail API
├── mail_store.py    # MailStore — SQLite + .eml local cache
├── auth.py          # OAuth credential management for Docs, Drive, and Gmail scopes
├── markdown.py      # Markdown to Google Docs API request translator (3-phase pipeline)
└── frontmatter.py   # YAML front matter parse/serialize for sync

GoogleDocsClient and GmailClient are the API wrapper classes, initialized once per CLI invocation with a secrets directory. CLI command handlers instantiate the relevant client and route to the appropriate method. MailStore handles local persistence for Gmail. No state is shared between invocations beyond filesystem-stored credentials and the optional Gmail cache. Sync orchestration lives in __main__.py (not in client.py) to keep API wrapping and workflow logic separate.

Out of Scope

  • Google Sheets, Slides, or Calendar
  • Multi-user session management
  • Merged table cells or column width control
  • Code blocks with syntax highlighting
  • Image embedding within Markdown content
  • Document templates