← All Skills

snappy-email

v1.0.0
11 files, 100.7 KB ~7,075 words · 29 min read Updated 2026-09-09

snappy-email skill

33 of 39 checks pass
What it can do
draft to subject body account?draft
get message-id account?read
label message-id label-name account?write
mark-read message-id account?write
read message-id account?read
search query limit? account?read
send email subject bodysend
thread thread-id account?read
trash message-id account?delete
What does not pass yet
$ npx snappy-skills install snappy-email
zip ↓
File Tree
├── AGENTS.md ├── SKILL.md ├── adapter.ts ├── api.ts ├── automation.md ├── contract.test.ts ├── data/ │ └── drafts/ │ └── 2026-04-11-snappy-mcp-invoice-reply.md ├── inbox-triage.md ├── list-management.md ├── templates.md └── workflow.md
Documents
AGENTS.md

snappy-email -- Agent Loader#

You handle Snappy's email channel: weekly newsletters and inbox triage via Gmail/Xano. ActiveCampaign is NOT in use. This file is the operational contract -- everything load-bearing lives here.

API module#

typescriptimport {
  searchGmailMessages, getMessage, getThread,
  trashMessages, addLabel, markGmailRead, createEmailDraft,
  updateEmailDraft, getEmailDraft,
  onMessageRead,
  sendTransactional,
} from "../snappy-email/api.ts";

Reads go through native Gmail API (service-account domain-wide delegation for robert@snappy.ai, OAuth refresh token for robertjboulos@gmail.com). The Xano emails/* visual function stack is poison — do not depend on it for reads. sendTransactional remains for legacy one-off sends but is deprecated.

Or CLI:

bashnpx tsx ~/.claude/skills/snappy-email/api.ts search "is:unread" 10 work    # search work inbox
npx tsx ~/.claude/skills/snappy-email/api.ts search "from:bob" 5 personal  # search personal
npx tsx ~/.claude/skills/snappy-email/api.ts get <messageId>               # full message
npx tsx ~/.claude/skills/snappy-email/api.ts thread <threadId>             # full thread
npx tsx ~/.claude/skills/snappy-email/api.ts read <messageId>              # enriched (resolvePerson + suggested action)
npx tsx ~/.claude/skills/snappy-email/api.ts trash <id>[,id,...]           # batch trash
npx tsx ~/.claude/skills/snappy-email/api.ts label <ids> "Review"          # apply label by name
npx tsx ~/.claude/skills/snappy-email/api.ts mark-read <ids>               # remove UNREAD
npx tsx ~/.claude/skills/snappy-email/api.ts draft to@x.com "Subj" "Body"  # save draft (never sends)
npx tsx ~/.claude/skills/snappy-email/api.ts send u@x.com "Subj" "Body"    # legacy Xano, dry run

Operations#

Function Purpose
searchGmailMessages(query, limit?, account?) Native Gmail search. account = "work" (default) or "personal". Includes spam+trash.
getMessage(id, account?) Full message: headers + plaintext body + html body
getThread(threadId, account?) All messages in a thread, ordered by time
trashMessages(ids, account?) Batch trash. Returns {trashed, failed}
addLabel(ids, labelName, account?) Apply label by name (creates it if missing)
markGmailRead(ids, account?) Remove UNREAD label
createEmailDraft(to, subject, body, replyToThreadId?, account?) Save to Gmail Drafts. Never sends.
updateEmailDraft(draftId, to, subject, body, replyToThreadId?, account?) Update an existing Gmail draft in-place (PUT). Throws if draft no longer exists (already sent/deleted).
getEmailDraft(draftId, account?) Check if a draft still exists. Returns {draftId, messageId} or null if sent/deleted (404).
onMessageRead(messageId, account?) Enriched read: composes resolvePerson() + suggests action (reply_needed/already_handled/noise/fyi). Logs an email_read interaction.
sendTransactional(toEmail, subject, body, dryRun?) LEGACY Xano transactional send. Prefer createEmailDraft + manual send for anything non-trivial.

Auth notes#

  • Work inbox (robert@snappy.ai): service account xano-automation@snappy-424813.iam.gserviceaccount.com with domain-wide delegation. Issues gmail.modify scope (verified 2026-04-11). If a call returns 403 insufficient_scope, Robert must update the delegation at admin.google.com → Security → API controls → Domain-wide delegation and re-add the SA with https://www.googleapis.com/auth/gmail.modify.
  • Personal inbox (robertjboulos@gmail.com): OAuth refresh token in GMAIL_PERSONAL_REFRESH_TOKEN. Currently empty — personal reads will throw with instructions. Re-consent via npx tsx ~/.claude/skills/snappy-inbox-sweep/gmail-oauth.ts consent. Note: that consent flow currently requests gmail.readonly — edit SCOPES in gmail-oauth.ts to https://www.googleapis.com/auth/gmail.modify before running if you need write access.

Credentials loaded via snappy-settings/load.ts from .env.cache.


Newsletter Send -- API endpoint and payload#

Newsletters go through Xano transactional API (Gmail backend). ActiveCampaign is NOT in use.

Xano transactional send#

POST api:PB9UH7b9/emails/send with { to_email, subject, body, dry_run }. Always dry_run: true first.

bashcurl -s -X POST "$XANO/api:PB9UH7b9/emails/send" \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer $XANO_METADATA_TOKEN" \
  -d '{
    "to_email": "robert@snappy.ai",
    "subject": "Test: Subject line",
    "body": "<p>Body here</p>",
    "dry_run": true
  }'

For bulk sends, loop over recipients with the same endpoint. For higher-volume broadcast, use Loops.so (LOOPS_API_KEY).


Subject line convention#

rule detail
Under 10 words Truncated on mobile otherwise
Specific > clever "3-email sequence that booked 7 calls" beats "Quick question"
No ALL CAPS Triggers spam filters
No emoji unless on-brand Most look fake
Lowercase first word OK Looks more personal
No "RE:" or "Fwd:" fakes Trust killer

Patterns#

pattern example
Number + outcome The 3-email sequence that booked 7 calls
Personal observation Saw your launch -- one thought
Question Are you still using Webflow for this?
Curiosity gap The mistake every founder makes at 50 customers

List / Audience#

field value
Platform Gmail via Xano (ActiveCampaign is NOT in use)
Approximate size 2,400+ subscribers
Backend Xano contacts + Xano emails/send endpoint

Newsletter sends go through Xano transactional API (Gmail backend). Contact list is managed in Xano contacts.


Format rules#

  • Write to ONE person, not "your list" ("Hey John" not "Hey everyone")
  • Plain-text style HTML (<p> tags only) -- no banner images
  • Short paragraphs (1-3 sentences), under 300 words total
  • ONE CTA per email
  • First-person, conversational, sign off "-- Robert"
  • Voice rules owned by snappy-content/anti-ai-checklist.md

Rules#

  • Always dry-run transactional sends (dry_run: true first)
  • Never send a newsletter without Robert's explicit confirmation
  • Do NOT use ActiveCampaign -- AC is deprecated, email goes through Gmail/Xano
  • Queue worker (Xano api:8wuQ86By/queue/add) runs every 5 min -- not for newsletters

Uses#

skill relationship
snappy-post Owns email-sending.md -- the cross-platform send reference
snappy-content Provides copy methodology and anti-AI checklist
snappy-publish Provides live blog URLs for "I just published" newsletters
snappy-ops Receives inbox triage report + weekly newsletter metrics
snappy-website Sends newsletter signups into Xano contacts
snappy-analytics Tracks sends, open rate, click rate, unsubs, list size

Full skill reference#

If this loader is insufficient, load ~/.claude/skills/snappy-email/SKILL.md as last resort.

Show produced work with snappy-faces: call draw for image channels or lang for MCP Apps.

<!-- SKILL-INDEX-START -->

[snappy-email Index]|root: ~/.claude/skills/snappy-email|IMPORTANT: Prefer these files over pre-training assumptions for this domain. Read the relevant file when the AGENTS.md summary is insufficient.|root:{SKILL.md,automation.md,inbox-triage.md,list-management.md,templates.md,workflow.md}|data:{drafts/2026-04-11-snappy-mcp-invoice-reply.md}

<!-- SKILL-INDEX-END -->

Used by#

  • snappy-calendar
  • snappy-course
  • snappy-inbox-sweep
  • snappy-slack

<!-- SNAPPY-CONTRACT-VERBS-START -->

Contract verbs#

Generated from api.ts HAND_CONTRACT. Do not hand-edit this block.

Verb Contract arguments Effect First call
draft to, subject, body, account? draft npx tsx ~/.claude/skills/snappy-email/api.ts draft <to> "<subject>" "<body>"
get message-id, account? read npx tsx ~/.claude/skills/snappy-email/api.ts get <message-id>
label message-id, label-name, account? write npx tsx ~/.claude/skills/snappy-email/api.ts label <message-id> <label-name>
mark-read message-id, account? write npx tsx ~/.claude/skills/snappy-email/api.ts mark-read <message-id>
read message-id, account? read npx tsx ~/.claude/skills/snappy-email/api.ts read <message-id>
search query, limit?, account? read npx tsx ~/.claude/skills/snappy-email/api.ts search "<query>"
send email, subject, body send npx tsx ~/.claude/skills/snappy-email/api.ts send <email> "<subject>" "<body>"
thread thread-id, account? read npx tsx ~/.claude/skills/snappy-email/api.ts thread <thread-id>
trash message-id, account? delete npx tsx ~/.claude/skills/snappy-email/api.ts trash <message-id>

Show the result#

When an answer carries face_hint, show it with one snappy_present(<answer>) call.

See /snappy-faces for face selection. Human-facing images must crop to the

element, render at 2x on Retina, and fill the destination channel instead of

placing a small card in a full-page screenshot.

<!-- SNAPPY-CONTRACT-VERBS-END -->

---
name: snappy-email
role: Email operations -- newsletter sends, inbox triage. Gmail/Google backend (ActiveCampaign is NOT in use).
loaded-by: PreToolUse hook (auto-injected when "snappy-email" is mentioned)
---

# snappy-email -- Agent Loader

You handle Snappy's email channel: weekly newsletters and inbox triage via Gmail/Xano. **ActiveCampaign is NOT in use.** This file is the operational contract -- everything load-bearing lives here.

## API module

```typescript
import {
  searchGmailMessages, getMessage, getThread,
  trashMessages, addLabel, markGmailRead, createEmailDraft,
  updateEmailDraft, getEmailDraft,
  onMessageRead,
  sendTransactional,
} from "../snappy-email/api.ts";
```

**Reads go through native Gmail API** (service-account domain-wide delegation for `robert@snappy.ai`, OAuth refresh token for `robertjboulos@gmail.com`). The Xano `emails/*` visual function stack is poison — do not depend on it for reads. `sendTransactional` remains for legacy one-off sends but is deprecated.

Or CLI:
```bash
npx tsx ~/.claude/skills/snappy-email/api.ts search "is:unread" 10 work    # search work inbox
npx tsx ~/.claude/skills/snappy-email/api.ts search "from:bob" 5 personal  # search personal
npx tsx ~/.claude/skills/snappy-email/api.ts get <messageId>               # full message
npx tsx ~/.claude/skills/snappy-email/api.ts thread <threadId>             # full thread
npx tsx ~/.claude/skills/snappy-email/api.ts read <messageId>              # enriched (resolvePerson + suggested action)
npx tsx ~/.claude/skills/snappy-email/api.ts trash <id>[,id,...]           # batch trash
npx tsx ~/.claude/skills/snappy-email/api.ts label <ids> "Review"          # apply label by name
npx tsx ~/.claude/skills/snappy-email/api.ts mark-read <ids>               # remove UNREAD
npx tsx ~/.claude/skills/snappy-email/api.ts draft to@x.com "Subj" "Body"  # save draft (never sends)
npx tsx ~/.claude/skills/snappy-email/api.ts send u@x.com "Subj" "Body"    # legacy Xano, dry run
```

## Operations

| Function | Purpose |
|----------|---------|
| `searchGmailMessages(query, limit?, account?)` | Native Gmail search. `account = "work"` (default) or `"personal"`. Includes spam+trash. |
| `getMessage(id, account?)` | Full message: headers + plaintext body + html body |
| `getThread(threadId, account?)` | All messages in a thread, ordered by time |
| `trashMessages(ids, account?)` | Batch trash. Returns `{trashed, failed}` |
| `addLabel(ids, labelName, account?)` | Apply label by name (creates it if missing) |
| `markGmailRead(ids, account?)` | Remove UNREAD label |
| `createEmailDraft(to, subject, body, replyToThreadId?, account?)` | Save to Gmail Drafts. Never sends. |
| `updateEmailDraft(draftId, to, subject, body, replyToThreadId?, account?)` | Update an existing Gmail draft in-place (PUT). Throws if draft no longer exists (already sent/deleted). |
| `getEmailDraft(draftId, account?)` | Check if a draft still exists. Returns `{draftId, messageId}` or `null` if sent/deleted (404). |
| `onMessageRead(messageId, account?)` | Enriched read: composes `resolvePerson()` + suggests action (reply_needed/already_handled/noise/fyi). Logs an `email_read` interaction. |
| `sendTransactional(toEmail, subject, body, dryRun?)` | **LEGACY** Xano transactional send. Prefer `createEmailDraft` + manual send for anything non-trivial. |

## Auth notes

- **Work inbox** (`robert@snappy.ai`): service account `xano-automation@snappy-424813.iam.gserviceaccount.com` with domain-wide delegation. Issues `gmail.modify` scope (verified 2026-04-11). If a call returns 403 insufficient_scope, Robert must update the delegation at admin.google.com → Security → API controls → Domain-wide delegation and re-add the SA with `https://www.googleapis.com/auth/gmail.modify`.
- **Personal inbox** (`robertjboulos@gmail.com`): OAuth refresh token in `GMAIL_PERSONAL_REFRESH_TOKEN`. Currently **empty** — personal reads will throw with instructions. Re-consent via `npx tsx ~/.claude/skills/snappy-inbox-sweep/gmail-oauth.ts consent`. Note: that consent flow currently requests `gmail.readonly` — edit `SCOPES` in gmail-oauth.ts to `https://www.googleapis.com/auth/gmail.modify` before running if you need write access.

Credentials loaded via `snappy-settings/load.ts` from `.env.cache`.

---

## Newsletter Send -- API endpoint and payload

Newsletters go through **Xano transactional API** (Gmail backend). ActiveCampaign is NOT in use.

### Xano transactional send

`POST api:PB9UH7b9/emails/send` with `{ to_email, subject, body, dry_run }`. Always `dry_run: true` first.

```bash
curl -s -X POST "$XANO/api:PB9UH7b9/emails/send" \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer $XANO_METADATA_TOKEN" \
  -d '{
    "to_email": "robert@snappy.ai",
    "subject": "Test: Subject line",
    "body": "<p>Body here</p>",
    "dry_run": true
  }'
```

For bulk sends, loop over recipients with the same endpoint. For higher-volume broadcast, use Loops.so (`LOOPS_API_KEY`).

---

## Subject line convention

|rule|detail|
|----|------|
|Under 10 words|Truncated on mobile otherwise|
|Specific > clever|"3-email sequence that booked 7 calls" beats "Quick question"|
|No ALL CAPS|Triggers spam filters|
|No emoji unless on-brand|Most look fake|
|Lowercase first word OK|Looks more personal|
|No "RE:" or "Fwd:" fakes|Trust killer|

### Patterns

|pattern|example|
|-------|-------|
|Number + outcome|`The 3-email sequence that booked 7 calls`|
|Personal observation|`Saw your launch -- one thought`|
|Question|`Are you still using Webflow for this?`|
|Curiosity gap|`The mistake every founder makes at 50 customers`|

---

## List / Audience

|field|value|
|-----|-----|
|Platform|Gmail via Xano (ActiveCampaign is NOT in use)|
|Approximate size|2,400+ subscribers|
|Backend|Xano contacts + Xano `emails/send` endpoint|

Newsletter sends go through Xano transactional API (Gmail backend). Contact list is managed in Xano contacts.

---

## Format rules

- Write to ONE person, not "your list" ("Hey John" not "Hey everyone")
- Plain-text style HTML (`<p>` tags only) -- no banner images
- Short paragraphs (1-3 sentences), under 300 words total
- ONE CTA per email
- First-person, conversational, sign off "-- Robert"
- Voice rules owned by `snappy-content/anti-ai-checklist.md`

---

## Rules

- **Always dry-run transactional sends** (`dry_run: true` first)
- **Never send a newsletter without Robert's explicit confirmation**
- **Do NOT use ActiveCampaign** -- AC is deprecated, email goes through Gmail/Xano
- Queue worker (Xano `api:8wuQ86By/queue/add`) runs every 5 min -- not for newsletters

---

## Uses

| skill | relationship |
|-------|-------------|
| `snappy-post` | Owns email-sending.md -- the cross-platform send reference |
| `snappy-content` | Provides copy methodology and anti-AI checklist |
| `snappy-publish` | Provides live blog URLs for "I just published" newsletters |
| `snappy-ops` | Receives inbox triage report + weekly newsletter metrics |
| `snappy-website` | Sends newsletter signups into Xano contacts |
| `snappy-analytics` | Tracks sends, open rate, click rate, unsubs, list size |

---

## Full skill reference

If this loader is insufficient, load `~/.claude/skills/snappy-email/SKILL.md` as last resort.


Show produced work with `snappy-faces`: call `draw` for image channels or `lang` for MCP Apps.

<!-- SKILL-INDEX-START -->
[snappy-email Index]|root: ~/.claude/skills/snappy-email|IMPORTANT: Prefer these files over pre-training assumptions for this domain. Read the relevant file when the AGENTS.md summary is insufficient.|root:{SKILL.md,automation.md,inbox-triage.md,list-management.md,templates.md,workflow.md}|data:{drafts/2026-04-11-snappy-mcp-invoice-reply.md}
<!-- SKILL-INDEX-END -->

## Used by

- `snappy-calendar`
- `snappy-course`
- `snappy-inbox-sweep`
- `snappy-slack`

<!-- SNAPPY-CONTRACT-VERBS-START -->
## Contract verbs

Generated from `api.ts` `HAND_CONTRACT`. Do not hand-edit this block.

| Verb | Contract arguments | Effect | First call |
|---|---|---|---|
| `draft` | `to`, `subject`, `body`, `account?` | `draft` | `npx tsx ~/.claude/skills/snappy-email/api.ts draft <to> "<subject>" "<body>"` |
| `get` | `message-id`, `account?` | `read` | `npx tsx ~/.claude/skills/snappy-email/api.ts get <message-id>` |
| `label` | `message-id`, `label-name`, `account?` | `write` | `npx tsx ~/.claude/skills/snappy-email/api.ts label <message-id> <label-name>` |
| `mark-read` | `message-id`, `account?` | `write` | `npx tsx ~/.claude/skills/snappy-email/api.ts mark-read <message-id>` |
| `read` | `message-id`, `account?` | `read` | `npx tsx ~/.claude/skills/snappy-email/api.ts read <message-id>` |
| `search` | `query`, `limit?`, `account?` | `read` | `npx tsx ~/.claude/skills/snappy-email/api.ts search "<query>"` |
| `send` | `email`, `subject`, `body` | `send` | `npx tsx ~/.claude/skills/snappy-email/api.ts send <email> "<subject>" "<body>"` |
| `thread` | `thread-id`, `account?` | `read` | `npx tsx ~/.claude/skills/snappy-email/api.ts thread <thread-id>` |
| `trash` | `message-id`, `account?` | `delete` | `npx tsx ~/.claude/skills/snappy-email/api.ts trash <message-id>` |

## Show the result

When an answer carries `face_hint`, show it with one `snappy_present(<answer>)` call.
See `/snappy-faces` for face selection. Human-facing images must crop to the
element, render at 2x on Retina, and fill the destination channel instead of
placing a small card in a full-page screenshot.
<!-- SNAPPY-CONTRACT-VERBS-END -->

Keyboard Shortcuts

Search in document⌘K
Focus search/
Previous file tab
Next file tab
Close overlayEsc
Show shortcuts?