carloscape/octorato/inbox-triage-classifier

Claude Code and agent skill by carloscape/octorato

Trust tier 3
  • community
Source check pending

Checking that the SKILL.md file is still in the repository.

Triage for inboxes (email, Slack, Teams, JIRA, PRs): classifies what needs action and returns a per-item verdict with urgency. Read-only, never marks as read and never replies. Use it on 'que tengo pendiente?', 'do I owe anyone a reply?'.

Install

npx vskill@latest install carloscape/octorato/inbox-triage-classifier

SKILL.md

Inbox-Triage Classifier

What

A general-purpose classification pipeline that scans a stream of items (emails, chat messages, tickets, mentions) and tells the operator which ones require their attention — with structured reasons and suggested actions, sorted by urgency.

This is the discovery layer for "what do I owe anyone today?" workflows. It is intentionally read-only and does not write back to the source (no marking read, no draft replies, no status changes).

Why

phi-aware-rag-ingestion solves the problem of building a searchable corpus from regulated content. That's the wrong tool for "tell me what to act on today" because:

  • Retrieval-via-query assumes you know what you're looking for. Triage assumes you don't.
  • RAG outputs document chunks. Triage outputs verdicts (needs_followup: bool + urgency + suggested action).
  • RAG runs as a background sync. Triage is a synchronous read on the current state of the inbox.
  • RAG accumulates state (vector store, cursor). Triage is stateless.

The plumbing (auth, PHI screen, dual-route LLM) is shared. The downstream goal is not.

When to Use

Trigger when the operator asks:

  • "What do I need to follow up on in my inbox / Slack / Teams?"
  • "Do I owe anyone a reply?"
  • "What's the most urgent thing in my queue right now?"
  • "Triage my JIRA / GitHub / Notion mentions"
  • "Read my mail and tell me what's actionable"

Do NOT use for:

  • Drafting replies (separate skill / step)
  • Building a searchable archive (use phi-aware-rag-ingestion)
  • Bulk export / backup (use a dedicated scraper)
  • Anything that writes back to the source

Pipeline

operator invokes: /<arm>-triage <source> [args]
│
▼
orchestrator
│
┌──────────────────────────────┼──────────────────────────────┐
▼ ▼ ▼
[1] FETCH [2] NORMALIZE [3] PHI SCREEN
delta-query for strip HTML, sigs, regex + (optional)
last N days, quoted replies; build classifier; mark
READ-ONLY stable hash each item phi or clean
│
▼
┌────────┴────────┐
▼ ▼
clean items phi-tainted items
│ │
▼ ▼
[4a] CLASSIFY [4b] CLASSIFY
cloud LLM local LLM
(Groq / OpenAI) (ollama)
│ │
└────────┬────────┘
▼
[5] STRUCTURED VERDICT
{ needs_followup: bool,
reason, suggested_action,
urgency: low|med|high,
phi: bool, llm: str }
│
▼
[6] SORT + REPORT
sort by urgency, then
by recency; print to
stdout (+ optional
PHI-redacted digest)

Classifier Contract (Mandatory Schema)

The LLM must return a single JSON object with this exact shape. No prose, no markdown, no code fence.

json
{
"needs_followup": true,
"reason": "<=120 chars — why this needs attention",
"suggested_action": "<=140 chars — what the operator should do",
"urgency": "low" | "med" | "high"
}

Urgency calibration (consistent across all sources):

LevelTrigger
highExplicit deadline today/tomorrow OR explicit blocker on a third party
medSoft deadline this week OR external party waiting OR named stakeholder asks directly
lowNo deadline, internal nudge, "when you get a chance", informational reply expected

Conservative bias: when uncertain, the classifier should default to needs_followup=false rather than invent urgency. False positives waste operator attention; false negatives are recoverable on the next run.

Sandwich System Prompt (Critical)

Smaller models (7B-13B local) lose attention on rules placed only at the top. Use the sandwich pattern:

# GOLDEN RULES (must obey)
- Output ONLY a single JSON object — no prose, no markdown, no code fence.
- Schema: {"needs_followup": boolean, "reason": "...", "suggested_action": "...", "urgency": "low"|"med"|"high"}
- needs_followup=true only if the item expects an action from the recipient.
- needs_followup=false for FYI broadcasts, auto-notifications, newsletters, already-handled invites.
- Be conservative: when uncertain, prefer needs_followup=false.
# TASK
Classify whether <RECIPIENT_NAME>, a <RECIPIENT_ROLE>, needs to follow up on this <ITEM_TYPE>.
# REMEMBER
- JSON only. No invention. Concrete actions, not vague advice.

The # REMEMBER block at the bottom is non-negotiable for local models. Without it, ollama llama3.1:8b drifts to markdown 1-in-5 runs.

Per-Item User Message Template

Subject: <subject> # or: Channel: <channel>, Ticket: <id>
From: <name> <<email/handle>>
Received: <ISO datetime>
Read: <yes|no> # if source supports unread state
Importance/Priority: <normal|high>
Flag: <followup|completed|none> # if source supports flags
---
<first 3500 chars of normalized body>

3500 chars is the sweet spot for Groq llama-3.3-70b and ollama llama3.1:8b. Going higher buys marginal classifier quality at large latency / cost.

Dual-Route LLM (Reuse phi-aware-rag-ingestion)

js
async function classifyItem(item, normalizedText, ctx) {
const phiRisk = phi.isPhiRisk(normalizedText, { strictness: 'medium' });
const messages = [
{ role: 'system', content: SANDWICH_SYSTEM },
{ role: 'user', content: buildUserMessage(item, normalizedText) },
];
if (phiRisk) {
if (!ctx.ollamaAvailable) {
// FAIL CLOSED — do NOT degrade to cloud
return { error: 'PHI detected but ollama unavailable', phi: true, llm: 'none' };
}
return await ollama.chat(messages, { maxTokens: 180, temperature: 0 });
}
return await groq.chat(messages, { maxTokens: 180, temperature: 0 });
}

PHI policy is non-negotiable: if phiRisk && !ollamaAvailable, the item is not classified — it is reported as blocked with exit code 3 (see phi-aware-rag-ingestion §Fail-Closed Exit Codes).

CLI Flags (Convention Across Implementations)

FlagDefaultEffect
--days N7Time window in days
--limit N50Max items to process
--unread-onlyfalseFilter to unread items (where source supports it)
--dry-runfalseList item subjects only, skip LLM classification
--savefalseWrite PHI-redacted markdown digest to users/<slug>/digests/<source>-<date>.md
--jsonfalseOutput machine-readable JSON for piping into other tools
--no-colorautoDisable ANSI colors (auto-detected from isTTY)

--dry-run is critical — it lets the operator verify auth + fetch + filter logic without spending LLM tokens on bad input.

Output Format (Human Mode)

🐙 <source> follow-up scanner
window=7d limit=50 ...
user: <email>
→ Fetching items...
N item(s) returned
→ Checking ollama availability...
ollama: up (M models)
→ Classifying items...
●●·!··●·
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
<source> follow-up report
<user> · last <N>d · X items scanned
PHI-tainted: A (ollama) · Clean: B (Groq) · Errors: C
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
K item(s) need follow-up:
● HIGH 2026-05-22 08:21 <subject>
from: <name> <<email>>
why: <reason>
do: <suggested_action>

Per-item progress glyphs during classification:

  • ● (yellow) — followup needed
  • · (gray) — no action needed
  • ! (red) — classifier error or PHI-blocked

Sort Order

  1. Urgency descending: high → med → low
  2. Within same urgency: most recent first

Operators triage top-down. Putting high first means they can stop reading as soon as they've handled what matters.

PHI-Redacted Digest

When --save is passed, write a digest at users/<sha256(email)[:12]>/digests/<source>-<date>.md:

  • Sender names/emails on PHI items go through phi.redact() before serialization
  • Body content is NEVER copied into the digest — only the LLM's reason + suggested_action
  • webLink (deep link back to source) is preserved so the operator can open the raw item in the native app
  • The digest is committable IF the arm's .gitignore has users/<slug>/digests/ excluded (recommended — digests still carry metadata)

Anti-patterns

Anti-patternWhy it fails
Marking items as read after classificationSide effect that breaks the operator's mental model. Read-only is the contract.
Auto-drafting repliesDrafting is a separate step; conflating them creates accidental sends
Copying item bodies into the digestBody may contain PHI even if redacted; digest exists to summarize, not archive
Falling back to cloud when ollama is downPHI policy violation. Halt with exit 3, do not degrade.
Sending JSON-mode prompts to local models without sandwich pattern7B-13B models drift to markdown ~20% of runs without # REMEMBER block
Classifying items with empty/preview-only bodyBias toward needs_followup=false; mark llm: 'skipped' and report separately
Caching classifier results across runsItem state changes (replied to elsewhere, no longer relevant). Run fresh every time.
Hardcoding the recipient's name in the system promptPer-arm config; resolve from auth token's upn / email claim

Source-Specific Plumbing

The classifier core is source-agnostic. Per-source adapters provide:

SourceFetch endpointItem shapeAdapter notes
Outlook MailGraph /me/mailFolders/inbox/messages{ subject, from, body, receivedDateTime, isRead, flag, webLink }Requires Mail.Read; see browser-bearer-graph-auth for token
Teams chatsGraph /me/chats?$expand=lastMessagePreview{ topic, members, lastMessage, lastUpdatedDateTime }Requires Chat.Read; only Outlook-driven token has it
Slack DMsconversations.list + conversations.history{ channel, user, text, ts }Requires mpim:read + im:history scopes
JIRA assignedREST /search?jql=assignee=currentUser() AND updated > -7d{ key, summary, status, updated, reporter }API token auth, no browser
GitHub PR reviewsGraphQL viewer.pullRequests(states: OPEN) filtered by reviewRequests{ title, author, repo, updatedAt, url }GH token with repo scope
Notion mentionsDatabase query filtered by mention property{ page, mentionedBy, lastEdited }Workspace integration

Each adapter outputs the common shape:

js
{
id: string,
subject: string,
from: { name, email|handle },
body: string, // raw, may be HTML
bodyType: 'html' | 'text',
receivedDateTime: ISO,
isRead: bool | null,
flag: string | null,
importance: 'normal' | 'high',
webLink: string, // deep link back to source
hasAttachments: bool,
}

This shape was designed against Outlook first. Slack/JIRA adapters normalize to it via small translation functions — do not invent a new shape per source.

Composability

This is the start of the file. Read the full SKILL.md on GitHub.

Security

  • Security scanPassed, 95 of 100Pattern checks on the skill's files.
  • Model reviewPassed

Quality evaluation

  • Task evaluationNot runNo evaluation yet. Run one in Skill Studio.

Source

Popularity

Repository stars
17
Repository forks
7
7-day trend score
14

Stars and forks are for the whole GitHub repository, not this skill alone. Not refreshed yet.

Works with

Any agent vskill installs to: 56 targets in all. These 39 have a page on this site.

  • Claude Code
  • Cursor
  • GitHub Copilot
  • Windsurf
  • Codex
  • Gemini CLI
  • Cline
  • Amp
  • Roo Code
  • Goose
  • Continue
  • Kilo Code
  • Junie
  • Trae
  • OpenHands
  • Qwen Code
  • AI Agent
  • Tabnine Chat
  • Sourcegraph Cody
  • Amazon Q
  • Firebase Genkit
  • Sweep
  • Devin
  • Phind
  • Mentat
  • Claude Desktop
  • Aider
  • GPT Pilot
  • Plandex
  • bolt.new
  • v0
  • Replit Agent
  • Anse
  • TypingMind
  • BetterChatGPT
  • LibreChat
  • Warp AI
  • Pieces
  • Fig AI
Embed badge
vskill VERIFIED badge
[![vskill VERIFIED](https://verified-skill.com/api/v1/skills/carloscape/octorato/inbox-triage-classifier/badge)](https://verified-skill.com/skills/carloscape/octorato/inbox-triage-classifier)

Something wrong with this skill or its page? Tell us.