- 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- 01
Install
Run the command in your project. The CLI asks which agents to install for.
- 02
Describe the task
Ask your agent for something this skill covers. It loads conversation-inbox-triage automatically when relevant.
- 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_SECONDSinsrc/hub/guard.rs), so digests minted during preview would come backdigest_expiredafter 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 listrequires--inbox-id— there is no portal-wide thread listing.- No server-side filter on
statusorassignedTo— filter client-side withjq. - Every
listcommand pages at 100 rows and only--format jsonexposes the cursor (.meta.next). Bulk and counting steps must useconv_page; a bare--format jsonlredirect silently truncates and makes a partial run look complete. - Threads carry no
channelType— joinoriginalChannelIdagainstchannels listfor 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-runthat 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
whilebody runs in a subshell, so failure counts collected inside it are lost when the loop exits. actors getlooks up a single actor by ID; there is no actors list command. Start fromhubspot owners listfor 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
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.
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.
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.
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.
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.
Popular security skills
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.
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.
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.
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.
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.
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.
