Imported from willfell/sauce (
.claude/skills/cowork/skills/gather-gmail/SKILL.md). Install upstream withnpx skills add willfell/sauce --skill gather-gmail. Copyright stays with the author.
cowork:gather-gmail
Searches Gmail for human-relevant threads from the last lookback window, categorizes each into Action needed, Awaiting reply, or FYI, and emits a single [!example]+ Inbox digest callout for the daily note.
Inputs
engagement_id(string, required): id of the engagement this gather runs for. Resolves to per-engagement Gmail scope.window(string, optional, default"newer_than:1d"): a Gmail search-query fragment specifying the time window. Examples:"newer_than:1d","newer_than:12h","newer_than:7d". Caller passes the fullnewer_than:<value>clause.filters(list[string], optional, default["-category:promotions", "-category:social", "-category:updates", "-category:forums"]): list of additional Gmail search-query fragments appended verbatim. Caller controls exclusion / inclusion patterns.exclude_categories(list[string], optional): alternate input shape for the same purpose asfilters. When both are present,filterswins.max_threads(number, optional, default25): hard cap on threads to fully expand viaget_thread.
Outputs
markdown(string): a single> [!example]+ Inbox digestcallout, paste-ready.action_required(list[object]): structured rows{ from, subject, snippet, action_hint }for the morning Email callout's Action-Required table.fyi(list[object]): same shape, populated for the FYI table.
Steps
-
Resolve engagement. Read
<vault>/spice/cowork/context/vault-config.mdviamcp__obsidian__get_frontmatter. Look upengagements[]entry whereid == engagement_id. Captureengagement. If not found, return the Gmail-unavailable warning callout + Noticecowork:gather-gmail — engagement '<id>' not in vault-config.md. -
Compose query:
{{window}} <space-joined filters>. Ifengagement.gmail_labelis set, appendlabel:<engagement.gmail_label>to the filter list. (When onlyexclude_categoriesis provided, treat it as the filter list.) -
Call
mcp__claude_ai_Gmail__search_threadswithquery: <composed>,max_results: {{max_threads}}. -
For each returned thread id, call
mcp__claude_ai_Gmail__get_threadwiththread_id: <id>and capture: latestfrom,subject,snippet, whether the user is the latest sender (= awaiting reply from them) or recipient (= action may be needed). -
Classify each thread:
- Action needed: latest message is FROM someone else AND snippet contains imperative phrasing (
please,can you,need,?,due,deadline,confirm,reply). - Awaiting reply: latest message is FROM the user (the user already replied; waiting on the other party).
- FYI: everything else.
- Action needed: latest message is FROM someone else AND snippet contains imperative phrasing (
-
Resolve sender + build per-thread bullet. For each thread:
- Extract
<sender_name>and<sender_email>from the latest message'sfromfield. - Call
cowork:resolve-personwithinput: <sender_email>,prefer_type: "email",engagement_id: <engagement_id>. On miss, retry withinput: <sender_name>,prefer_type: "name",engagement_id: <engagement_id>. - On resolve hit: emit
**[[<person_basename>]]** - <Subject> - [first 80 chars of snippet, ellipsis-truncated]. - On resolve miss: emit
**<sender_name>** - <Subject> - [first 80 chars of snippet, ellipsis-truncated](current plaintext behavior preserved).
The bold-wrap (
**...**) is preserved in BOTH branches for callout-table parsability. - Extract
-
Compose the callout per Returns. Omit empty categories. If all three categories are empty, emit the empty-case callout.
-
Return the assembled markdown.
Returns
Non-empty case:
> [!example]+ Inbox digest - last [lookback]
>
> **Action needed**
> - **[Sender]** - [Subject] - [snippet excerpt]
>
> **Awaiting reply**
> - **[Sender]** - [Subject] - [snippet excerpt]
>
> **FYI**
> - **[Sender]** - [Subject] - [snippet excerpt]
> - +N more not expanded
Empty case:
> [!example]+ Inbox digest
> No notable email in the last [lookback].
Structured items (NEW v0.96.0)
In addition to markdown, return items[] — one entry per surfaced thread:
{
"items": [
{
"item_id": "<deriveItemId({ kind: 'email', engagement_id, day, stable_key: thread.id })>",
"kind": "email",
"callout_type": "example",
"title": "<thread.subject truncated to 80 chars>",
"features": {
"sender_or_organizer_inner_circle": <bool>,
"recency_bucket": "today|yesterday|within-week",
"thread_length_bucket": "single|2-5|6-plus",
"has_unread": <bool>,
"is_warning": false
}
}
]
}
item_id derives from the immutable Gmail thread ID, ensuring stability across same-day re-fires.
Errors
- Gmail MCP unavailable / not authenticated / search error: return:
> [!warning]+ Gmail unavailable > Gmail MCP not connected. Re-authenticate via the Anthropic connectors UI. - Missing
window: fall back to"newer_than:1d"silently (no warning). - Never throw; always return a paste-ready callout string.
MCP routing
This skill can pull email data from any of the following MCPs, in priority order:
- Gmail —
mcp__claude_ai_Gmail__search_threads(Anthropic-managed; available in personal vaults). - Outlook mail —
mcp__claude_ai_Outlook__*(when wired by the user).
At runtime: introspect on the available tool list. Pick the first MCP whose primary thread-search tool is available. If none are available, do NOT attempt the call. Instead emit:
gather-skipped: no email MCP available in this Claude Code runtime
Pass warning: gmail_unavailable (or warning: email_unavailable for non-Gmail providers — the orchestrator normalizes) up to the orchestrator.