Cremind
Agent SkillsBuilt-in Skills

gmail

Read, search, send, reply, label, and trash Gmail over OAuth2, and receive new-email events in real time — authorized through Cremind Connect, tokens stay local.

The gmail skill is a Python CLI plus an event listener for Gmail over OAuth2. It can read, search, send, reply to, label, and trash messages, and it can receive new-email events in real time. Authorization goes through the Cremind Connect service, so you never touch a Google Cloud project — and the tokens it mints stay on your machine. It runs via uv (PEP 723 inline metadata), so there's no environment to set up.

This page distills the skill's shipped SKILL.md. The skill is one of the built-ins Cremind copies into every profile, so it's available out of the box.

How it works (token-less relay)

The skill separates actions from events, and the relay never sees your data:

  • Actions (list, send, …) call the Gmail API directly with your local token.
  • Events: the listener calls Gmail users.watch() into the org's Pub/Sub topic (from the relay's discovery doc), then connects a WebSocket to the relay and proves account control with a short-lived Google ID token. When mail arrives, the relay sends a content-free resync nudge; the listener then runs history.list() locally and writes the new message to events/new_email/.
  • The same account linked in two Cremind apps receives events in both — the relay fans out to every connected app for that account.

The OAuth code-to-token exchange happens locally (loopback PKCE), and the token is stored only on this machine at scripts/.google_token.json. See Local Tokens & Secrets.

Setup

No configuration is required by default. CREMIND_CONNECT_URL defaults to https://connect.cremind.io, and the OAuth GOOGLE_CLIENT_ID / GOOGLE_CLIENT_SECRET are fetched dynamically from Cremind Connect (GET /credentials/google) so the org can rotate them without a client update. Set any of these in scripts/.env (or via the Settings UI) only to override:

CREMIND_CONNECT_URL=https://connect.cremind.io   # optional; this is the default
GOOGLE_CLIENT_ID=                                # optional; otherwise fetched from Cremind Connect
GOOGLE_CLIENT_SECRET=                            # optional; otherwise fetched from Cremind Connect

Linking the account

Link the account with the link subcommand:

uv run scripts/__main__.py link

link prints a Google consent URL and then waits in the background for consent to complete. Surface that URL to the user and ask them to open it and approve access. The consent redirect is received by the always-running Cremind backend (a persistent loopback listener), so linking completes even though the command keeps running in the background. Once the user confirms they've approved, check status:

uv run scripts/__main__.py status

--no-browser only affects the standalone fallback used when the Cremind backend isn't running; under the app the URL is always printed for the user.

CLI commands

Run uv run scripts/__main__.py <subcommand>. Output is JSON (human-readable on a TTY; force JSON with --json).

SubcommandRequiredOptional
link--no-browser
status
list--query, --max-results (10), --detail summary|full
search--query--max-results (10), --detail summary|full
get--id
send--to (repeatable), --subject--cc, --bcc (repeatable), --body/--body-file/stdin
reply--id--cc, --bcc, body via --body/--body-file/stdin
trash--id
watchestablish the Gmail watch once; the listener does this automatically
unwatch

--id is the Gmail message id (from list/search). --query uses Gmail search syntax, e.g. from:alice newer_than:7d.

Examples

uv run scripts/__main__.py status
uv run scripts/__main__.py list --max-results 5
uv run scripts/__main__.py search --query "from:boss is:unread"
uv run scripts/__main__.py get --id 1923abc...
uv run scripts/__main__.py send --to a@b.com --subject "Hi" --body "Hello there"
uv run scripts/__main__.py reply --id 1923abc... --body "Thanks!"
uv run scripts/__main__.py trash --id 1923abc...

The new_email event

The skill declares one event type, new_email, fired when a new message arrives in the Gmail INBOX. The listener:

uv run scripts/event_listener.py

Behavior:

  • Baseline on first run — records the current historyId; emits nothing for existing mail.
  • Live — on each relay resync nudge, runs incremental history.list() and writes new INBOX messages to events/new_email/<YYYY-MM-DDTHH-MM-SS> <subject>.md.
  • Catch-up — on startup it also syncs anything that arrived while offline.
  • Watch renewal — re-calls users.watch() well within Google's 7-day limit.
  • Offline more than ~7 days — if the historyId is too old the cursor is reset, and the bounded gap is not replayed (by design — no full-mailbox dump).
  • Statescripts/.listener_state.json (gitignored). Shuts down on SIGINT/SIGTERM.

Start and monitor the listener with the event CLI (see Event Subscriptions):

cremind skill-events listener-start gmail
cremind skill-events listener-status gmail

Event markdown schema

Each new email is dropped as a markdown file with structured frontmatter and a plain-text body:

---
id: "1923abc..."
thread_id: "1923a..."
message_id: "<CABc...@mail.gmail.com>"
from: "Alice <alice@example.com>"
to: "you@gmail.com"
cc: ""
subject: "Lunch?"
date: "Fri, 06 Jun 2026 09:00:00 +0000"
labels: ["INBOX", "UNREAD"]
event_type: "new_email"
received_at: "2026-06-06T09:00:05+00:00"
---

<plain-text body>

Troubleshooting

SymptomFix
Account not linkedRun uv run scripts/__main__.py link.
No GOOGLE_CLIENT_SECRET availableCremind Connect must be reachable (it serves the secret), or set it in scripts/.env to override.
Google did not return a refresh tokenRevoke at myaccount.google.com/permissions and re-link.
No events arrivingConfirm the listener is running, that link used openid email scopes, and that the relay is reachable (curl $CREMIND_CONNECT_URL/.well-known/cremind-connect).
Can't link a restricted scopeWhile the org's consent screen is in "Testing", only added test users can link.

Module layout

gmail/
├── SKILL.md
├── events/new_email/                 # markdown drop-zone
└── scripts/
    ├── .env                          # optional overrides (creds fetched from Cremind Connect by default)
    ├── __main__.py                   # CLI entry
    ├── event_listener.py             # listener entry
    └── app/
        ├── config.py                 # env + paths + logging
        ├── gmail_api.py              # Gmail API wrapper (watch/history/list/get/send/...)
        ├── formatter.py              # message parsing + markdown
        ├── listener.py               # watch lifecycle + relay client + incremental sync
        ├── cli.py                    # argparse + dispatch
        └── google/                   # shared: account_key, discovery, auth (PKCE), relay_client

On this page