MCPInstall

conversation-inbox-triage

Audit and triage conversation inboxes — list open and unassigned threads, find stale conversations, assign threads to team members, archive resolved threads in bulk, and produce a status summary by inbox and channel.

Installs
186
Last 24h
—
Trending rank
—
Security
Not audited

Install

Works with every agent the skills CLI supports.

npx skills add https://github.com/hubspot/agent-cli-skills --skill conversation-inbox-triage
  1. 01

    Install

    Run the command in your project. The CLI asks which agents to install for.

  2. 02

    Describe the task

    Ask your agent for something this skill covers. It loads conversation-inbox-triage automatically when relevant.

  3. 03

    Review the result

    The agent follows the SKILL.md steps, scripts and references bundled with the skill.

SKILL.md

The instructions your agent reads when the skill is triggered.

Requires the conversations command group from hub-cli PR #251. hubspot conversations --help is authoritative. Archive uses the dry-run/digest/confirm guard pattern — follow it exactly.

Paging: never triage from a --format jsonl redirect

Every conversations list command defaults to --limit 100 and returns a cursor for the next page. --format jsonl prints only the data rows — the cursor is surfaced only by --format json, at .meta.next. So threads list --format jsonl > file silently stops at 100 threads: on a busy inbox the operator sees a clean run while every thread past the first page is never assigned, never archived, and missing from the counts. Raising --limit does not fix it — the endpoint caps the page size.

Use this helper for every bulk or counting operation:

# conv_page <output-file> <conversations list args...>
# Pages a conversations list endpoint to exhaustion, writing one JSON object
# per line to <output-file>.
conv_page() {
  out=$1; shift
  after=""
  : > "$out"
  while :; do
    if [ -n "$after" ]; then
      page=$(hubspot "$@" --format json --after "$after") || return 1
    else
      page=$(hubspot "$@" --format json) || return 1
    fi
    printf '%s' "$page" | jq -c '.data[]' >> "$out"
    after=$(printf '%s' "$page" | jq -r '.meta.next // empty')
    [ -n "$after" ] || break
  done
}

The bare --format jsonl calls in steps 2–4 are fine for eyeballing the first page interactively. Steps 6–8 mutate or count, so they page.

1. Discover inboxes

Inbox IDs are portal-specific. Always enumerate at the start of a session — never hardcode.

conv_page /tmp/inboxes.jsonl conversations inboxes list
# {"id":"123","name":"Support Inbox","createdAt":"...","updatedAt":"..."}
# {"id":"124","name":"Sales Chat","createdAt":"...","updatedAt":"..."}

To see channels inside an inbox (email, live chat, form, etc.):

conv_page /tmp/channels.jsonl conversations channels list --inbox-id 123
# {"id":"456","name":"Support Email","type":"EMAIL","inboxId":"123"}

2. List open threads

threads list requires --inbox-id. Filter client-side on .status for a focused view:

# all threads in an inbox
hubspot conversations threads list --inbox-id 123 --format jsonl

# only OPEN threads
hubspot conversations threads list --inbox-id 123 --format jsonl \
| jq -c 'select(.status == "OPEN")'

Output shape per row:

{"id":"111","status":"OPEN","createdAt":"2026-07-28T10:02:00Z","latestMessageTimestamp":"2026-07-30T14:22:00Z","assignedTo":"A-78901","inboxId":"123","originalChannelId":"456","originalChannelAccountId":"148662","associatedContactId":"73235","spam":false,"archived":false}

A thread has no channelType — it names its originating channel by id, in originalChannelId, which you join against channels list (step 8). subject is also not guaranteed; read it through a // "(no subject)" fallback.

3. Find unassigned threads

assignedTo is null or absent for unassigned threads:

hubspot conversations threads list --inbox-id 123 --format jsonl \
| jq -c 'select(.status == "OPEN") | select(.assignedTo == null or .assignedTo == "")'

4. Find stale threads (no reply within N hours)

latestMessageTimestamp is ISO 8601 and compares lexicographically.

# macOS: threads with no activity in 24 hours
CUTOFF=$(date -u -v-24H +%Y-%m-%dT%H:%M:%SZ)
# Linux: CUTOFF=$(date -u -d '24 hours ago' +%Y-%m-%dT%H:%M:%SZ)

hubspot conversations threads list --inbox-id 123 --format jsonl \
| jq -c --arg cutoff "$CUTOFF" \
    'select(.status == "OPEN") | select(.latestMessageTimestamp < $cutoff)'

For 48-hour and 72-hour variants change -v-24H to -v-48H / -v-72H.

5. Look up an actor ID

assign takes an actor ID. Get a team member's actor ID by their agent ID or email. HubSpot actor IDs use the A- prefix:

hubspot conversations actors get --id A-123456 --format jsonl

To map owner IDs to actor IDs, cross-reference hubspot owners list --format jsonl (returns .email and .id) and use the actor ID format A-<ownerId> as a starting point — verify with actors get that the ID resolves.

owners list needs a service key. Under an OAuth login it returns 403 This endpoint does not support user-level OAuth tokens, so set HUBSPOT_ACCESS_TOKEN to a service key before the owner-to-actor lookup. The conversations commands themselves work under either auth mode.

6. Assign a thread

hubspot conversations threads assign --id 111 --actor-id A-123456
# {"id":"111","ok":true}

Bulk assign (all unassigned open threads to one actor). Two things matter here: read the id list from a file, not a pipe — a pipeline-fed while runs in a subshell, so counters and failure lists are discarded when it ends — and report a non-zero status at the end, so a partial run is never mistaken for a complete one.

ACTOR=A-123456
conv_page /tmp/threads.jsonl conversations threads list --inbox-id 123

jq -r 'select(.status == "OPEN")
       | select(.assignedTo == null or .assignedTo == "")
       | .id' /tmp/threads.jsonl > /tmp/assign_ids.txt

assign_threads() {
  actor=$1; ids=$2
  assigned=0
  : > /tmp/assign_failed.txt

  while read -r tid; do
    if out=$(hubspot conversations threads assign --id "$tid" --actor-id "$actor" 2>&1); then
      assigned=$((assigned + 1))
    else
      printf '%s\t%s\n' "$tid" "$out" >> /tmp/assign_failed.txt
    fi
  done < "$ids"

  failed=$(wc -l < /tmp/assign_failed.txt)
  echo "Assigned $assigned of $(wc -l < "$ids"); $failed failed"
  [ "$failed" -eq 0 ] || { echo "Failures:"; cat /tmp/assign_failed.txt; return 1; }
}

assign_threads "$ACTOR" /tmp/assign_ids.txt

7. Archive resolved threads

Archive is guarded: run --dry-run first to get a digest, then re-run with --digest and --confirm (value = thread ID).

# Single thread — two steps
hubspot conversations threads archive --id 111 --dry-run
# {"id":"111","digest":"blast-29cfdd48b583","ok":true,"executed":false,"dry_run":true}

hubspot conversations threads archive --id 111 --digest blast-29cfdd48b583 --confirm 111
# {"id":"111","ok":true}

Bulk archive — preview, then execute

Bulk archive runs as two separate phases with a human decision between them. Do not collapse them into one loop that dry-runs and archives each thread in turn: that starts destroying threads before anyone has seen the full candidate set, and there is no point at which the operation can be declined as a whole.

Phase 1 — preview. Read-only; nothing is mutated.

INBOX=123
conv_page /tmp/threads.jsonl conversations threads list --inbox-id "$INBOX"

jq -c 'select(.status == "CLOSED")
       | {id, last_activity: .latestMessageTimestamp, assigned_to: .assignedTo}' \
  /tmp/threads.jsonl > /tmp/archive_candidates.jsonl

echo "Candidates for archive: $(wc -l < /tmp/archive_candidates.jsonl)"
jq -r '"\(.id)  \(.last_activity // "?")  \(.assigned_to // "unassigned")"' \
  /tmp/archive_candidates.jsonl

Stop here and get the candidate list approved. Re-run phase 1 as often as you like; it changes nothing.

Persist candidate ids, not digests. A digest is only valid for 5 minutes (DIGEST_TTL_SECONDS in src/hub/guard.rs), so digests minted during preview would come back digest_expired after any realistic review pause. Phase 2 therefore mints each digest immediately before it uses it.

Phase 2 — execute. Only after the list above is approved.

jq -r '.id' /tmp/archive_candidates.jsonl > /tmp/archive_ids.txt

archive_threads() {
  ids=$1
  archived=0
  : > /tmp/archive_failed.txt

  while read -r tid; do
    preview=$(hubspot conversations threads archive --id "$tid" --dry-run 2>&1)
    digest=$(printf '%s' "$preview" | jq -r '.digest // empty' 2>/dev/null)
    if [ -z "$digest" ]; then
      printf '%s\tdry-run\t%s\n' "$tid" "$preview" >> /tmp/archive_failed.txt
      continue
    fi

    if out=$(hubspot conversations threads archive --id "$tid" --digest "$digest" --confirm "$tid" 2>&1); then
      archived=$((archived + 1))
    else
      printf '%s\tarchive\t%s\n' "$tid" "$out" >> /tmp/archive_failed.txt
    fi
  done < "$ids"

  failed=$(wc -l < /tmp/archive_failed.txt)
  echo "Archived $archived of $(wc -l < "$ids"); $failed failed"
  [ "$failed" -eq 0 ] || { echo "Failures (id / phase / error):"; cat /tmp/archive_failed.txt; return 1; }
}

archive_threads /tmp/archive_ids.txt

A failing thread is recorded and the run continues, but the function returns non-zero and prints every failure, so a partially drained inbox is never reported as a clean run. Re-running phase 2 on the same id file is safe — already-archived threads simply fail and are listed.

To undo, use restore:

hubspot conversations threads restore --id 111

8. Produce a triage summary

Count threads by status across an inbox:

echo "=== Inbox 123 triage summary ==="
conv_page /tmp/threads.jsonl conversations threads list --inbox-id 123

echo "Total threads:      $(wc -l < /tmp/threads.jsonl)"
echo "Open:               $(jq -c 'select(.status=="OPEN")' /tmp/threads.jsonl | wc -l)"
echo "Closed:             $(jq -c 'select(.status=="CLOSED")' /tmp/threads.jsonl | wc -l)"
echo "Unassigned (open):  $(jq -c 'select(.status=="OPEN") | select(.assignedTo==null or .assignedTo=="")' /tmp/threads.jsonl | wc -l)"

# Breakdown by channel type. Threads carry originalChannelId, not a type name,
# so build a channelId -> type map from `channels list` and join on it.
conv_page /tmp/channels.jsonl conversations channels list --inbox-id 123
CHANNEL_TYPES=$(jq -s 'map({key: (.id|tostring), value: .type}) | from_entries' /tmp/channels.jsonl)

echo ""
echo "=== By channel type ==="
jq -r --argjson types "$CHANNEL_TYPES" \
  '$types[(.originalChannelId | tostring)] // "UNKNOWN"' /tmp/threads.jsonl \
| sort | uniq -c | sort -rn

For a multi-inbox summary, loop over all inbox IDs from step 1 and aggregate.

Known constraints

  • threads list requires --inbox-id — there is no portal-wide thread listing.
  • No server-side filter on status or assignedTo — filter client-side with jq.
  • Every list command pages at 100 rows and only --format json exposes the cursor (.meta.next). Bulk and counting steps must use conv_page; a bare --format jsonl redirect silently truncates and makes a partial run look complete.
  • Threads carry no channelType — join originalChannelId against channels list for a type. A thread can switch channels mid-conversation, so this describes where it started.
  • Bulk archive cannot be collapsed into a single pipe through --digest/--confirm; each thread must be archived individually with its own two-step flow, and the candidate set must be previewed and approved before any of it executes.
  • Digests expire 5 minutes after the --dry-run that minted them, so they cannot be collected up front and applied later — mint each one immediately before use.
  • Drive bulk loops from a file rather than a pipe: a pipeline-fed while body runs in a subshell, so failure counts collected inside it are lost when the loop exits.
  • actors get looks up a single actor by ID; there is no actors list command. Start from hubspot owners list for owner-to-actor mapping.

FAQ

What does the conversation-inbox-triage skill do?

Audit and triage conversation inboxes — list open and unassigned threads, find stale conversations, assign threads to team members, archive resolved threads in bulk, and produce a status summary by inbox and channel.

How do I install conversation-inbox-triage?

Run npx skills add https://github.com/hubspot/agent-cli-skills --skill conversation-inbox-triage in your project. The skills CLI detects the agents you use and copies the skill into the right folder. For Claude Code you can also place the skill folder at ~/.claude/skills/conversation-inbox-triage/ (personal) or .claude/skills/conversation-inbox-triage/ (project).

Which AI agents can use conversation-inbox-triage?

Any agent that supports the open Agent Skills format (a folder with a SKILL.md file), including Claude Code, Codex, Cursor, GitHub Copilot, Gemini CLI, Windsurf, Cline, OpenCode and more.

Is conversation-inbox-triage safe to install?

No partner security audit is available yet. Skills can run commands and scripts, so read the SKILL.md and any bundled files before installing.

Who maintains conversation-inbox-triage?

It is published by hubspot in hubspot/agent-cli-skills. It has 186 recorded installs.

More from hubspot/agent-cli-skills

View all →

ticket-resolution

hubspot/agent-cli-skills

Create, triage, advance, and close HubSpot support tickets — pipeline discovery, contact/company association, priority queues, bulk stage moves, resolution close-out.

2K installs

crm-lookup

hubspot/agent-cli-skills

Find a specific CRM record by ID, email, domain, or name fragment, and traverse associations for the full account picture.

Business & Finance1.8K installs

bulk-operations

hubspot/agent-cli-skills

Foundation patterns for the hubspot CLI — JSONL piping, batch read, pagination, dry-run/digest/confirm for destructive ops, and hubspot history for recovery. Every other skill builds on this one.

1.8K installs

crm-data-quality

hubspot/agent-cli-skills

Find incomplete records, normalize field values in bulk, dedupe with hubspot objects merge, and audit custom properties. Builds on bulk-operations for JSONL piping and dry-run/digest/confirm.

Business & Finance1.8K installs

workflow-automation

hubspot/agent-cli-skills

List, inspect, create, update, and delete HubSpot workflows (v4 flows API) from the hubspot agent CLI, not the hs developer CLI. Classic v3 contact-based workflows are not covered.

deal-management

hubspot/agent-cli-skills

Run the full deal lifecycle from CLI — discover pipelines/stages, qualify MQLs into deals with associations, advance/reassign in bulk, hunt stalled deals, and close.

1.7K installs

Popular security skills

View category →

azure-compliance

microsoft/azure-skills

Run Azure compliance and security audits with azqr plus Key Vault expiration checks. Covers best-practice assessment, resource review, policy/compliance validation, and security posture checks. WHEN: compliance scan, security audit, BEFORE running azqr (compliance cli tool), Azure best practices, Key Vault expiration check, expired certificates, expiring secrets, orphaned resources, compliance assessment.

OfficialSecurity567K installs

minimal-run-and-audit

lllllllama/rigorpilot-skills

Rigor Run skill for README-first deep learning repo reproduction. Use when the task is specifically to capture or normalize evidence from the selected smoke test or documented inference or evaluation command and write standardized reprooutputs/ files, including patch notes when repository files changed. Do not use for training execution, initial repo intake, generic environment setup, paper lookup, target selection, hidden scientific-meaning changes, or end-to-end orchestration by itself.

Security477.4K installs

seo-audit

coreyhaines31/marketingskills

When the user wants to audit, review, or diagnose SEO issues on their site. Also use when the user mentions "SEO audit," "technical SEO," "why am I not ranking," "SEO issues," "on-page SEO," "meta tags review," "SEO health check," "my traffic dropped," "lost rankings," "not showing up in Google," "site isn't ranking," "Google update hit me," "page speed," "core web vitals," "crawl errors," "indexing issues," "stuck on page 2," "striking distance keywords," "title tag rewrite," "low CTR," "local SEO," "Google Business Profile," or "map pack." Use this even if the user just says something vague like "my SEO is bad" or "help with SEO" — start with an audit. For building pages at scale to target keywords, see programmatic-seo. For adding structured data, see schema. For AI search optimization, see ai-seo.

Security238.7K installs

firebase-security-rules-auditor

firebase/agent-skills

Audits Firebase (Firestore, Cloud Storage) security rules for vulnerabilities, privilege escalation, role bypasses, create vs update inconsistencies, resource exhaustion, type safety, size limits, and hasOnly ownership checks. Use when auditing/reviewing rules, running red-team rule assessments, or scoring against auditor checklists. Don't use for Firebase CLI (login, deploy), Auth, Crashlytics, Remote Config, or database queries.

OfficialSecurity139.1K installs

audit

pbakaus/impeccable

Run technical quality checks across accessibility, performance, theming, responsive design, and anti-patterns. Generates a scored report with P0-P3 severity ratings and actionable plan. Use when the user wants an accessibility check, performance audit, or technical quality review.

Security81.9K installs

competitors

coreyhaines31/marketingskills

When the user wants to create competitor comparison or alternative pages for SEO and buyer-facing use. Also use when the user mentions 'alternative page,' 'vs page,' 'competitor comparison,' 'comparison page,' '[Product] vs [Product],' '[Product] alternative,' 'competitive landing pages,' 'how do we compare to X,' 'competitor teardown,' 'audit our competitor pages,' 'are our comparison pages out of date,' or 'competitive asset audit.' Use this for any content that positions your product against competitors. Covers four formats: singular alternative, plural alternatives, you vs competitor, and competitor vs competitor. For auditing existing claims (not researching competitors from scratch, which is competitor-profiling; not technical SEO on these pages, which is seo-audit), use the asset audit here. For internal battle cards and sales-specific competitor docs, see sales-enablement.

Security79.3K installs