gmail-triage

Daily multi-account Gmail inbox triage — scan unread, classify by urgency, cross-reference contacts, generate actionable digest. Supports ace/personal/skestates accounts.

5 stars

Best use case

gmail-triage is best used when you need a repeatable AI agent workflow instead of a one-off prompt.

Daily multi-account Gmail inbox triage — scan unread, classify by urgency, cross-reference contacts, generate actionable digest. Supports ace/personal/skestates accounts.

Teams using gmail-triage should expect a more consistent output, faster repeated execution, less prompt rewriting.

When to use this skill

  • You want a reusable workflow that can be run more than once with consistent structure.

When not to use this skill

  • You only need a quick one-off answer and do not need a reusable workflow.
  • You cannot install or maintain the underlying files, dependencies, or repository context.

Installation

Claude Code / Cursor / Codex

$curl -o ~/.claude/skills/gmail-triage/SKILL.md --create-dirs "https://raw.githubusercontent.com/vamseeachanta/workspace-hub/main/.agents/skills/email/gmail-triage/SKILL.md"

Manual Installation

  1. Download SKILL.md from GitHub
  2. Place it in .claude/skills/gmail-triage/SKILL.md inside your project
  3. Restart your AI agent — it will auto-discover the skill

How gmail-triage Compares

Feature / Agentgmail-triageStandard Approach
Platform SupportNot specifiedLimited / Varies
Context Awareness High Baseline
Installation ComplexityUnknownN/A

Frequently Asked Questions

What does this skill do?

Daily multi-account Gmail inbox triage — scan unread, classify by urgency, cross-reference contacts, generate actionable digest. Supports ace/personal/skestates accounts.

Where can I find the source code?

You can find the source code on GitHub using the link provided at the top of the page.

SKILL.md Source

# Gmail Triage

Scan all 3 Gmail accounts, classify emails, cross-reference contacts, and produce an actionable digest.

## Prerequisites

- himalaya configured with 3 accounts (see `gmail-multi-account` skill)
- Contact CSVs available in respective repos

## Core Principle: Email is a QUEUE, Not an ARCHIVE

Do NOT save all emails to repos. The workflow is:

```
INBOUND → TRIAGE → EXTRACT DATA → ACT → DELETE
                                      ↓
                          Topic completed? → DELETE email
                          Awaiting reply?  → KEEP (live)
                          New reply arrives → RE-ACTIVATE
```

Key rules:
1. Extract only structured DATA/information needed (not raw email bodies)
2. Delete the email when the topic is completed
3. Keep email alive when awaiting response from other party
4. When client/other party responds, the topic is live again
5. Learn from patterns — extraction and routing improve over time

See GitHub issue #2017 for the full workflow design.

## Triage Workflow

### Step 1: Scan all inboxes

```bash
for acct in ace personal skestates; do
  echo "=== $acct ==="
  himalaya --account $acct envelope list --page-size 50 --output json
done
```

### Step 2: Classify each email

Categories (priority order):
1. **URGENT** — from VIP/client contacts, contains "urgent", "asap", "deadline", invoice/payment
2. **ACTIONABLE** — requires response, question asked, meeting request, RFP
3. **FYI** — informational, no action needed, CC'd
4. **NEWSLETTER** — marketing, subscription content, bulk sender
5. **SPAM** — unknown sender, no contact match, suspicious

### Step 3: Cross-reference contacts

For each sender:
1. Search contact CSV for the account
2. If found: use contact category (client/vendor/recruiter/personal)
3. If NOT found: flag as "unknown sender" — recommend add-to-contacts or unsubscribe

### Step 4: Generate digest

Format:
```
=== GMAIL DAILY DIGEST — {date} ===

[ACE] vamsee.achanta@aceengineer.com
  URGENT (2):
    - From: client@company.com | Subject: RFP Response Deadline
    - From: vendor@co.com | Subject: Invoice #1234 Past Due
  ACTIONABLE (3):
    - ...
  FYI (5): [collapsed]
  NEWSLETTER (12): [collapsed, unsubscribe candidates marked]

[PERSONAL] achantav@gmail.com
  ...

[SKESTATES] skestatesinc@gmail.com
  ...

=== RECOMMENDED ACTIONS ===
1. Reply to client@company.com RE: RFP (ACE)
2. Review invoice from vendor@co.com (ACE)
3. Unsubscribe from 8 newsletters (PERSONAL)
4. Add 2 unknown senders to contacts or block
```

## Communication Style Profiles

When drafting suggested responses in the digest, load the per-account style profile:

```
config/email/ace-style.yaml       — professional-technical, concise (2-5 sentences)
config/email/personal-style.yaml  — casual-terse, ultra-short (1-2 sentences)
config/email/skestates-style.yaml — business-formal-warm, medium (3-6 sentences)
```

Each profile defines greeting, closing, signature, tone, and formality rules.
Match the profile to the sending account and check `formality_rules` for context-specific adjustments.

See `config/email/README.md` for full usage guide. Reference: #1986.

## Account-Specific Classification Rules

### ace
- VIP: anyone in GTM prospect list, active clients
- URGENT: anything from @ril.com, @lng-agroup.com, @mcdermott.com (known clients)
- EXTRACT (not noise): CRE listing senders — sandsig.com, marcusmillichap.com, loopnet.com,
  partnersrealestate.com, ten-x.ccsend.com, c.costarmail.com. These feed structured data
  extraction to assethold/data/cre-listings/ via the cre-listing template (#1991).
  Do NOT classify as NEWSLETTER or recommend unsubscribe.
- NEWSLETTER: LinkedIn notifications, industry digests (keep subscribed but low priority)
- NOISE: confirmed noise domains listed in config/email-filters/ace-noise-domains.yaml —
  safe to delete and unsubscribe (collide.io, skylineseven.ccsend.com, etc.)

### personal
- VIP: family (achanta*, @gmail.com family addresses)
- URGENT: banks, government, medical
- NEWSLETTER: aggressive unsubscribe candidates

### skestates
- VIP: TX_Rents@familydollar.com, leaseadministration@familydollar.com
- URGENT: insurance, tax, legal, tenant maintenance requests
- NEWSLETTER: real estate marketing (unsubscribe)

## Automation

This skill is designed to run as a cron job:

```
# Daily at 7 AM CT
0 7 * * * hermes "Load gmail-triage skill. Scan all 3 accounts and deliver digest."
```

Deliver to: Telegram or CLI local file at `~/.hermes/email-digests/`

## Gmail API Direct Usage Pattern (no dependencies)

For headless server automation, use the Gmail REST API directly with urllib (no pip deps):

```python
import json, os, urllib.request, urllib.parse, base64
from email.mime.text import MIMEText
from email.mime.multipart import MIMEMultipart

# OAuth config (shared file, never hardcoded)
cfg_path = os.path.expanduser("~/.gmail-mcp/oauth-env.json")
with open(cfg_path) as f:
    cfg = json.load(f)

def refresh_token(acct):
    """Refresh access token using stored refresh token"""
    cred_path = os.path.expanduser(f"~/.gmail-{acct}/credentials.json")
    with open(cred_path) as f:
        saved = json.load(f)
    data = urllib.parse.urlencode({
        "client_id": cfg["client_id"],
        "client_secret": cfg["client_secret"],
        "refresh_token": saved["refresh_token"],
        "grant_type": "refresh_token",
    }).encode("utf-8")
    req = urllib.request.Request("https://oauth2.googleapis.com/token", data=data, method="POST")
    with urllib.request.urlopen(req, timeout=30) as resp:
        tokens = json.loads(resp.read().decode())
    saved.update(tokens)
    with open(cred_path, "w") as f:
        json.dump(saved, f, indent=2)
    return tokens["access_token"]

def gmail_get(endpoint, token):
    req = urllib.request.Request(
        f"https://gmail.googleapis.com/gmail/v1/{endpoint}",
        headers={"Authorization": f"Bearer {token}"}
    )
    with urllib.request.urlopen(req, timeout=30) as resp:
        return json.loads(resp.read().decode())

def gmail_post(endpoint, token, body):
    data = json.dumps(body).encode("utf-8")
    req = urllib.request.Request(
        f"https://gmail.googleapis.com/gmail/v1/{endpoint}",
        data=data,
        headers={"Authorization": f"Bearer {token}", "Content-Type": "application/json"},
        method="POST"
    )
    with urllib.request.urlopen(req, timeout=30) as resp:
        return json.loads(resp.read().decode())

# Usage:
token = refresh_token("ace")
profile = gmail_get("users/me/profile", token)
messages = gmail_get(f"users/me/messages?maxResults=25&q=is:unread", token)
```

### Creating a Draft in an Existing Thread

To reply within an existing email chain (not create a new thread):

```python
from email.mime.text import MIMEText
from email.mime.multipart import MIMEMultipart

# 1. Get the original thread's last message Message-ID
detail = gmail_get(f"users/me/threads/{thread_id}", token)
last_msg = detail["messages"][-1]
last_hdrs = {h["name"]: h["value"] for h in last_msg["payload"]["headers"]}
last_message_id = last_hdrs.get("Message-ID", "")

# 2. Build email with threading headers
msg = MIMEMultipart("alternative")
msg["To"] = "recipient@example.com"
msg["Subject"] = last_hdrs["Subject"]  # EXACT same subject
msg["In-Reply-To"] = last_message_id
msg["References"] = " ".join(all_message_ids_in_thread)  # all Message-IDs from thread
msg.attach(MIMEText(text_body, "plain"))
msg.attach(MIMEText(html_body, "html"))

# 3. Create draft WITH threadId
raw_b64 = base64.urlsafe_b64encode(msg.as_bytes()).decode("utf-8")
gmail_post("users/me/drafts", token, {"message": {"threadId": thread_id, "raw": raw_b64}})
```

### Extracting Structured Data from Email Subjects

Common CRE/business email patterns:

```python
import re

# Cap rate: "9.75% CAP", "7.00% CAP"
cap_match = re.search(r'(\d+\.?\d*)\s*%?\s*CAP', subject, re.IGNORECASE)

# Price: "$800K", "$3.2M", "$500,000"
price_match = re.search(r'\$(\d+(?:,\d+)*(?:\.\d+)?[KMB]?)', subject)

# Building SF: "62,225 SF", "18,265 SF"
sf_match = re.search(r'([\d,]+)\s*SF', subject, re.IGNORECASE)

# Lease years: "15 Years Remaining", "10 Yr NNN"
years_match = re.search(r'(\d+)\s*(?:Years?|Yr)(?:\s+Remaining)?', subject, re.IGNORECASE)

# Vehicles per day: "72,000+ VPD"
vpd_match = re.search(r'([\d,]+)\s*VPD', subject, re.IGNORECASE)

# State: "| FL |", "- TX"
state_match = re.search(r'\|\s*([A-Z]{2})\s*\|', subject)
```

### Legal Scan Before Committing Email Data

Always scan extracted email content before committing to git repos:

```python
import yaml

# Load deny list
with open("/path/to/workspace-hub/.legal-deny-list.yaml") as f:
    deny = yaml.safe_load(f)

# Scan all extracted text
all_text = " ".join(extracted_data).lower()
for item in deny.get("client_references", []):
    pattern = item["pattern"]
    case_sensitive = item.get("case_sensitive", False)
    searchable = all_text if not case_sensitive else " ".join(extracted_data)
    if (pattern.lower() if not case_sensitive else pattern) in searchable:
        # BLOCK — do not commit, flag for review
        raise ValueError(f"Protected client reference found: {pattern}")

# If no matches, safe to commit
```

## Pitfalls

1. himalaya JSON output can be large — use `--page-size` to limit
2. Contact CSV parsing: watch for malformed entries (angle brackets in email fields)
3. Don't auto-act on emails — digest is READ-ONLY, actions need user approval
4. Rate limit Gmail IMAP — space requests 1-2 seconds apart
5. Some emails have no From header — skip gracefully
6. Do NOT archive entire email bodies to repos — extract structured data only (#2017)
7. Old skills (now in _archived/) used the archive-everything model — use the queue model via gmail-extract-and-act instead (#2019)

Related Skills

session-start-dirty-state-triage-with-background-agents

5
from vamseeachanta/workspace-hub

Distinguish real implementation dirt from generated session-state churn when resuming a repo with active overnight/background agents.

multi-machine-ai-readiness-and-issue-triage

5
from vamseeachanta/workspace-hub

Assess a multi-machine, multi-repo AI-enabled workspace for readiness, provider allocation, and issue prioritization.

issue-portfolio-triage

5
from vamseeachanta/workspace-hub

Triage and rank GitHub issues across a workspace-hub style multi-repo ecosystem using machine-readiness, provider-utilization, and leverage scoring.

gmail-outreach

5
from vamseeachanta/workspace-hub

Outbound email actions — periodic relationship touchbase messages and batch unsubscribe from newsletters/marketing. Combines gmail-touchbase + gmail-unsubscribe into one skill.

gmail-operations

5
from vamseeachanta/workspace-hub

Class-level Gmail and email operations: multi-account setup, OAuth, triage, extraction, archiving, attachments, unsubscribe, and touchbase workflows.

gmail-multi-account

5
from vamseeachanta/workspace-hub

Multi-account Gmail management via himalaya CLI. Three accounts (aceengineer, achantav, skestates) with distinct triage rules, contact DBs, and tone profiles. Foundation skill for email automation.

gmail-headless-oauth

5
from vamseeachanta/workspace-hub

Manual OAuth2 token exchange for Gmail on headless servers. Bypass gmail-mcp-multiauth browser requirement. Generate auth URLs, exchange codes, manage multi-account credentials with auto-refresh.

gmail-extract-and-act

5
from vamseeachanta/workspace-hub

The email-as-queue workflow — extract structured data from emails, act on it, track thread state, and delete emails when topics complete. Email is transient; extracted data is persistent.

gmail-attachment-to-document

5
from vamseeachanta/workspace-hub

Download attachments from Gmail threads, parse their content (Excel, PDF), extract structured data, and save to target repos with proper legal scanning.

gmail-unsubscribe

5
from vamseeachanta/workspace-hub

Identify and batch-unsubscribe from newsletters and marketing emails across Gmail accounts. Scans List-Unsubscribe headers, generates candidates, executes with user approval.

gmail-touchbase

5
from vamseeachanta/workspace-hub

Periodic relationship maintenance via email — identify contacts due for outreach, draft personalized check-ins, queue for user approval. Supports per-account tone and cadence.

gmail-extract-archive

5
from vamseeachanta/workspace-hub

DEPRECATED — superseded by gmail-extract-and-act. Extract Gmail data into archive repo, parse attachments, legal scan, then delete. Uses archive-everything model.