
Email Triage
- 51 installs
- 61 repo stars
- Updated August 4, 2026
- joelhooks/joelclaw
Triage email inboxes through the joelclaw email CLI over Front: scan, categorize, archive noise, surface actionable items, and draft replies.
About
Scans Front inboxes, triages conversations by importance, and surfaces items needing attention using the email CLI and Front search API. A developer uses it to clean up an inbox or find what needs a reply.
- Front is canonical; use is:unreplied (no is:unread) and unix-timestamp date filters
- Documents recipient/entity filters, OR/AND logic, and no-negation limits
Email Triage by the numbers
- 51 all-time installs (skills.sh)
- Ranked #1,075 of 2,715 Automation & Workflows skills by installs in the Skillselion catalog
- Data as of Aug 5, 2026 (Skillselion catalog sync)
npx skills add https://github.com/joelhooks/joelclaw --skill email-triageAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 51 |
|---|---|
| repo stars | ★ 61 |
| Last updated | August 4, 2026 |
| Repository | joelhooks/joelclaw ↗ |
What it does
Triage email inboxes through the joelclaw email CLI over Front: scan, categorize, archive noise, surface actionable items, and draft replies.
Files
Email Triage
Scan Joel's email inboxes (Front), triage conversations by importance, archive noise, and surface items needing attention. All operations use the joelclaw email CLI.
Canonical Email Interface
- Front is canonical for all email triage in joelclaw.
- Never use `gog gmail` for triage —
bricks@aihero.dev, VIP threads, and all inboxes are in Front. - Use
gogfor Google Workspace operations (Calendar, Drive, Docs, etc.), not email triage.
Front Search API Reference
The search endpoint is GET /conversations/search/{url_encoded_query}. Results sorted by last activity date (not configurable).
Valid is: statuses
open, archived, assigned, unassigned, snoozed, trashed, unreplied, waiting, resolved
There is NO `is:unread`. Use is:unreplied for conversations where the last message was inbound.
Status conflicts (cannot combine):
archived↔open↔trashed↔snoozed(mutually exclusive)assigned↔unassigned(mutually exclusive)
Date filters — UNIX TIMESTAMPS ONLY
before:<unix_seconds>— messages/comments created before this timestampafter:<unix_seconds>— messages/comments created after this timestampduring:<unix_seconds>— messages/comments on same day as timestamp
Front does NOT accept `before:YYYY-MM-DD` — always convert to unix seconds. The CLI --before and --after flags accept YYYY-MM-DD and convert automatically.
Recipient filters
from:<handle>— sender (email, social handle)to:<handle>— recipient (includes to/cc/bcc)cc:<handle>,bcc:<handle>— specific fieldsrecipient:<handle>— any role (from, to, cc, bcc)- Multiple same-type filters use OR logic:
from:a@x.com from:b@x.com→ either - Different filter types use AND logic:
from:a@x.com to:b@x.com→ both
Entity filters (use IDs, not names)
inbox:<inbox_id>— e.g.inbox:inb_41w25tag:<tag_id>— e.g.tag:tag_13o8r1assignee:<teammate_id>— e.g.assignee:tea_hjx3participant:<teammate_id>,author:<teammate_id>,mention:<teammate_id>,commenter:<teammate_id>contact:<contact_id>,link:<link_id>custom_field:"<name>=<value>"
Free text
Bare words search subject + body. Phrases in quotes: "exact phrase".
No negation
Front search has no negation — no -from:, no NOT, no exclusion operators.
Max 15 filters per query.
CLI Commands
Scan inbox
joelclaw email inbox # default: is:open, 50 results
joelclaw email inbox -n 100 # more results
joelclaw email inbox -q "is:open is:unreplied" # awaiting reply
joelclaw email inbox --from notifications@github.com # by sender
joelclaw email inbox --before 2026-02-01 # auto-converts to unix ts
joelclaw email inbox --after 2026-01-15 # auto-converts to unix ts
joelclaw email inbox --page-token "TOKEN" # pagination (from previous response)Flags can be combined:
joelclaw email inbox --from matt@totaltypescript.com --after 2026-02-01 -n 20Archive single
joelclaw email archive --id cnv_xxxArchive multiple by ID (fast, parallel, batched)
joelclaw email archive-ids --ids cnv_abc,cnv_def,cnv_ghiRuns 10 concurrent requests. Best for triaging a page of results — grab the IDs of noise and blast them.
Archive bulk by query (dry-run by default)
joelclaw email archive-bulk -q "is:open from:notifications@github.com" # dry run — shows count + sample
joelclaw email archive-bulk -q "is:open from:notifications@github.com" --confirm # execute
joelclaw email archive-bulk -q "is:open from:notifications@github.com" --limit 100 --confirm # batch sizeRead a conversation
joelclaw email read --id cnv_xxx
joelclaw email read --id cnv_xxx --refresh # bypass local cachejoelclaw email read uses a local thread + attachment cache (default TTL: 60 minutes). Cache path: ~/.cache/joelclaw/email/. Use --refresh to bypass cache and force a fresh fetch from Front.
Read output shape + jq patterns
joelclaw email read returns:
result.conversation: {id, subject, status, from: {name, email}, date, tags}
result.messages[]: {id, from: {name, email}, date, is_inbound, body_preview (or body), attachments[]}
result.attachment_summary: {total, cached, metadata_only, cache_errors}
result.cache: {hit, source, cache_path, age_seconds, ttl_seconds}Message content is currently in body_preview, with a migration to body underway. Treat both as valid for backwards compatibility.
Correct extraction pattern (current field):
joelclaw email read --id cnv_xxx 2>&1 | jq '.result.messages[] | {from: .from.email, date: .date, body: .body_preview}'Backwards/forwards-compatible extraction pattern:
joelclaw email read --id cnv_xxx 2>&1 | jq '.result.messages[] | {from: .from.email, date: .date, body: (.body // .body_preview)}'List inboxes
joelclaw email inboxesTriage Workflow
1. Load inbox state
joelclaw email inbox -n 50Parse the JSON result. Each conversation has: id, subject, from (name + email), date, status, tags.
Useful follow-up queries:
joelclaw email inbox -q "is:open is:unreplied" -n 50 # awaiting reply
joelclaw email inbox --before 2026-01-01 -n 50 # old stuff to clean
joelclaw email inbox --from notifications@github.com # CI noise check2. Categorize using inference
Read each conversation and decide its category based on sender, subject, and context. Do NOT use hardcoded domain lists. Use judgment:
- Reply needed — Real people expecting a response. Colleagues, collaborators, friends, business contacts with personal messages. Look for reply threads (
Re:), questions, invitations to specific meetings, requests. - Read later — Interesting content worth saving. Newsletters Joel subscribes to intentionally (The Information, Astral Codex Ten, Lenny's Newsletter), industry news, technical deep-dives.
- Actionable — Requires action but not a reply. Bills, security alerts, expiring trials, tax documents, delivery updates for real orders, calendar invites needing RSVP.
- Archive — No value. Marketing spam, promotional offers, cold outreach, duplicate notifications, resolved alerts, automated reports nobody reads, vendor upsells.
3. Present triage summary
Organize findings for Joel. Lead with what matters:
## 🔴 Reply needed (N)
- **Name** — Subject (why it needs a reply)
## ⚡ Actionable (N)
- **Sender** — Subject (what action)
## 📖 Read later (N)
- **Source** — Subject
## 🗑️ Archive candidates (N)
- Count by type (e.g., "14 marketing, 8 CI failures, 5 duplicate notifications")4. Execute decisions
Fastest path for noise: scan a page, collect noise IDs, use archive-ids:
joelclaw email archive-ids --ids cnv_aaa,cnv_bbb,cnv_ccc,cnv_dddFor sender-based cleanup: use archive-bulk with from: filter:
joelclaw email archive-bulk -q "is:open from:notifications@github.com" --limit 100 --confirmRate limiting: Front allows ~50 req/s. If archiving large batches (>200), add pauses between batches or expect 429s. The CLI handles batching for archive-ids (10 concurrent), but archive-bulk iterates page-by-page and may hit limits on very large sets.
Read before deciding:
joelclaw email read --id cnv_xxxKey Context
- Joel has 8 Front inboxes across joel@egghead.io, joelhooks@gmail.com, joel@skillrecordings.com, joel@badass.dev, joel.hooks@vercel.com, LinkedIn, DMs, and Inngest
joelclaw email inboxeslists them all with IDs- Joel's teammate ID:
tea_hjx3 - The CLI handles Front API auth automatically via
secrets lease front_api_token - Draft-then-approve for replies — never send directly
- Front search results are eventually consistent — recently archived items may still appear in search for a few minutes
- Pagination: responses include
next_page_tokenwhen more results exist; pass via--page-token [aih]subject prefix indicates AI Hero project threads viabricks@aihero.devGoogle Group. Key participants: Alex Hillman (alex@indyhall.org), Matt Pocock (mattpocockvoice@gmail.com), Amy Hoy (team@stackingthebricks.com). Treat as VIP: always surface, never archive.
Signals for "Reply Needed"
These are heuristics, not rules. Use judgment for each:
Re:prefix + real person sender (not a bot/noreply)[aih]prefix — AI Hero collaboration viabricks@aihero.dev(Alex Hillman, Matt Pocock, Amy Hoy). VIP: always surface, never archive.- Direct questions in subject line
- Meeting invitations from known contacts
- Threads where Joel was the last sender and someone replied
- Slack notification summaries (may indicate missed conversations worth checking)
Signals for "Archive"
noreply@,no-reply@,notifications@senders with no actionable content- Marketing subject patterns: "LAST chance", "% off", "deal", "unlock", "limited time"
- Duplicate conversations (same subject from same sender — archive all but most recent)
- Resolved monitoring alerts (
RESOLVEDin subject) - CI failure notifications older than 24h (stale — either fixed or won't be)
- Cold outreach from unknown senders with sales language
Common Noise Senders (bulk-archive candidates)
These are historically high-volume, low-signal senders. Use archive-bulk with from: filter:
notifications@github.com — CI failures, dependabot, PR notifications
alerts@alerts.betterstack.com — resolved uptime alerts
no-reply@is.email.nextdoor.com — neighborhood spamAlways dry-run first to verify the query matches what you expect.
interface:
icon_small: "./assets/small-logo.svg"
icon_large: "./assets/large-logo.png"
<svg width="16" height="16" viewBox="0 0 512 512" xmlns="http://www.w3.org/2000/svg" role="img" aria-label="JoelClaw icon">
<defs>
<clipPath id="circle-clip">
<circle cx="256" cy="256" r="248" />
</clipPath>
</defs>
<circle cx="256" cy="256" r="248" fill="#0a0a0a" />
<g clip-path="url(#circle-clip)">
<path fill="#ff1493" d="M175.656 22.375l-48.47 82.094c-23.017 4.384-43.547 11.782-60.124 22.374-24.436 15.613-40.572 37.414-45.5 67.875-4.79 29.62 1.568 68.087 24.125 116.093 93.162 22.88 184.08-10.908 257.25-18.813 37.138-4.012 71.196-.898 96.344 22.97 22.33 21.19 36.21 56.808 41.908 113.436 29.246-35.682 44.538-69.065 49.343-99.594 5.543-35.207-2.526-66.97-20.31-95.593-8.52-13.708-19.368-26.618-32-38.626l14.217-33-41.218 10.625c-8.637-6.278-17.765-12.217-27.314-17.782l-7.03-59.782-38.157 37.406c-12.418-5.186-25.184-9.804-38.158-13.812l-8.375-71.28-57.625 56.5c-9.344-1.316-18.625-2.333-27.812-2.97l-31.094-78.125zM222 325.345c-39.146 7.525-82.183 14.312-127.156 11.686 47.403 113.454 207.056 224.082 260.125 87-101.18 33.84-95.303-49.595-132.97-98.686z" />
</g>
</svg>