Triage de bandejas (correo, Slack, Teams, JIRA, PRs): clasifica que requiere accion y devuelve veredicto por item con urgencia. Solo lectura, nunca marca leido ni responde. Usalo ante 'que tengo pendiente?', 'do I owe anyone a reply?'.
Scanned 9/2/2026
Install to Claude Code
npx -y skills add CarlosCaPe/octorato --skill inbox-triage-classifier --agent claude-codeInstalls into .claude/skills of the current project.
Are you the author of Inbox Triage Classifier?
Add the live security badge to your README — it updates automatically with every re-scan.
[](https://www.skillsdirectory.com/skills/carloscape-inbox-triage-classifier)More formats (shields.io, HTML) on the badges page.
---
name: inbox-triage-classifier
description: "Triage de bandejas (correo, Slack, Teams, JIRA, PRs): clasifica que requiere accion y devuelve veredicto por item con urgencia. Solo lectura, nunca marca leido ni responde. Usalo ante 'que tengo pendiente?', 'do I owe anyone a reply?'."
metadata:
short-description: "PHI-aware triage classifier for inbox-like streams"
---
# 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)**:
| Level | Trigger |
|---|---|
| `high` | Explicit deadline today/tomorrow OR explicit blocker on a third party |
| `med` | Soft deadline this week OR external party waiting OR named stakeholder asks directly |
| `low` | No 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)
| Flag | Default | Effect |
|---|---|---|
| `--days N` | 7 | Time window in days |
| `--limit N` | 50 | Max items to process |
| `--unread-only` | false | Filter to unread items (where source supports it) |
| `--dry-run` | false | List item subjects only, skip LLM classification |
| `--save` | false | Write PHI-redacted markdown digest to `users/<slug>/digests/<source>-<date>.md` |
| `--json` | false | Output machine-readable JSON for piping into other tools |
| `--no-color` | auto | Disable 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-pattern | Why it fails |
|---|---|
| Marking items as read after classification | Side effect that breaks the operator's mental model. Read-only is the contract. |
| Auto-drafting replies | Drafting is a separate step; conflating them creates accidental sends |
| Copying item bodies into the digest | Body may contain PHI even if redacted; digest exists to summarize, not archive |
| Falling back to cloud when ollama is down | PHI policy violation. Halt with exit 3, do not degrade. |
| Sending JSON-mode prompts to local models without sandwich pattern | 7B-13B models drift to markdown ~20% of runs without `# REMEMBER` block |
| Classifying items with empty/preview-only body | Bias toward `needs_followup=false`; mark `llm: 'skipped'` and report separately |
| Caching classifier results across runs | Item state changes (replied to elsewhere, no longer relevant). Run fresh every time. |
| Hardcoding the recipient's name in the system prompt | Per-arm config; resolve from auth token's `upn` / `email` claim |
## Source-Specific Plumbing
The classifier core is source-agnostic. Per-source adapters provide:
| Source | Fetch endpoint | Item shape | Adapter notes |
|---|---|---|---|
| Outlook Mail | Graph `/me/mailFolders/inbox/messages` | `{ subject, from, body, receivedDateTime, isRead, flag, webLink }` | Requires `Mail.Read`; see `browser-bearer-graph-auth` for token |
| Teams chats | Graph `/me/chats?$expand=lastMessagePreview` | `{ topic, members, lastMessage, lastUpdatedDateTime }` | Requires `Chat.Read`; only Outlook-driven token has it |
| Slack DMs | `conversations.list` + `conversations.history` | `{ channel, user, text, ts }` | Requires `mpim:read` + `im:history` scopes |
| JIRA assigned | REST `/search?jql=assignee=currentUser() AND updated > -7d` | `{ key, summary, status, updated, reporter }` | API token auth, no browser |
| GitHub PR reviews | GraphQL `viewer.pullRequests(states: OPEN)` filtered by `reviewRequests` | `{ title, author, repo, updatedAt, url }` | GH token with `repo` scope |
| Notion mentions | Database 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
- `phi-aware-rag-ingestion` — provides `phi-redact.js`, `text-normalize.js`, `groq-client.js`, `ollama-client.js`, `user-paths.js`; the triage script depends on all of them
- `browser-bearer-graph-auth` — provides the Graph bearer for Outlook/Teams sources
- `sops-age-git-encryption` — encrypts the digest dir if the arm policy treats digests as PHI-derivative
- `incident-capture` — when the classifier mis-categorizes a critical item, file an incident with the raw item ID + LLM verdict + ground truth
## Lessons Learned
- **Read-only by contract**: the first instinct was to mark classified items as "seen" — resist. The operator's existing read/unread state is theirs, not the classifier's.
- **Conservative classification beats aggressive**: false positives in `needs_followup=true` train the operator to ignore the report. False negatives are recoverable next run. Bias conservative.
- **PHI in subject/sender is rare; in body is common**: triage on subject+sender alone gives ~80% accuracy with 0 PHI risk. Body classification is the marginal 20% that pushes accuracy to ~95% — at the cost of needing ollama up. Worth the trade-off when ollama is local.
- **Local model JSON discipline requires sandwich pattern**: llama3.1:8b drifts to markdown without `# REMEMBER`. Top-only rules are not enough.
- **One adapter shape, many sources**: trying to design a per-source classifier shape (different urgency calibration per platform) made the LLM prompts inconsistent. Normalize at the adapter layer; classifier prompt stays identical.
- **Outlook Mail follow-up classification works zero-shot at Groq llama-3.3-70b quality**: no fine-tuning needed for English/Spanish corporate mail. Calibration drift was observed on extremely terse messages ("ok thanks") — those should be filtered out at normalize step (empty after sig/quote strip).
Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.
No comments yet. Be the first to comment!