Google Workspace Skill for Hermes Agent: Gmail, Calendar, Drive, Docs, Sheets
Gmail, Calendar, Drive, Docs, Sheets via gws CLI or Python.
Written by Neura Market from the official Hermes Agent documentation for Google Workspace. Commands, paths, and version numbers are reproduced from the source unchanged.
Read the official documentationThe Google Workspace skill gives a Hermes Agent hands-on access to Gmail, Calendar, Drive, Contacts, Sheets, and Docs through a single OAuth grant and a thin command-line wrapper. It is the skill you reach for when an agent needs to read a mailbox, check a schedule, pull a spreadsheet, or file a document as part of a larger task. If all you need is email, there is a lighter option, and the setup section below tells you when to take it.
What it does
The skill bundles two pieces of Python: a one-time OAuth setup script and an API wrapper that exposes each Google service as a small set of subcommands. Every command returns JSON, so an agent can parse results directly or pipe them through jq. When the gws CLI is present on the machine, the wrapper uses it as the execution backend for broader Workspace coverage; when it is not, the bundled Python client handles the same operations. Either way the JSON output contract stays the same, so downstream logic does not care which backend ran.
In practice that means an agent can search a mailbox with Gmail query syntax, read a full message body, send or reply with threading handled for you, list and create calendar events, search and upload Drive files, read and write Sheets ranges, and read, create, or append to Docs. The skill also ships two reference files: one on Gmail search operators and one describing a daily brief procedure that combines calendar and mail.
Before you start
The skill is bundled with Hermes Agent, so there is nothing to install from a marketplace. It lives at skills/productivity/google-workspace, is version 1.2.0, authored by Nous Research under the MIT licence, and runs on linux, macos, and windows.
What you do need is a Google Cloud OAuth client. That is a one-time, roughly five-minute exercise in the Google Cloud Console, and it is the part most people underestimate. You will create or select a project, enable the APIs you intend to use, create a Desktop app OAuth client, and download its JSON file. If your app is still in Testing mode, you also add your own Google account as a test user. The setup script walks an agent through this, but a human has to click through the console.
One platform caveat from the source: if you are driving setup from the Hermes CLI and the JSON file path starts with /, do not send the bare path as its own message. It can be mistaken for a slash command. Put it in a sentence instead.
First-time setup
The setup is fully non-interactive by design. The agent drives it step by step, which is what makes it work over CLI, Telegram, Discord, or any other surface. Run the setup script with the Python from the Hermes environment, not some unrelated system Python. Define a shorthand first:
GSETUP="python ${HERMES_HOME:-$HOME/.hermes}/skills/productivity/google-workspace/scripts/setup.py"
If Hermes is not importable in that environment, run hermes setup first rather than installing packages with pip. The --install-deps flag syncs Hermes' declared Google extra through PM; after that sync, restart Hermes and rerun the OAuth command.
Step 0: Check if already set up
$GSETUP --check
If it prints AUTHENTICATED, setup is already done and you can skip to usage.
Step 1: Triage the request
Before any OAuth work, the agent should ask two questions. The first is what services the user actually needs. Email only means this skill is the wrong tool: use the himalaya skill instead, which works with a Gmail App Password from Settings, Security, App Passwords and takes about two minutes with no Google Cloud project. Email plus Calendar means continuing here but passing --services email,calendar so the consent screen only asks for what is needed. Calendar, Drive, Sheets, or Docs without email means a narrower set like calendar,drive,sheets,docs. Full Workspace access means the default all set.
The second question is whether the Google account uses Advanced Protection, meaning hardware security keys are required to sign in. Most people will not have enrolled in it. If they have, their Workspace admin must add the OAuth client ID to the org's allowed apps list before the code exchange in Step 4 will work, and they should know that upfront.
Step 2: Create OAuth credentials
The agent tells the user to create a Google Cloud OAuth client. The steps are: create or select a project at the project selector dashboard, enable the required APIs from the API Library (Gmail API, Google Calendar API, Google Drive API, Google Sheets API, Google Docs API, People API), create the OAuth client under Credentials as an OAuth 2.0 Client ID with application type Desktop app, add the user as a test user under Audience if the app is still in Testing, and download the JSON file. Once the user provides the path:
$GSETUP --client-secret /path/to/client_secret.json
If the user pastes raw client ID and secret values instead of a file path, write a valid Desktop OAuth JSON file yourself, save it somewhere explicit such as ~/Downloads/hermes-google-client-secret.json, and run --client-secret against that file.
Step 3: Get the authorization URL
Use the service set chosen in Step 1. The examples below cover the three common shapes:
$GSETUP --auth-url --services email,calendar --format json
$GSETUP --auth-url --services calendar,drive,sheets,docs --format json
$GSETUP --auth-url --services all --format json
This returns JSON with an auth_url field and also saves the exact URL to ~/.hermes/google_oauth_last_url.txt. The agent should extract auth_url and send that exact URL to the user as a single line. Tell the user the browser will likely fail on http://localhost:1 after approval, and that this is expected. Tell them to copy the entire redirected URL from the address bar. If they hit Error 403: access_denied, send them to https://console.cloud.google.com/auth/audience to add themselves as a test user.
Step 4: Exchange the code
The user pastes back either the full redirect URL or just the code string; both work. The --auth-url step stores a temporary pending OAuth session locally so --auth-code can complete the PKCE exchange later, which is what makes this work on headless systems:
$GSETUP --auth-code "THE_URL_OR_CODE_THE_USER_PASTED" --format json
If the exchange fails because the code expired, was already used, or came from an older browser tab, the command returns a fresh fresh_auth_url. Send that new URL immediately and have the user retry with the newest redirect only.
Step 5: Verify
$GSETUP --check
It should print AUTHENTICATED. From then on the token refreshes automatically.
Setup notes
The token lives at ~/.hermes/google_token.json and auto-refreshes. Pending OAuth session state and verifier sit temporarily at ~/.hermes/google_oauth_pending.json until the exchange completes. If gws is installed, google_api.py points it at the same ~/.hermes/google_token.json credentials file, so there is no separate gws auth login flow to run. To revoke access, run:
$GSETUP --revoke
Usage
Every operational command goes through the API script. Set a second shorthand:
GAPI="python ${HERMES_HOME:-$HOME/.hermes}/skills/productivity/google-workspace/scripts/google_api.py"
Gmail
# Search (returns JSON array with id, from, subject, date, snippet)
$GAPI gmail search "is:unread" --max 10
$GAPI gmail search "from:boss@company.com newer_than:1d"
$GAPI gmail search "has:attachment filename:pdf newer_than:7d"
# Read full message (returns JSON with body text)
$GAPI gmail get MESSAGE_ID
# Send
$GAPI gmail send --to user@example.com --subject "Hello" --body "Message text"
$GAPI gmail send --to user@example.com --subject "Report" --body "<h1>Q4</h1><p>Details...</p>" --html
$GAPI gmail send --to user@example.com --subject "Hello" --from '"Research Agent" <user@example.com>' --body "Message text"
# Reply (automatically threads and sets In-Reply-To)
$GAPI gmail reply MESSAGE_ID --body "Thanks, that works for me."
$GAPI gmail reply MESSAGE_ID --from '"Support Bot" <user@example.com>' --body "Thanks"
# Labels
$GAPI gmail labels
$GAPI gmail modify MESSAGE_ID --add-labels LABEL_ID
$GAPI gmail modify MESSAGE_ID --remove-labels UNREAD
Search takes Gmail's own query syntax, so operators like is:unread, from:, newer_than:, has:attachment, and filename: all work. --max caps the result count. get returns the full message including body text. send supports plain text, HTML with --html, and a custom --from display name. reply threads automatically and sets the In-Reply-To header for you. labels lists label IDs, and modify adds or removes them, which is how you mark something read or file it.
Calendar
# List events (defaults to next 7 days)
$GAPI calendar list
$GAPI calendar list --start 2026-03-01T00:00:00Z --end 2026-03-07T23:59:59Z
# Create event (ISO 8601 with timezone required)
$GAPI calendar create --summary "Team Standup" --start 2026-03-01T10:00:00-06:00 --end 2026-03-01T10:30:00-06:00
$GAPI calendar create --summary "Lunch" --start 2026-03-01T12:00:00Z --end 2026-03-01T13:00:00Z --location "Cafe"
$GAPI calendar create --summary "Review" --start 2026-03-01T14:00:00Z --end 2026-03-01T15:00:00Z --attendees "alice@co.com,bob@co.com"
# Delete event
$GAPI calendar delete EVENT_ID
list defaults to the next seven days, and you can widen or shift the window with --start and --end. Event creation requires ISO 8601 timestamps with a timezone offset or a Z for UTC. Attendees are passed as a comma-separated string. Deletion takes an event ID.
Drive
# Search existing files
$GAPI drive search "quarterly report" --max 10
$GAPI drive search "mimeType='application/pdf'" --raw-query --max 5
# Get metadata for a single file
$GAPI drive get FILE_ID
# Upload a local file (auto-detects MIME type)
$GAPI drive upload /path/to/report.pdf
$GAPI drive upload /path/to/image.png --name "Logo.png" --parent FOLDER_ID
# Download (binary files download as-is; Google-native files export to a
# sensible default — Docs→pdf, Sheets→csv, Slides→pdf, Drawings→png)
$GAPI drive download FILE_ID
$GAPI drive download DOC_ID --output ~/doc.pdf
$GAPI drive download DOC_ID --export-mime text/plain --output ~/doc.txt
# Create a folder
$GAPI drive create-folder "Reports"
$GAPI drive create-folder "Q4" --parent FOLDER_ID
# Share
$GAPI drive share FILE_ID --email alice@example.com --role reader
$GAPI drive share FILE_ID --email alice@example.com --role writer --notify
$GAPI drive share FILE_ID --type anyone --role reader # anyone with link
$GAPI drive share FILE_ID --type domain --domain example.com --role reader
# Delete — defaults to trash (reversible). Use --permanent to skip the trash.
$GAPI drive delete FILE_ID
$GAPI drive delete FILE_ID --permanent
Search accepts plain text by default; --raw-query lets you pass Drive query syntax such as a MIME type filter. Upload detects MIME type from the file and accepts an optional --name and --parent. Download handles binary files as-is, while Google-native files export to a default format: Docs to pdf, Sheets to csv, Slides to pdf, Drawings to png. You can override with --export-mime and choose the destination with --output. Sharing supports individual emails, anyone with the link, or a whole domain, with roles like reader and writer, and --notify sends a notification. Delete goes to the trash by default, which is reversible; --permanent skips the trash.
Contacts
$GAPI contacts list --max 20
Lists contacts up to the given maximum.
Sheets
# Create a new spreadsheet
$GAPI sheets create --title "Q4 Budget"
$GAPI sheets create --title "Inventory" --sheet-name "Stock"
# Read
$GAPI sheets get SHEET_ID "Sheet1!A1:D10"
# Write
$GAPI sheets update SHEET_ID "Sheet1!A1:B2" --values '[["Name","Score"],["Alice","95"]]'
# Append rows
$GAPI sheets append SHEET_ID "Sheet1!A:C" --values '[["new","row","data"]]'
Create takes a title and optionally a sheet name. Read and write both use A1 notation for the range. update takes a JSON array of rows, and append adds rows after existing data in the range.
Docs
# Read (a tabbed Doc returns a "tabs" array; single-tab and legacy Docs also return "body")
$GAPI docs get DOC_ID
$GAPI docs get DOC_ID --tab TAB_ID # read one tab of a tabbed Doc
# Create a new Doc (optionally seeded with body text)
$GAPI docs create --title "Meeting Notes"
$GAPI docs create --title "Draft" --body "First paragraph..."
# Append text to the end of an existing Doc
$GAPI docs append DOC_ID --text "Additional content to append"
$GAPI docs append DOC_ID --tab TAB_ID --text "..." # --tab required when the Doc has multiple tabs
Reading a tabbed Doc returns a tabs array; single-tab and legacy Docs also return body. You can read one tab with --tab. Create takes a title and an optional body seed. Append adds text to the end of a Doc, and --tab is required when the Doc has multiple tabs.
Output format
Every command returns JSON. Parse it with jq or read it directly. The key shapes are: Gmail search returns an array of {id, threadId, from, to, subject, date, snippet, labels}; Gmail get returns {id, threadId, from, to, subject, date, labels, body}; Gmail send and reply return {status: "sent", id, threadId}. Calendar list returns an array of {id, summary, start, end, location, description, htmlLink}, and create returns {status: "created", id, summary, htmlLink}. Drive search returns {id, name, mimeType, modifiedTime, webViewLink} entries; get adds size, parents, and owners; upload returns {status: "uploaded", id, name, mimeType, webViewLink}; download returns {status: "downloaded", id, name, path, mimeType}; create-folder returns {status: "created", id, name, webViewLink}; share returns {status: "shared", permissionId, fileId, role, type}; delete returns {status: "trashed" | "deleted", fileId, permanent}. Contacts list returns {name, emails, phones} entries. Sheets get returns a two-dimensional array of cells; create returns {status: "created", spreadsheetId, title, spreadsheetUrl}. Docs create returns {status: "created", documentId, title, url}, and append returns {status: "appended", documentId, inserted_at, characters}.
When not to use it
If the only thing you need is email, skip this skill. The himalaya skill works with a Gmail App Password and takes about two minutes with no Google Cloud project, which is a much smaller commitment than the OAuth client setup here. The Google Workspace skill earns its setup cost when you need Calendar, Drive, Sheets, or Docs alongside mail, or when you want the agent to combine several of those services in one task.
Limits and gotchas
The source is explicit about a few rules. Never send email, create or delete calendar events, delete Drive files, share files, or modify Docs or Sheets without confirming with the user first. Show what will be done, including recipients, file IDs, content, and share role, and ask for approval. For drive delete, prefer the default trash over --permanent. Check auth before first use with setup.py --check, and guide the user through setup if it fails. For complex Gmail queries, load the search syntax reference with skill_view("google-workspace", file_path="references/gmail-search-syntax.md"). Calendar times must always include a timezone, either an ISO 8601 offset or Z. And respect rate limits: avoid rapid-fire sequential API calls and batch reads where possible.
The troubleshooting table covers the common failures. NOT_AUTHENTICATED means running setup steps 2 through 5. REFRESH_FAILED means the token was revoked or expired, so redo steps 3 through 5. HttpError 403: Insufficient Permission means a missing API scope, fixed by $GSETUP --revoke and redoing steps 3 through 5. AUTHENTICATED (partial) or a token missing scopes message means new write capabilities such as Drive write and delete or Docs create and edit need re-authorization, again via revoke and redo. HttpError 403: Access Not Configured means the API is not enabled in the Google Cloud Console. ModuleNotFoundError means running $GSETUP --install-deps. Advanced Protection blocking auth means the Workspace admin must allowlist the OAuth client ID.
What pairs with it
The related skill is himalaya, the lighter email-only path described above. The skill also ships two references worth knowing about: references/gmail-search-syntax.md for Gmail operators, and references/daily-brief.md, which describes a morning brief procedure combining schedule, conflicts, meeting prep, and urgent mail. Load the daily brief reference when a user asks for a morning brief, meeting preparation, or what is on their calendar and what email needs attention.