Gmail

Email

Manage Gmail email, including drafting, sending, organizing, filters, vacation replies, and inbox analysis

Install
assistant skills install gmail

compatibility:Designed for Vellum personal assistants
metadata.vellum.user-invocable:true

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": "..." }
ScriptOperationDescription
gmail-email.tsdraftCreate email drafts in the Drafts folder (including reply drafts)
gmail-email.tssend-draftSend an existing draft (requires explicit user confirmation)
gmail-email.tsforwardCreate forward drafts, preserving attachments
gmail-email.tstrashMove messages to Trash
gmail-manage.tslabelAdd or remove labels on messages
gmail-manage.tsfollow-upTrack/untrack messages for follow-up using a dedicated "Follow-up" label
gmail-manage.tsattachmentsList and download email attachments
gmail-manage.tsfiltersCreate, list, and delete Gmail filters
gmail-manage.tsvacationGet, enable, or disable the vacation auto-responder
gmail-manage.tsunsubscribeUnsubscribe from mailing lists (requires explicit user confirmation)
gmail-scan.tssender-digestScan inbox and group messages by sender for declutter workflows
gmail-scan.tsoutreach-scanIdentify cold outreach senders (no List-Unsubscribe header)
gmail-archive.tsarchiveArchive messages (single, batch message_ids, cache_key+sender-emails, query)
gmail-archive.tsarchive --dry-runPreview what would be archived without executing (writes staged ops to log)
gmail-archive.tsarchive --resumeResume an interrupted archive run from its last checkpoint
gmail-commit.tscommitExecute all staged ops from a dry-run
gmail-commit.tscancelDelete a run log without executing anything
gmail-runs.tslistList recent operation runs with status summaries
gmail-runs.tsinspectShow detailed log entries for a specific run
gmail-runs.tspruneDelete operation logs older than 30 days
gmail-reverse.ts--run-idReverse all committed ops in a run (un-archive, un-label, un-trash)
gmail-reverse.ts--run-id --threadReverse a specific message within a committed run
gmail-prefs.tslistList blocklist and safelist preferences
gmail-prefs.tsadd-blocklistAdd sender emails to the blocklist
gmail-prefs.tsadd-safelistAdd sender emails to the safelist
gmail-prefs.tsremove-blocklistRemove sender emails from the blocklist
gmail-prefs.tsremove-safelistRemove sender emails from the safelist
gmail-prefs.tsget-management-configGet inbox management config (stage, interrupt threshold, last run)
gmail-prefs.tsset-management-configUpdate inbox management config (--stage, --interrupt-threshold, --last-run)

Email Operations

# 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

# 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

# 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:

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

Archive Operations

# 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.

# 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.

# 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:

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:

# 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:

# 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

# 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.

Communication Style

  • Be action-oriented. When the user asks to do something ("declutter", "check my email"), start doing it immediately. Don't ask for permission to read their inbox - that's obviously what they want.
  • Keep it human. Never mention OAuth, tokens, APIs, sandboxes, credential proxies, or other technical internals. If something isn't working, say "Gmail needs to be reconnected" - not "the OAuth2 access token for google has expired."
  • Show progress. When running a script that scans many emails, tell the user what you're doing: "Scanning your inbox for clutter..." Don't go silent.
  • Be brief and warm. One or two sentences per update is plenty. Don't over-explain what you're about to do - just do it and narrate lightly.

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:

OperatorExampleWhat it finds
from:from:alice@example.comMessages from a specific sender
to:to:bob@example.comMessages sent to a recipient
subject:subject:meetingMessages with a word in the subject
newer_than:newer_than:7dMessages from the last 7 days
older_than:older_than:30dMessages older than 30 days
is:unreadis:unreadUnread messages
has:attachmenthas:attachmentMessages with attachments
label:label:workMessages 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" means no unsubscribe link was found - these will be archived but you may continue receiving them."
  • Action buttons (exactly 2): "Archive & Unsubscribe" (primary), "Archive Only" (secondary). NEVER offer Delete, Trash, or any destructive action.
  1. Embed cache_key in button data: When constructing the action buttons, include the cache_key from the scan result in each button's data field. This ensures cache_key is forwarded automatically when the user clicks — the LLM does not need to recall it from earlier context:
    {
      "id": "archive_unsubscribe",
      "label": "Archive & Unsubscribe",
      "style": "primary",
      "data": { "cache_key": "<cache_key value here>" }
    }
    
  2. Wait for user action: Stop and wait. Do NOT proceed to archiving or unsubscribing until the user explicitly confirms which senders to clean up and which action to take. When the user clicks an action button you will receive an action message containing action data: { cache_key, selectedIds }:
    • selectedIds are sender IDs (base64-encoded email addresses) — NOT Gmail message IDs. Always use them as sender emails with cache_key, never as message_ids.
    • Show progress with steps for each phase (e.g., "Archiving 89 senders (2,400 emails)", "Unsubscribing from 72 senders"). Update each step as each phase finishes.
  3. Act on selection - batch, don't loop:
    • Archive all at once: Run bun run scripts/gmail-archive.ts archive --skip-confirm once with --cache-key (from action data) + --sender-emails set to all selected sender emails. The script resolves message IDs from the cache and batches the Gmail API calls internally - never loop sender-by-sender. Use --skip-confirm because the user already confirmed via the UI table.
    • Unsubscribe in bulk: If the action is "Archive & Unsubscribe", run bun run scripts/gmail-manage.ts unsubscribe for each sender that has hasUnsubscribe: true — but emit all unsubscribe calls in a single assistant response (parallel tool use) rather than one-at-a-time across separate turns.
  4. Accurate summary: The scan counts are exact - the messageCount shown in the table matches the number of messages archived. Format: "Cleaned up [total_archived] emails from [sender_count] senders. Unsubscribed from [unsub_count]."
  5. Ongoing protection offer: After reporting results, offer auto-archive filters:
    • "Want me to set up auto-archive filters so future emails from these senders skip your inbox?"
    • If yes, run bun run scripts/gmail-manage.ts filters --action create for each sender with --from set to the sender's email and --remove-labels "INBOX".
    • Then offer a recurring declutter schedule: "Want me to scan for new clutter monthly?"

Cold Outreach Cleanup

After the newsletter/promotions pass, offer to clean up cold outreach — unsolicited emails from senders without unsubscribe links. This catches sales pitches, recruiting spam, and mass outreach that newsletter filters miss.

  1. Scan: Run bun run scripts/gmail-scan.ts outreach-scan (default: last 90 days, senders without List-Unsubscribe headers). The scan includes a hasPriorReply flag per sender — true means the user has previously replied to that sender.
  2. Filter out known contacts: Exclude senders where hasPriorReply: true — these are conversations, not cold outreach. If the contacts skill is loaded, also cross-reference against Google Contacts and exclude matches.
  3. Classify senders using sample subjects, email domains, and message patterns. Categorize into:
    • Clear junk (pre-select for archive): loan/LOC offers, generic SaaS pitches, mass marketing from unknown domains, senders with random/concatenated domain names
    • Sales outreach (pre-select for archive): targeted product pitches with personalised subject lines ("Hi [name]", "for [company]"), outreach tool domains (apollo.io, outreach.io, lemlist.com, instantly.ai, etc.)
    • Potentially useful (deselect / keep by default): recruiting, investor outreach, partnership proposals, vendor introductions that reference the user's specific product or role
    • Ambiguous (deselect / keep by default): anything you're not confident about
  4. Present as a table following the same pattern as the newsletter workflow. Use two visual sections:
    • Pre-selected rows: clear junk + sales outreach
    • Deselected rows: potentially useful + ambiguous senders (user reviews these)
    • Caption: "Cold outreach from the last 90 days (senders without unsubscribe links). Pre-selected senders look like spam or sales pitches. Deselected senders may be useful — review before archiving."
  5. Archive on user action: Same flow as newsletter cleanup — wait for explicit user confirmation, then batch archive.

Key principle: Not all cold outreach is unwanted. Recruiting, investor, and partnership emails can be valuable. When uncertain, default to keeping the sender (deselected) and let the user decide.

Large Inbox Handling

When a scan returns truncated: true or timeBudgetExceeded: true, the inbox has more messages than a single scan pass can cover. Split subsequent scans by date range to ensure full coverage:

Pass 1: in:inbox older_than:90d                     (oldest backlog)
Pass 2: in:inbox newer_than:90d older_than:30d      (recent months)
Pass 3: in:inbox newer_than:30d older_than:7d       (recent weeks)
Pass 4: in:inbox newer_than:7d                      (this week)

Merge results from all passes before presenting the final table. Each pass covers a smaller window, reducing per-scan message count and avoiding timeouts. Only split when a scan actually reports truncation — most inboxes are handled fine in a single pass.

Edge Cases

  • Zero results: Tell the user "No newsletter emails found" and suggest broadening the query (e.g. removing the category filter or extending the date range)
  • Unsubscribe failures: Report per-sender success/failure; the unsubscribe script handles edge cases
  • Truncation handling: The scan covers up to 1,000 messages by default (cap 2,000). If truncated is true, the top senders are still captured. Offer to run additional date-range passes to cover the remaining messages (see Large Inbox Handling above).
  • Time budget exceeded: If the scan returns timeBudgetExceeded: true, present whatever results were collected. Offer to run additional date-range passes for uncovered periods.

Cleanup Preferences (Blocklist & Safelist)

The gmail-prefs.ts script persists sender preferences across cleanup sessions:

  • Blocklist: Sender emails archived in previous sessions. On future cleanups, pre-pass archive all blocklisted senders before scanning (use bun run scripts/gmail-archive.ts archive --query "from:email1 OR from:email2 ... in:inbox"). The user will see a confirmation prompt for the archive — once approved, the script proceeds.
  • Safelist: Sender emails the user explicitly deselected (chose to keep). Exclude these senders from future cleanup tables entirely.

Workflow integration

  1. Before scanning: Run bun run scripts/gmail-prefs.ts --action list. If blocklisted senders exist, offer to auto-archive them first ("I have N previously archived senders — want me to clean those up first?"). Remove safelisted senders from scan results before presenting the table.
  2. After archiving: Run bun run scripts/gmail-prefs.ts --action add-blocklist with the archived sender emails to persist them for future sessions.
  3. After user deselects: When the user deselects senders from a cleanup table, run bun run scripts/gmail-prefs.ts --action add-safelist with the deselected sender emails.
  4. User overrides: If the user asks to stop blocking or stop keeping a sender, use remove-blocklist or remove-safelist accordingly.

Company-Domain Sender Handling

When scan results include senders from the user's own company domain (e.g., @vellum.ai):

  • Protect individual colleagues: Cofounders, direct reports, teammates sending from their personal work address (e.g., akash@company.com, aaron@company.com) — deselect these from cleanup tables.
  • Archive mailing lists and group addresses: Addresses like devops@, alerts@, noreply@, security@, billing@ at the company domain are automated forwards, not personal correspondence. These are archivable by default — pre-select them in the cleanup table.
  • When uncertain: If a company-domain sender could be either a colleague or a mailing list (e.g., team@company.com), check sample subjects. Automated alerts, CI notifications, and vendor forwards → archivable. Direct messages with personal context → protect.

Do not blanket-protect an entire domain. The user's company domain often generates more automated noise than any external sender.

Scan Operations

Scan scripts (gmail-scan.ts sender-digest, gmail-scan.ts outreach-scan) return a cache_key that references message IDs stored in the assistant's cache. This keeps thousands of message IDs out of the conversation context.

  • Pass cache_key + sender emails to bun run scripts/gmail-archive.ts archive instead of individual message_ids
  • Cache entries expire after 30 minutes. When a cache entry expires, archiving automatically falls back to query-based archiving per sender.
  • To retrieve cached data: assistant cache get <cache_key> --json
  • Raw message_ids still work as a fallback for non-scan workflows

The outreach-scan operation enriches each sender with hasPriorReply (whether the user has ever sent an email to that address). Use this signal to filter out legitimate correspondents before classifying cold outreach.

Batch Operations

bun run scripts/gmail-archive.ts archive supports --cache-key + --sender-emails (preferred for declutter workflows), --message-ids, --message-id, or --query.
bun run scripts/gmail-manage.ts label supports --message-id or --message-ids only — it does not accept cache_key.
  • First scan to get a cache_key, then use the archive script to batch-archive by sender.
  • Always confirm with the user before batch operations on large numbers of messages.

Attachments

  • List attachments: bun run scripts/gmail-manage.ts attachments --action list --message-id "18f..." — returns filename, MIME type, size, and attachment ID for each attachment on a message.
  • Download attachment: bun run scripts/gmail-manage.ts attachments --action download --message-id "18f..." --attachment-id "ANGj..." --filename "report.pdf" — saves a specific attachment to disk.

Workflow: use attachments --action list to discover attachments, then attachments --action download to save them locally.

Common Workflows

Declutter Inbox

  1. Run bun run scripts/gmail-scan.ts sender-digest to scan for newsletters and promotions
  2. Present results showing senders, message counts, and unsubscribe availability
  3. Wait for user to select senders and confirm an action
  4. Archive selected senders and unsubscribe where available (if user chose "Archive & Unsubscribe")
  5. Offer to create filters for ongoing protection

Create a Gmail Filter

  1. User says "auto-archive emails from newsletters@example.com"
  2. Run bun run scripts/gmail-manage.ts filters --action create --from "newsletters@example.com" --remove-labels "INBOX"
  3. Confirm the filter was created and explain what it does

Set Vacation Auto-Reply

  1. User says "set my out-of-office for next week"
  2. Run bun run scripts/gmail-manage.ts vacation --action enable --message "I'm out of office until Monday"
  3. Confirm the auto-reply is active and when it expires

Manage Follow-ups

  1. User says "flag this email for follow-up" — run bun run scripts/gmail-manage.ts follow-up --action track --message-id "18f..."
  2. User says "what emails am I tracking?" — run bun run scripts/gmail-manage.ts follow-up --action list
  3. User says "mark that as done" — run bun run scripts/gmail-manage.ts follow-up --action untrack --message-id "18f..."

Identify Cold Outreach

  1. Run bun run scripts/gmail-scan.ts outreach-scan to find senders without unsubscribe headers
  2. Present results for review — these are likely cold outreach or unsolicited emails
  3. User can choose to archive, create filters to block, or ignore

Date Verification

Before composing any email that references a date or time:

  1. Check the current_time: field in the <turn_context> block for today's date and timezone
  2. Verify that "tomorrow" means the day after today's date, "next week" means the upcoming Monday–Friday, etc.
  3. If the email references a date from another message, cross-check it against the turn context to ensure it's in the future

Confidence Scores

Medium and high risk operations require a confidence score between 0 and 1:

  • 0.9-1.0: User explicitly requested this exact action
  • 0.7-0.8: Action is strongly implied by context
  • 0.5-0.6: Reasonable inference but some ambiguity
  • Below 0.5: Ask the user to confirm before proceeding
CreatorVellum
LicenseMIT
Updated1 month ago
SecurityVerified
View on GitHub

The Personal AI you were promised

GET STARTED