conversation-inbox-triage
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseRequires the command group from hub-cli PR #251. is authoritative. Archive uses the dry-run/digest/confirm guard pattern — follow it exactly.
conversationshubspot conversations --help需要使用 hub-cli PR #251 中的 命令组。 是权威说明。归档操作采用 dry-run/digest/confirm 防护模式——请严格按照该模式执行。
conversationshubspot conversations --helpPaging: never triage from a --format jsonl
redirect
--format jsonl分页:切勿通过 --format jsonl
重定向执行分流
--format jsonlEvery conversations command defaults to and returns a cursor for the next page. prints only the data rows — the cursor is surfaced only by , at . So 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 does not fix it — the endpoint caps the page size.
list--limit 100--format jsonl--format json.meta.nextthreads list --format jsonl > file--limitUse this helper for every bulk or counting operation:
bash
undefined每个 conversations 命令默认使用 并返回下一页的游标。** 只打印数据行——游标仅由 暴露,位于 。**因此, 会在 100 个线程处静默停止:在繁忙的收件箱中,操作人员看到的是一次正常结束的运行,而第一个分页之后的所有线程都未被分配、未被归档,也未计入统计。提高 也无法解决——该端点会对页面大小设置上限。
list--limit 100--format jsonl--format json.meta.nextthreads list --format jsonl > file--limit所有批量处理或计数操作都要使用以下辅助函数:
bash
undefinedconv_page <output-file> <conversations list args...>
conv_page <output-file> <conversations list args...>
Pages a conversations list endpoint to exhaustion, writing one JSON object
Pages a conversations list endpoint to exhaustion, writing one JSON object
per line to <output-file>.
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.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
}
第 2–4 步中直接使用 `--format jsonl` 没问题,适合交互式快速查看第一页。第 6–8 步会修改数据或进行统计,因此必须分页。1. Discover inboxes
1. 发现收件箱
Inbox IDs are portal-specific. Always enumerate at the start of a session — never hardcode.
bash
conv_page /tmp/inboxes.jsonl conversations inboxes list收件箱 ID 与具体 portal 绑定。始终在每次会话开始时枚举收件箱——切勿硬编码。
bash
conv_page /tmp/inboxes.jsonl conversations inboxes list{"id":"123","name":"Support Inbox","createdAt":"...","updatedAt":"..."}
{"id":"123","name":"Support Inbox","createdAt":"...","updatedAt":"..."}
{"id":"124","name":"Sales Chat","createdAt":"...","updatedAt":"..."}
{"id":"124","name":"Sales Chat","createdAt":"...","updatedAt":"..."}
To see channels inside an inbox (email, live chat, form, etc.):
```bash
conv_page /tmp/channels.jsonl conversations channels list --inbox-id 123
要查看某个收件箱中的渠道(email、live chat、form 等),请执行:
```bash
conv_page /tmp/channels.jsonl conversations channels list --inbox-id 123{"id":"456","name":"Support Email","type":"EMAIL","inboxId":"123"}
{"id":"456","name":"Support Email","type":"EMAIL","inboxId":"123"}
undefinedundefined2. List open threads
2. 列出开放线程
threads list--inbox-id.statusbash
undefinedthreads list--inbox-id.statusbash
undefinedall threads in an inbox
all threads in an inbox
hubspot conversations threads list --inbox-id 123 --format jsonl
hubspot conversations threads list --inbox-id 123 --format jsonl
only OPEN threads
only OPEN threads
hubspot conversations threads list --inbox-id 123 --format jsonl
| jq -c 'select(.status == "OPEN")'
| jq -c 'select(.status == "OPEN")'
Output shape per row:
```json
{"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 — it names its originating channel by id, in , which you join against (step 8). is also not guaranteed; read it through a fallback.
channelTypeoriginalChannelIdchannels listsubject// "(no subject)"hubspot conversations threads list --inbox-id 123 --format jsonl
| jq -c 'select(.status == "OPEN")'
| jq -c 'select(.status == "OPEN")'
每行的输出结构:
```json
{"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}会话线程没有 ——它通过 中的 ID 标识原始渠道,需要与 (第 8 步)进行关联。 也不保证存在;请使用 作为回退值读取。
channelTypeoriginalChannelIdchannels listsubject// "(no subject)"3. Find unassigned threads
3. 查找未分配线程
assignedTobash
hubspot conversations threads list --inbox-id 123 --format jsonl \
| jq -c 'select(.status == "OPEN") | select(.assignedTo == null or .assignedTo == "")'assignedTobash
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)
4. 查找陈旧线程(N 小时内无回复)
latestMessageTimestampbash
undefinedlatestMessageTimestampbash
undefinedmacOS: threads with no activity in 24 hours
macOS: threads with no activity in 24 hours
CUTOFF=$(date -u -v-24H +%Y-%m-%dT%H:%M:%SZ)
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)
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)'
| 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`.hubspot conversations threads list --inbox-id 123 --format jsonl
| jq -c --arg cutoff "$CUTOFF"
'select(.status == "OPEN") | select(.latestMessageTimestamp < $cutoff)'
| jq -c --arg cutoff "$CUTOFF"
'select(.status == "OPEN") | select(.latestMessageTimestamp < $cutoff)'
对于 48 小时和 72 小时版本,将 `-v-24H` 改为 `-v-48H` 或 `-v-72H`。5. Look up an actor ID
5. 查询 actor ID
assignA-bash
hubspot conversations actors get --id A-123456 --format jsonlTo map owner IDs to actor IDs, cross-reference (returns and ) and use the actor ID format as a starting point — verify with that the ID resolves.
hubspot owners list --format jsonl.email.idA-<ownerId>actors getowners list403 This endpoint does not support user-level OAuth tokensHUBSPOT_ACCESS_TOKENconversationsassignA-bash
hubspot conversations actors get --id A-123456 --format jsonl要将 owner ID 映射为 actor ID,请交叉查询 (返回 和 ),并以 actor ID 格式 作为起点——再使用 验证该 ID 能否正确解析。
hubspot owners list --format jsonl.email.idA-<ownerId>actors getowners list403 This endpoint does not support user-level OAuth tokensHUBSPOT_ACCESS_TOKENconversations6. Assign a thread
6. 分配线程
bash
hubspot conversations threads assign --id 111 --actor-id A-123456bash
hubspot conversations threads assign --id 111 --actor-id A-123456{"id":"111","ok":true}
{"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.
```bash
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
批量分配(将所有未分配的开放线程分配给一个 actor)。这里有两个关键点:ID 列表必须从**文件**读取,而不是管道——由管道提供输入的 `while` 循环在子 shell 中运行,因此循环结束时计数器和失败列表都会丢失;并且最后要返回非零状态,避免将部分完成的运行误认为完整完成。
```bash
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.txt7. Archive resolved threads
7. 归档已解决的线程
Archive is guarded: run first to get a digest, then re-run with and (value = thread ID).
--dry-run--digest--confirmbash
undefined归档操作有防护机制:先运行 获取 digest,然后使用 和 重新运行(其值 = 线程 ID)。
--dry-run--digest--confirmbash
undefinedSingle thread — two steps
Single thread — two steps
hubspot conversations threads archive --id 111 --dry-run
hubspot conversations threads archive --id 111 --dry-run
{"id":"111","digest":"blast-29cfdd48b583","ok":true,"executed":false,"dry_run":true}
{"id":"111","digest":"blast-29cfdd48b583","ok":true,"executed":false,"dry_run":true}
hubspot conversations threads archive --id 111 --digest blast-29cfdd48b583 --confirm 111
hubspot conversations threads archive --id 111 --digest blast-29cfdd48b583 --confirm 111
{"id":"111","ok":true}
{"id":"111","ok":true}
undefinedundefinedBulk 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.
bash
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.jsonlStop 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 (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
Phase 2 — execute. Only after the list above is approved.
bash
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.txtA 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 :
restorebash
hubspot conversations threads restore --id 111批量归档分为两个独立阶段,中间需要人工决策。不要将它们合并为一个循环,逐个执行 dry-run 并归档线程:这样会在任何人查看完整候选集之前就开始销毁线程,而且整个操作将不存在可供整体拒绝的节点。
阶段 1——预览。只读;不会修改任何内容。
bash
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在此停止并获取候选列表的批准。阶段 1 可以任意次重新运行;它不会造成任何变更。
保存候选线程的 ID,而不是 digest。digest 仅在 5 分钟内有效(位于DIGEST_TTL_SECONDS中),因此,在预览期间生成的 digest 会在任何合理的人工审查暂停后返回src/hub/guard.rs。所以,阶段 2 会在使用每个 digest 前立即生成它。digest_expired
阶段 2——执行。仅在上方列表获得批准后执行。
bash
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\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\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失败的线程会被记录,运行会继续,但函数将返回非零状态并打印每项失败,因此不会将仅部分完成归档的收件箱报告为正常完成。对同一 ID 文件重新运行阶段 2 是安全的——已归档的线程只会失败并被列出。
如需撤销,请使用 :
restorebash
hubspot conversations threads restore --id 1118. Produce a triage summary
8. 生成分流汇总
Count threads by status across an inbox:
bash
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)"统计一个收件箱中各状态的线程数量:
bash
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,
Breakdown by channel type. Threads carry originalChannelId, not a type name,
so build a channelId -> type map from channels list
and join on it.
channels listso build a channelId -> type map from channels list
and join on it.
channels listconv_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
'$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.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
'$types[(.originalChannelId | tostring)] // "UNKNOWN"' /tmp/threads.jsonl
| sort | uniq -c | sort -rn
如需汇总多个收件箱,请遍历第 1 步获得的所有收件箱 ID 并进行聚合。Known constraints
已知限制
- requires
threads list— there is no portal-wide thread listing.--inbox-id - No server-side filter on or
status— filter client-side withassignedTo.jq - Every command pages at 100 rows and only
listexposes the cursor (--format json). Bulk and counting steps must use.meta.next; a bareconv_pageredirect silently truncates and makes a partial run look complete.--format jsonl - Threads carry no — join
channelTypeagainstoriginalChannelIdfor a type. A thread can switch channels mid-conversation, so this describes where it started.channels list - Bulk archive cannot be collapsed into a single pipe through /
--digest; 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.--confirm - Digests expire 5 minutes after the that minted them, so they cannot be collected up front and applied later — mint each one immediately before use.
--dry-run - Drive bulk loops from a file rather than a pipe: a pipeline-fed body runs in a subshell, so failure counts collected inside it are lost when the loop exits.
while - looks up a single actor by ID; there is no actors list command. Start from
actors getfor owner-to-actor mapping.hubspot owners list
- 要求指定
threads list——不存在 portal 范围的线程列表。--inbox-id - 没有针对 或
status的服务端过滤器——请使用assignedTo在客户端过滤。jq - 每个 命令每页返回 100 行,且只有
list会暴露游标(--format json)。批量处理和统计步骤必须使用.meta.next;直接使用conv_page重定向会静默截断结果,使部分运行看起来像完整运行。--format jsonl - 线程不携带 ——需将
channelType与originalChannelId进行关联以获取类型。线程可能会在会话过程中切换渠道,因此该类型描述的是会话开始时的渠道。channels list - 不能将批量归档简化为一条串联 /
--digest的管道流程;每个线程都必须采用各自的两步流程单独归档,并且必须先预览并批准整个候选集,然后才能执行任何归档。--confirm - 由某次 生成的 digest 会在 5 分钟后过期,因此不能预先收集后再统一应用——必须在每次使用前立即生成。
--dry-run - 批量循环应由文件而非管道驱动:由管道提供输入的 循环体在子 shell 中运行,因此循环退出时,内部收集的失败计数会丢失。
while - 按 ID 查询单个 actor;没有 actors list 命令。可从
actors get开始进行 owner 到 actor 的映射。hubspot owners list