conversation-inbox-triage について
hub-cliを使ってHubSpotの会話スレッドを管理するトリアージスキルです。ページングによる全件取得、バルク操作の安全性、アーカイブ前の承認を重視します。
主な機能
- 01受信トレイとチャネルの確認
- 02未割り当てや滞留中のスレッド検索
- 03スレッドを担当者に割り当て
- 04解決済みスレッドのプレビューと保護付きアーカイブ
- 05全件取得のためのページングと集計
- 06受信トレイのスレッド状況を集計
活用シーン
- 01未割り当てや返信遅延のスレッドを特定
- 02オープン中の会話をチームメンバーに割り当て
- 03アーカイブ前に解決済みスレッドを確認
- 04複数の受信トレイのスレッドを漏れなく集計
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ hubspot/ agent-cli-skills --skill conversation-inbox-triageインストール後、Claude Code で/conversation-inbox-triageを入力して読み込まれたか確認します。
詳細
hub-cli PR #251 の conversations コマンドグループが必要です。hubspot conversations --help が正式な情報源です。アーカイブは dry-run/digest/confirm のガードパターンを使用します — これに正確に従ってください。
ページング: --format jsonl のリダイレクトからトリアージしないこと
すべての conversations の list コマンドはデフォルトで --limit 100 になり、次のページ用のカーソルを返します。--format jsonl はデータ行のみを出力します — カーソルは --format json でのみ、.meta.next に現れます。 そのため threads list --format jsonl > file は 100 スレッドで暗黙に停止します。混雑した受信トレイでは、オペレーターにはクリーンな実行に見える一方で、最初のページ以降のすべてのスレッドは割り当てられず、アーカイブされず、カウントからも漏れます。--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
}
ステップ 2〜4 の素の --format jsonl 呼び出しは、最初のページを対話的に目視確認するには問題ありません。ステップ 6〜8 は変更またはカウントを行うため、ページングします。
1. 受信トレイを検出する
受信トレイ ID はポータル固有です。セッションの開始時に必ず列挙してください — ハードコードは決してしないでください。
conv_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"}
2. オープンスレッドを一覧表示する
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}
スレッドには channelType はありません — 元のチャネルを originalChannelId 内で id によって示し、これを channels list(ステップ 8)と結合します。subject も保証されていません。// "(no subject)" フォールバックを通して読み取ってください。
3. 未割り当てのスレッドを検出する
未割り当てのスレッドでは assignedTo は null または存在しません:
hubspot conversations threads list --inbox-id 123 --format jsonl \
| jq -c 'select(.status == "OPEN") | select(.assignedTo == null or .assignedTo == "")'
4. 滞留スレッドを検出する(N 時間以内に返信がない)
latestMessageTimestamp は ISO 8601 であり、辞書順で比較されます。
# 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)'
48 時間および 72 時間のバリアントについては、-v-24H を -v-48H / -v-72H に変更してください。
5. アクター ID を検索する
assign はアクター ID を取ります。チームメンバーのアクター ID を、そのエージェント ID またはメールアドレスから取得します。HubSpot のアクター ID は A- プレフィックスを使用します:
hubspot conversations actors get --id A-123456 --format jsonl
オーナー ID をアクター ID にマッピングするには、hubspot owners list --format jsonl(.email と .id を返します)を相互参照し、アクター ID の形式 A-<ownerId> を出発点として使用してください — その ID が解決することを actors get で検証してください。
owners list にはサービスキーが必要です。OAuth ログイン下では 403 This endpoint does not support user-level OAuth tokens を返すため、オーナーからアクターへの検索の前に HUBSPOT_ACCESS_TOKEN をサービスキーに設定してください。conversations コマンド自体はどちらの認証モードでも動作します。
6. スレッドを割り当てる
hubspot conversations threads assign --id 111 --actor-id A-123456
# {"id":"111","ok":true}
一括割り当て(すべての未割り当てのオープンスレッドを 1 人のアクターに)。ここで重要な点が 2 つあります: id リストをパイプではなくファイルから読み取ること — パイプラインで供給された while はサブシェルで実行されるため、終了時にカウンターと失敗リストが破棄されます — そして最後に非ゼロのステータスを報告し、部分的な実行が完全な実行と誤認されないようにすることです。
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. 解決済みスレッドをアーカイブする
アーカイブはガードされています: まず --dry-run を実行してダイジェストを取得し、次に --digest と --confirm(値 = スレッド 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}
一括アーカイブ — プレビューしてから実行
一括アーカイブは、間に人間による判断を挟む 2 つの別個のフェーズとして実行されます。ドライランと各スレッドのアーカイブを順番に行う 1 つのループにまとめてはいけません。それでは、候補の全体像が誰にも確認される前にスレッドの破壊が始まってしまい、操作全体をまとめて拒否できるタイミングが存在しなくなります。
フェーズ 1 — プレビュー。読み取り専用で、何も変更されません。
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 であり、ダイジェストではありません。ダイジェストは 5 分間だけ有効です(
src/hub/guard.rsのDIGEST_TTL_SECONDS)。そのため、プレビュー中に発行されたダイジェストは、現実的なレビューの中断を挟んだ後ではdigest_expiredを返します。したがってフェーズ 2 では、各ダイジェストを使用する直前に発行します。
フェーズ 2 — 実行。上記のリストが承認された後にのみ行います。
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
失敗したスレッドは記録され、実行は継続しますが、関数は非ゼロを返し、すべての失敗を出力します。そのため、部分的にしか処理されていない受信トレイが、クリーンな実行として報告されることはありません。同じ ID ファイルに対してフェーズ 2 を再実行しても安全です。すでにアーカイブ済みのスレッドは単に失敗し、リストに表示されます。
元に戻すには、restore を使用します:
hubspot conversations threads restore --id 111
8. トリアージサマリーを生成する
受信トレイ全体のスレッドをステータス別に集計します:
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
複数の受信トレイのサマリーが必要な場合は、ステップ 1 で取得したすべての受信トレイ ID をループして集計します。
既知の制約
threads listには--inbox-idが必要です。ポータル全体のスレッド一覧はありません。statusやassignedToに対するサーバー側のフィルターはありません。クライアント側でjqを使ってフィルタリングしてください。- すべての
listコマンドは 100 行単位でページングし、カーソル(.meta.next)を公開するのは--format jsonだけです。一括処理と集計のステップではconv_pageを使用する必要があります。素の--format jsonlへのリダイレクトは黙って切り捨てられ、部分的な実行が完了したように見えてしまいます。 - スレッドには
channelTypeがありません。型を知るにはoriginalChannelIdをchannels listと結合してください。スレッドは会話の途中でチャネルを切り替えることがあるため、これはスレッドがどこで 開始された かを示すものです。 - 一括アーカイブは、
--digest/--confirmを通す単一のパイプにまとめることはできません。各スレッドはそれぞれ独自の 2 ステップのフローで個別にアーカイブする必要があり、候補セットは実行前にプレビューして承認されなければなりません。 - ダイジェストは、それを発行した
--dry-runの 5 分後に失効するため、事前にまとめて収集して後から適用することはできません。使用する直前に 1 つずつ発行してください。 - 一括ループはパイプではなくファイルから駆動してください。パイプから供給された
whileの本体はサブシェルで実行されるため、その内部で収集された失敗カウントはループ終了時に失われます。 actors getは ID で単一のアクターを検索します。actors の一覧コマンドはありません。オーナーとアクターのマッピングには、hubspot owners listから始めてください。