---
name: Gmail
slug: gmail-2
category: Automation
description: Gmail manages Gmail email — drafting, sending, organizing, filters, vacation replies, and inbox analysis. Use it when an agent needs to control a Gmail inbox.
github: "https://github.com/vellum-ai/vellum-assistant/tree/main/skills/gmail"
language: TypeScript
stars: 1095
forks: 160
install: "npx degit https://github.com/vellum-ai/vellum-assistant/tree/main/skills/gmail ~/.claude/skills/gmail"
installs_to: ~/.claude/skills/gmail
source_path: skills/gmail/SKILL.md
collection_size: 25
category_size: 1523
collection_url: "https://dirskills.com/collections/vellum-ai/vellum-assistant"
added: 2026-08-20T07:56:36.635Z
last_synced: 2026-08-20T07:56:36.635Z
canonical_url: "https://dirskills.com/skills/gmail-2"
---

# Gmail

Gmail manages Gmail email — drafting, sending, organizing, filters, vacation replies, and inbox analysis. Use it when an agent needs to control a Gmail inbox.

**Install:**

```bash
npx degit https://github.com/vellum-ai/vellum-assistant/tree/main/skills/gmail ~/.claude/skills/gmail
```

## README

This skill provides Gmail-specific operations beyond the shared **messaging** skill. For cross-platform messaging (send, read, search, reply), use the messaging skill. Gmail operations depend on the messaging skill's provider infrastructure - load messaging first if Gmail is not yet connected.

## Script Reference

All operations use CLI scripts that return JSON:

- **Success**: `{ "ok": true, "data": ... }`
- **Failure**: `{ "ok": false, "error": "..." }`

| Script             | Operation               | Description                                                                  |
| ------------------ | ----------------------- | ---------------------------------------------------------------------------- |
| `gmail-email.ts`   | `draft`                 | Create email drafts in the Drafts folder (including reply drafts)            |
| `gmail-email.ts`   | `send-draft`            | Send an existing draft (**requires explicit user confirmation**)             |
| `gmail-email.ts`   | `forward`               | Create forward drafts, preserving attachments                                |
| `gmail-email.ts`   | `trash`                 | Move messages to Trash                                                       |
| `gmail-manage.ts`  | `label`                 | Add or remove labels on messages                                             |
| `gmail-manage.ts`  | `follow-up`             | Track/untrack messages for follow-up using a dedicated "Follow-up" label     |
| `gmail-manage.ts`  | `attachments`           | List and download email attachments                                          |
| `gmail-manage.ts`  | `filters`               | Create, list, and delete Gmail filters                                       |
| `gmail-manage.ts`  | `vacation`              | Get, enable, or disable the vacation auto-responder                          |
| `gmail-manage.ts`  | `unsubscribe`           | Unsubscribe from mailing lists (**requires explicit user confirmation**)     |
| `gmail-scan.ts`    | `sender-digest`         | Scan inbox and group messages by sender for declutter workflows              |
| `gmail-scan.ts`    | `outreach-scan`         | Identify cold outreach senders (no List-Unsubscribe header)                  |
| `gmail-archive.ts` | `archive`               | Archive messages (single, batch message_ids, cache_key+sender-emails, query) |
| `gmail-archive.ts` | `archive --dry-run`     | Preview what would be archived without executing (writes staged ops to log)  |
| `gmail-archive.ts` | `archive --resume`      | Resume an interrupted archive run from its last checkpoint                   |
| `gmail-commit.ts`  | `commit`                | Execute all staged ops from a dry-run                                        |
| `gmail-commit.ts`  | `cancel`                | Delete a run log without executing anything                                  |
| `gmail-runs.ts`    | `list`                  | List recent operation runs with status summaries                             |
| `gmail-runs.ts`    | `inspect`               | Show detailed log entries for a specific run                                 |
| `gmail-runs.ts`    | `prune`                 | Delete operation logs older than 30 days                                     |
| `gmail-reverse.ts` | `--run-id`              | Reverse all committed ops in a run (un-archive, un-label, un-trash)          |
| `gmail-reverse.ts` | `--run-id --thread`     | Reverse a specific message within a committed run                            |
| `gmail-prefs.ts`   | `list`                  | List blocklist and safelist preferences                                      |
| `gmail-prefs.ts`   | `add-blocklist`         | Add sender emails to the blocklist                                           |
| `gmail-prefs.ts`   | `add-safelist`          | Add sender emails to the safelist                                            |
| `gmail-prefs.ts`   | `remove-blocklist`      | Remove sender emails from the blocklist                                      |
| `gmail-prefs.ts`   | `remove-safelist`       | Remove sender emails from the safelist                                       |
| `gmail-prefs.ts`   | `get-management-config` | Get inbox management config (stage, interrupt threshold, last run)           |
| `gmail-prefs.ts`   | `set-management-config` | Update inbox management config (--stage, --interrupt-threshold, --last-run)  |

### Email Operations

```bash
# Draft an email
bun run scripts/gmail-email.ts draft --to "user@example.com" --subject "Hello" --body "Message body"

# Draft a reply in a thread
bun run scripts/gmail-email.ts draft --to "user@example.com" --subject "Re: Hello" --body "Reply body" --thread-id "18f..." --in-reply-to "18f..."

# Send an existing draft (REQUIRES user confirmation before execution)
bun run scripts/gmail-email.ts send-draft --draft-id "r123..."

# Forward a message
bun run scripts/gmail-email.ts forward --message-id "18f..." --to "recipient@example.com" --text "FYI"

# Trash a message
bun run scripts/gmail-email.ts trash --message-id "18f..."
```

### Management Operations

```bash
# Label - add a label to a message
bun run scripts/gmail-manage.ts label --message-id "18f..." --add-labels "Work,Important"

# Follow-up - track a message for follow-up
bun run scripts/gmail-manage.ts follow-up --action track --message-id "18f..."

# Attachments - list attachments on a message
bun run scripts/gmail-manage.ts attachments --action list --message-id "18f..."

# Attachments - download a specific attachment
bun run scripts/gmail-manage.ts attachments --action download --message-id "18f..." --attachment-id "ANGj..." --filename "report.pdf"

# Filters - create a Gmail filter
bun run scripts/gmail-manage.ts filters --action create --from "newsletter@example.com" --remove-labels "INBOX"

# Vacation - enable auto-responder
bun run scripts/gmail-manage.ts vacation --action enable --message "I'm out of office until Monday"

# Unsubscribe from a mailing list (REQUIRES user confirmation before execution)
bun run scripts/gmail-manage.ts unsubscribe --message-id "18f..."
```

### Scan Operations

```bash
# Sender digest - scan inbox grouped by sender
bun run scripts/gmail-scan.ts sender-digest [--query "in:inbox category:promotions newer_than:90d"]

# Outreach scan - identify cold outreach senders
bun run scripts/gmail-scan.ts outreach-scan [--time-range "90d"]
```

Scan scripts store message IDs in the assistant's cache and return a lightweight summary with a `cache_key`. This keeps thousands of message IDs out of the conversation context. To retrieve cached message IDs for a specific sender:

```bash
assistant cache get <cache_key> --json
# Returns: { "ok": true, "data": { "sender@example.com": ["msgId1", "msgId2", ...], ... } }
```

### Archive Operations

```bash
# Archive using cache_key + sender emails (preferred for declutter workflows)
bun run scripts/gmail-archive.ts archive --cache-key "scan:abc123" --sender-emails "news@example.com,promo@example.com"

# Archive specific message IDs
bun run scripts/gmail-archive.ts archive --message-ids "18f1...,18f2...,18f3..."

# Archive a single message
bun run scripts/gmail-archive.ts archive --message-id "18f..."

# Archive by query
bun run scripts/gmail-archive.ts archive --query "from:newsletter@example.com in:inbox"
```

### Operation Runs

All destructive archive operations are logged to a JSONL operation log for resumability and auditing. Each batch of archives is tracked as a "run" with a unique ID.

```bash
# List recent runs with status summaries
bun run scripts/gmail-runs.ts list [--limit 20]

# Show detailed log entries for a specific run
bun run scripts/gmail-runs.ts inspect --run-id "run_20260420_a1b2c3d4"

# Delete logs older than 30 days
bun run scripts/gmail-runs.ts prune
```

#### Dry-Run Mode

Dry-run mode runs the full pipeline (scanning, collecting message IDs) but skips all destructive API calls. Staged entries are written to the op log for review.

```bash
# Preview what would be archived
bun run scripts/gmail-archive.ts archive --query "..." --dry-run

# Review the staged operations
bun run scripts/gmail-runs.ts inspect --run-id "<run-id>"

# Commit the staged operations (executes the archive)
bun run scripts/gmail-commit.ts commit --run-id "<run-id>"

# Cancel (delete the log, nothing executed)
bun run scripts/gmail-commit.ts cancel --run-id "<run-id>"
```

Label and filter operations also support `--dry-run`:

```bash
bun run scripts/gmail-manage.ts label --message-ids "..." --add-labels "..." --dry-run
bun run scripts/gmail-manage.ts filters --action create --from "..." --remove-labels "INBOX" --dry-run
```

Archive operations now return a `run_id` in their output. Use this to resume interrupted runs:

```bash
# Resume an interrupted run (e.g. after daily quota hit)
bun run scripts/gmail-archive.ts archive --resume "run_20260420_a1b2c3d4"

# Pass --run-id to group multiple archive calls under one run
bun run scripts/gmail-archive.ts archive --query "..." --run-id "run_20260420_a1b2c3d4" --phase "noise_archive"
```

When a run is interrupted (e.g. Gmail daily quota exceeded), the operation log records the interruption with a resume hint. The assistant should detect interrupted runs and offer to resume them rather than starting fresh.

#### Reversing a Run

If a committed run archived messages incorrectly, reverse it:

```bash
# Reverse all committed operations in a run (requires confirmation)
bun run scripts/gmail-reverse.ts --run-id "run_20260420_a1b2c3d4"

# Reverse a specific message within a run (no confirmation needed)
bun run scripts/gmail-reverse.ts --run-id "run_20260420_a1b2c3d4" --thread "18f..."
```

Reversal semantics:

- **archive** → adds INBOX label back (un-archives)
- **label_add** → removes the labels that were added
- **label_remove** → adds back the labels that were removed
- **trash** → removes TRASH label and adds INBOX (un-trashes, within Gmail's 30-day window)
- **filter_create** → not auto-reversible; delete manually via `gmail-manage.ts filters --action delete`

Reversals are themselves logged as runs (auditable, resumable). The reversal only touches labels/state that the original run modified — it does not touch user-applied labels.

### Preferences Operations

```bash
# List all preferences
bun run scripts/gmail-prefs.ts --action list

# Add senders to blocklist
bun run scripts/gmail-prefs.ts --action add-blocklist --emails "spam@example.com,junk@example.com"

# Add senders to safelist
bun run scripts/gmail-prefs.ts --action add-safelist --emails "important@example.com"

# Remove from blocklist
bun run scripts/gmail-prefs.ts --action remove-blocklist --emails "spam@example.com"

# Remove from safelist
bun run scripts/gmail-prefs.ts --action remove-safelist --emails "important@example.com"
```

## Email Routing Priority

When the user mentions "email" - sending, reading, checking, decluttering, drafting, or anything else - **always default to the user's own email (Gmail)** unless they explicitly ask about the assistant's own email address (e.g., "set up your email", "send from your address", "check your inbox"). The vast majority of email requests are about the user's Gmail, not the assistant's @vellum.me address.

Do not offer the assistant's own email as an option unless the user specifically asks. If Gmail is not connected, guide them through Gmail setup.

## Connection Setup

### Gmail

1. **Check connection health first.** Run `assistant oauth status google`. This checks whether the user's Google account is connected and the token is valid.
2. **If no connection is found or the status check fails:** Load the `vellum-oauth-integrations` skill. The skill will evaluate whether managed or your-own mode is appropriate and guide the user accordingly.

## Error Recovery

When a Gmail script fails with a token or authorization error:

1. **Try to reconnect silently.** Run `assistant oauth ping google`. This often resolves expired tokens automatically.
2. **If reconnection fails, go straight to setup.** Don't present options, ask which route the user prefers, or explain what went wrong technically. Just tell the user briefly (e.g., "Gmail needs to be reconnected - let me set that up") and immediately load the `vellum-oauth-integrations` skill. The user came to you to get something done, not to troubleshoot - make it seamless.
3. **Never try alternative approaches.** Don't use curl, browser automation, or any workaround. If the scripts can't do it, the reconnection flow is the answer.
4. **Never expose error details.** The user doesn't need to see error messages about tokens, OAuth, or API failures. Translate errors into plain language.

## Safety: Send-Draft and Unsubscribe

Two operations are **high-risk** and require explicit user confirmation before execution:

- **`send-draft`**: Always show the user what will be sent (recipients, subject, body summary) and wait for explicit confirmation ("yes", "send it", etc.) before running the send-draft script. The script gates on `assistant ui confirm` — it will present a confirmation dialog to the user and only proceed if approved. Never auto-send.
- **`unsubscribe`**: Always tell the user which mailing list will be unsubscribed from and wait for explicit confirmation before running the unsubscribe script. The script gates on `assistant ui confirm`. Unsubscribe actions cannot be undone.

If the user has not explicitly confirmed, do not execute these operations. A general instruction like "clean up my inbox" is not confirmation to unsubscribe - it means scan and present options.

## Drafting vs Sending (Gmail)

Gmail uses a **draft-first workflow**. All compose and reply operations create Gmail drafts automatically:

- `bun run scripts/gmail-email.ts draft` creates a draft in Gmail Drafts
- `bun run scripts/gmail-email.ts draft` with `--thread-id` and `--in-reply-to` creates a threaded reply draft
- `bun run scripts/gmail-email.ts forward` creates a forward draft, preserving attachments
- The messaging skill's send operation with Gmail also creates a draft

**To actually send**: Use `bun run scripts/gmail-email.ts send-draft` with the draft ID after the user has reviewed it. Only run send-draft when the user explicitly says "send it" or equivalent.

## Email Threading (Gmail)

When replying to or continuing an email thread:

- **Preferred**: Use the messaging skill's send with the thread's `thread_id` — it automatically handles threading, reply-all recipients, and subject lines.
- **Manual drafting**: Use `bun run scripts/gmail-email.ts draft` with both `--thread-id` and `--in-reply-to` for full control. The `thread_id` places the draft in the correct Gmail thread; `in_reply_to` sets the RFC 822 threading headers.
- **Getting the Message-ID**: Search and read results include `rfc822MessageId` in message metadata (looks like `<CABx...@mail.gmail.com>`). This is the value to pass as `--in-reply-to`. You can also pass a Gmail message ID directly — the draft script auto-resolves it to the RFC 822 header.

## Gmail Search Syntax

When searching Gmail, the query uses Gmail's search operators:

| Operator         | Example                  | What it finds                       |
| ---------------- | ------------------------ | ----------------------------------- |
| `from:`          | `from:alice@example.com` | Messages from a specific sender     |
| `to:`            | `to:bob@example.com`     | Messages sent to a recipient        |
| `subject:`       | `subject:meeting`        | Messages with a word in the subject |
| `newer_than:`    | `newer_than:7d`          | Messages from the last 7 days       |
| `older_than:`    | `older_than:30d`         | Messages older than 30 days         |
| `is:unread`      | `is:unread`              | Unread messages                     |
| `has:attachment` | `has:attachment`         | Messages with attachments           |
| `label:`         | `label:work`             | Messages with a specific label      |

## Email Decluttering

When a user asks to declutter, clean up, or organize their email - start scanning immediately. Don't ask what kind of cleanup they want or request permission to read their inbox. Go straight to scanning - but once results are ready, always present them and let the user choose actions before archiving or unsubscribing.

**CRITICAL**: Never archive, unsubscribe, or take similar bulk actions unless the user has explicitly confirmed for that specific batch. Each batch of results requires its own explicit user confirmation. If the user says "keep going" or "keep decluttering," that means scan and present new results - NOT auto-archive. Previous batch approvals do not carry forward, but **deselections DO carry forward**: when the user deselects senders from a cleanup batch, run `bun run scripts/gmail-prefs.ts --action add-safelist` with those sender emails. Before building the next cleanup table, run `bun run scripts/gmail-prefs.ts --action list` and exclude safelisted senders from the table — the user already indicated they want to keep those.

### Inbox Recon (run before cleanup passes)

Before starting category-specific cleanup, understand the inbox:

1. **Broad scan**: Run `bun run scripts/gmail-scan.ts sender-digest --query "in:inbox"` with `--max-senders 75`. This surfaces the top senders across ALL categories — not just promotions.
2. **Identify cleanup buckets**: Group the results mentally:
   - Newsletters/promotions (`hasUnsubscribe: true`) → handle in promotions pass
   - Mailing lists / automated forwards (group addresses like `devops@`, `alerts@`, `noreply@`) → handle in general noise pass
   - Reminder chains (DocuSign, GCP, Auth0 — same sender, many messages) → handle in general noise pass
   - Automated receipts (Brex, Stripe, OpenAI credits) → handle in general noise pass
   - Cold outreach (unfamiliar senders, no unsubscribe) → handle in outreach pass
   - Real correspondence → skip
3. **Design passes**: Use the recon results to decide which passes to run and in what order. If 80% of the inbox is promotions, start there. If the biggest bucket is mailing-list forwards, start with a targeted query for that address.
4. **General noise pass**: After promotions and before outreach, run `bun run scripts/gmail-scan.ts sender-digest --query "in:inbox -category:promotions -category:personal"` to catch mailing lists, reminder chains, and automated notifications that aren't categorized as promotions. Present as a table following the same pattern as the promotions pass. Pre-select automated/noise senders, deselect anything that looks like real correspondence.

### Workflow

1. **Scan**: Run `bun run scripts/gmail-scan.ts sender-digest`. Default query targets promotions currently in the inbox from the last 90 days (`in:inbox category:promotions newer_than:90d`). The script returns a `cache_key` plus a lightweight sender summary (counts, unsubscribe availability, sample subjects). Message IDs are stored in the assistant's cache — do NOT ask for them unless needed for archiving. Counts shown in the table reflect only what is currently in the inbox — these are the emails that will be archived.
2. **Present**: Show results as a table with `selectionMode: "multiple"`:
   - **Columns (exactly 3)**: Sender, Emails Found, Unsub?
     - **Unsub? cell values**: Use rich cell format: `{ "text": "Yes", "icon": "checkmark.circle.fill", "iconColor": "success" }` when `hasUnsubscribe` is true, `{ "text": "No", "icon": "minus.circle", "iconColor": "muted" }` when false.
   - **Pre-select all rows** (`selected: true`) - users deselect what they want to keep
   - **Caption**: Include two parts separated by a newline: (1) data scope, e.g. "Newsletters, notifications, and outreach from last 90 days. Deselect anything you want to keep." (adjusted to match the query used), and (2) the Unsub? column legend: "Unsub? - \"Yes\" means these emails contain an unsubscribe link, so I can opt you out automatically. \"No\" m
