Loading...
Loading...
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.
npx skill4agent add hubspot/agent-cli-skills conversation-inbox-triageconversationshubspot conversations --help--format jsonllist--limit 100--format jsonl--format json.meta.nextthreads list --format jsonl > file--limit# 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
}--format jsonlconv_page /tmp/inboxes.jsonl conversations inboxes list
# {"id":"123","name":"Support Inbox","createdAt":"...","updatedAt":"..."}
# {"id":"124","name":"Sales Chat","createdAt":"...","updatedAt":"..."}conv_page /tmp/channels.jsonl conversations channels list --inbox-id 123
# {"id":"456","name":"Support Email","type":"EMAIL","inboxId":"123"}threads list--inbox-id.status# 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")'{"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}channelTypeoriginalChannelIdchannels listsubject// "(no subject)"assignedTohubspot conversations threads list --inbox-id 123 --format jsonl \
| jq -c 'select(.status == "OPEN") | select(.assignedTo == null or .assignedTo == "")'latestMessageTimestamp# 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)'-v-24H-v-48H-v-72HassignA-hubspot conversations actors get --id A-123456 --format jsonlhubspot owners list --format jsonl.email.idA-<ownerId>actors getowners list403 This endpoint does not support user-level OAuth tokensHUBSPOT_ACCESS_TOKENconversationshubspot conversations threads assign --id 111 --actor-id A-123456
# {"id":"111","ok":true}whileACTOR=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--dry-run--digest--confirm# 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}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.jsonlPersist candidate ids, not digests. A digest is only valid for 5 minutes (inDIGEST_TTL_SECONDS), so digests minted during preview would come backsrc/hub/guard.rsafter any realistic review pause. Phase 2 therefore mints each digest immediately before it uses it.digest_expired
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.txtrestorehubspot conversations threads restore --id 111echo "=== 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 -rnthreads list--inbox-idstatusassignedTojqlist--format json.meta.nextconv_page--format jsonlchannelTypeoriginalChannelIdchannels list--digest--confirm--dry-runwhileactors gethubspot owners list