bulk-operations について
bulk-operations は、HubSpot CLI で CRM データを一括処理するスキルです。JSONL パイプ、ページネーション、ペイロード変換を説明し、取り消せない操作にはプレビューと確認の手順を設けています。
主な機能
- 01JSONL パイプによる一括読み書き
- 02付属スクリプトによるページネーション
- 03jq による読み書きデータの変換
- 04取り消せない操作のプレビューと確認
- 05操作履歴と失敗結果の確認
活用シーン
- 01条件に合う CRM レコードの一括更新や削除
- 02外部 JSONL データからのレコード作成・更新
- 03レコードをページ取得して書き込みデータを準備
- 04一括操作の監査と失敗行の確認
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ hubspot/ agent-cli-skills --skill bulk-operationsインストール後、Claude Code で/bulk-operationsを入力して読み込まれたか確認します。
同梱ファイルresources/json-patterns.mdresources/pagination-loop.sh
詳細
リソース
| ファイル | 使用するタイミング |
|---|---|
resources/json-patterns.md | 読み取りを更新ペイロードに、検索を削除リストに、CSV を upsert ストリームに変換するための整形パターンです。 |
信頼できる情報源
これは hubspot エージェント CLI です。hs 開発者 CLI (@hubspot/cli) は別のツールであり、CRM データやワークフローを管理しません。hubspot <command> --help が正式な情報です。このファイルの内容が --help と矛盾する場合は、--help を信頼し、ユーザーに伝えてください。セッションの開始時に一度 hubspot objects types を実行して、このポータルに存在するオブジェクトタイプ(標準 + カスタム)を確認してください。
フィードバックを送信
hubspot feedback コマンドを使用して、この CLI ツールの所有者にメッセージを送信します。--source agent を渡して、エージェントのトラフィックとして帰属させてください(デフォルトは user です):
hubspot feedback "batch upsert timed out on 5k rows" --source agent
これは次のような内容であれば何でもかまいません:
- 遭遇した具体的なバグや不具合
- CLI を使用する前に知っておきたかったこと
- ユーザーが混乱した、いらいらした、または怒ったこと
- ユーザーが求めたが、あなたが実行できなかったこと
- 今後のタスクを容易にするために存在してほしいと思ったツール、機能、スキル
短い1行で送信でき、アクティブな HubSpot アカウントに紐付けられ、タスクをブロックしません。送信して作業を続けてください。
出力形式
すべての読み取りコマンド (list、search、get) は JSONL を出力します — 1行につき1つの JSON オブジェクトです:
{"id":"123","properties":{"email":"[email protected]","firstname":"Jane"},"createdAt":"...","updatedAt":"...","archived":false,"url":"..."}
--properties email,firstname は、サーバーが .properties の下で返すフィールドを制限します。後続の jq は .prop_email ではなく .properties.email を使用する必要があります。
書き込みコマンド (create、update、upsert、delete、merge、associations create) は標準入力で JSONL を受け取り、JSONL を出力します — 入力行ごとに1つの結果です: {"id":"123","ok":true,"data":{...}} または {"id":"123","ok":false,"error":{"status":...,"message":"..."}}。結果の順序は入力順と一致します — 行の位置で結合しても安全です。
バッチで読み取る — 決して1つずつ読み取らない
CLI は複数の ID をネイティブに受け付けます。決して ID を xargs -I{} hubspot objects get ... にパイプしないでください — それはレコードごとに1つの CLI プロセスを起動します。
# Positional args (small, known list)
hubspot objects get --type contacts 12345 67890 23456 --properties email,firstname
# Stdin from another command — one CLI call total
hubspot associations list --from companies:67890 --to contacts \
| jq -c '{id}' \
| hubspot objects get --type contacts --properties email,firstname,jobtitle
# Bare IDs on stdin also work
printf '12345\n67890\n23456\n' | hubspot objects get --type contacts --properties email
1回の hubspot objects get は、バッチエンドポイントを介して1回の呼び出しで最大約100個の ID を読み取ります。それ以上の場合は、100個ずつのチャンクでページングしてください。
一括フロー: 最初にページングし、次に整形し、最後に書き込む
あるタイプのすべてのレコード(またはフィルターのすべての一致)を操作する場合、必ず pagination-loop.sh から始めてください — 「いくつあるか確認する」ために裸の list や search を実行しないでください。裸の呼び出しは最大100レコードしか返さず、いずれにせよ再取得する必要があります。まずジョブの規模を見積もるには、hubspot objects count --type <t> [--filter "..."] を使用してください。これはページングせずに一致する合計数(例: {"object_type":"contacts","total":42})を返します。
標準的な一括パターンは次のとおりです:
- すべてのレコードを JSONL ファイルにページングする
jqで書き込みペイロードに整形する- 最初に
--dry-runを付けて書き込みコマンド (update、deleteなど) にパイプする
ページング
list と search は1回の呼び出しにつき最大100レコードを返します。resources/pagination-loop.sh を使用して、すべてのページを単一の JSONL ファイルに収集してください:
bash resources/pagination-loop.sh <object_type> <output_file> [properties] [extra_flags...]
例:
# All contacts with specific properties
bash resources/pagination-loop.sh contacts /tmp/contacts.jsonl email,firstname,lastname
# Search with a filter (passes extra flags through to the CLI)
bash resources/pagination-loop.sh contacts /tmp/leads.jsonl email,firstname '--filter' 'lifecyclestage=lead'
# All deals, default properties
bash resources/pagination-loop.sh deals /tmp/deals.jsonl
このスクリプトは --after カーソルを自動的にページングし、進行状況を stderr に出力し、JSONL を出力ファイルに書き込みます。単一のフォアグラウンドコマンドとして実行してください — バックグラウンドで実行したり、ループをインラインで再構築したりしないでください。
結果の並び順
search はデフォルトで新しい順に返します。これは、オブジェクトタイプが定義する作成日プロパティ(連絡先では createdate、アクティビティでは hs_createdate など)で降順にソートするためで、最初のページにはポータルの 2017 年頃のレコードではなく最新のレコードが入ります。他のソート可能なプロパティでソートするには --sort <property>(hs_lastmodifieddate、amount など)を使い、方向を反転するには --sort-dir asc を使います。list にはソートの制御がありません。順序が重要な場合は search を使用してください。
associations list もページネーションを行います(--limit / --after、デフォルトの limit は 100)。--format json では、次ページのカーソルは .meta.next にあり、最後のページに達すると null になります。1 つのレコードには 1 ページに収まる数をはるかに超える関連レコードが存在し得るため、カーソルが存在する場合は「まだ残っている」とみなし、null になるまで --after でページを進めてください。
バッチで書き込む — 常にパイプする
書き込みコマンドは stdin で JSONL を受け付けます。読み取り用の形から書き込み用の形への変換は jq による整形です:
| 書き込みコマンド | 1 行あたりに必要な形 |
|---|---|
objects create | {"properties":{"field":"value"}} |
objects update | {"id":"123","properties":{"field":"value"}} |
objects upsert | {"idProperty":"email","id":"[email protected]","properties":{...}}(または一度だけ --id-property email を使用) |
objects delete | {"id":"123"} |
objects merge | {"primary":"123","secondary":"456"} |
associations create | {"from":"contacts:123","to":"companies:456"} |
from/to には複数形のオブジェクト名を使用してください(contact: ではなく contacts:)。
安全な破壊的ワークフロー
不可逆的な書き込み — objects delete、merge、update、upsert、associations delete / limits-update、およびメタデータの削除(ワークフロー、セグメント、ビュー、レポート、スキーマ、パイプライン、プロパティ)— は、常に 2 段階のドライラン → ダイジェスト → 確認のフローを必要とします。行数に関係なく(1 件のレコードでも 10k 件と同じくらい必要です)です。ソフト書き込み(objects create、views create/update/replace-field、ラベルのみの properties update、segments members-add)はダイジェストなしで実行されます。これらの --dry-run は単なるプレビューです。
1. ドライラン — すべてのサイズで、呼び出し全体に対して 1 行のプレビュー行を出力します(レコードごとではありません):
{"ok":true,"dry_run":true,"executed":false,"mutation_kind":"RecordMutation","command":"objects delete --type contacts","target":{"kind":"contacts_record","id":"123","name":"123"},"impact":{"records_affected":1,"note":"Delete 1 contacts record(s)","reversible":false},"portal":"123456","digest":"blast-29cfdd48b583","expires_in_seconds":300,"apply_command_hint":"hubspot objects delete --type contacts --digest blast-29cfdd48b583 --confirm '123'"}
mutation_kind は 100 行以下では RecordMutation、バルクしきい値を超えると BulkData になります。どちらの場合もダイジェストは存在するため、mutation_kind でフィルタリングしないでください。確認する値はコマンドによって異なります: objects delete/update → レコード ID(単一)または行数(2 件以上のバッチ)、objects upsert → 行数(常に)、objects merge → セカンダリ ID(1 ペア)または行数(バッチ)、メタデータの削除 → 対象の名前(ワークフロー/ビュー/レポート/スキーマ/パイプライン)、プロパティ/オプションの名前、または行数(バッチアーカイブ、関連のバッチ操作)。確認する値を自分で構築しないでください。apply_command_hint(またはプレビュー行の .target.id / .target.name)からコピーしてください。
2. 実行 は 5 分以内に(ダイジェストの TTL は 300 秒): 同じ入力を再パイプし、--digest と --confirm を加えます:
# 1. Preview
hubspot objects search --type contacts --filter "lifecyclestage=subscriber" \
| jq -c '{id}' \
| hubspot objects delete --type contacts --dry-run \
| tee /tmp/preview.jsonl
# 2. Lift the digest + confirm value (present at EVERY row count)
digest=$(jq -r 'select(.digest != null) | .digest' /tmp/preview.jsonl)
confirm=$(jq -r 'select(.digest != null) | .target.id' /tmp/preview.jsonl)
# 3. Execute — re-pipe the SAME inputs
hubspot objects search --type contacts --filter "lifecyclestage=subscriber" \
| jq -c '{id}' \
| hubspot objects delete --type contacts --digest "$digest" --confirm "$confirm"
--digest なしで実行すると digest_required で失敗します(エラーの next_step がドライランコマンドを示します)。確認する値が間違っていると confirm_mismatch で失敗し、ダイジェストが期限切れの場合は digest_expired で失敗します。--force は非推奨であり、無視されます。
hubspot history による復元
すべての破壊的な操作 (およびそのドライラン) はローカルに記録されます。直近 1 時間に何が起きたか、何が元に戻せるかを確認できます:
hubspot history --since 1h --format table
hubspot history --since 24h --kind BulkData # only bulk ops
hubspot history --since 7d --kind MetadataDestroy # schema deletes
history は現在レコードを復元しません — これは監査ログです。誤って何かを削除してしまった場合は、履歴の行を控えて、UI から復元するようユーザーに伝えてください。
CRM プロパティの source 履歴 (誰または何がプロパティを変更したか — WORKFLOW、INTEGRATION、IMPORT、CRM_UI) については、hubspot objects history --type <t> --properties <p> を使用します。これは各プロパティのバージョン履歴を、変更ごとに 1 行へと平坦化します。UI 駆動 (CRM_UI) の変更はデフォルトで除外されます (--include-ui を付けると保持されます)。--id <recordId> を追加すると 1 件のレコードを読み取り、省略するとページをスキャンします。これは上記のローカルな hubspot history 監査ログとは別物です — 一括操作の後でプロパティがなぜ変更されたかを調査するために使用してください。
Upsert は search-then-create より優れています
「存在しなければ作成、存在すれば更新」 (エンリッチメントパターン) には、upsert を使用します — レコードごとに 1 回の CLI 呼び出しで、競合状態がありません:
cat external.jsonl \
| jq -c '{idProperty:"email", id:.email, properties:{firstname:.first, lastname:.last, company:.company}}' \
| hubspot objects upsert --type contacts --dry-run
# Or set idProperty once (upsert is irreversible — dry-run first, then execute):
cat external.jsonl \
| jq -c '{id:.email, properties:{firstname:.first}}' \
| hubspot objects upsert --type contacts --id-property email --dry-run \
| tee /tmp/upsert.preview.jsonl
digest=$(jq -r 'select(.digest != null) | .digest' /tmp/upsert.preview.jsonl)
confirm=$(jq -r 'select(.digest != null) | .target.id' /tmp/upsert.preview.jsonl) # upsert confirm = the row count, even for one row
cat external.jsonl \
| jq -c '{id:.email, properties:{firstname:.first}}' \
| hubspot objects upsert --type contacts --id-property email --digest "$digest" --confirm "$confirm"
レート制限の衛生管理
create、update、upsert は標準入力を最大 100 件のチャンクにまとめ、実際の batch/create / batch/update / batch/upsert エンドポイントを呼び出します — 1 行ごとではなく、100 行ごとに 1 回の API 呼び出しです。delete は標準入力の 1 行につき 1 回の API 呼び出しを発行します。5 万行のファイルをパイプする前に head -n 50 でテストしてください — あるいは用途に特化した一括取り込みには hubspot imports を使用してください。API が 429 を返し始めた場合、1 行ごと (またはチャンクごと) の出力に {"ok":false,"error":{"status":429,...}} が表示されます — 入力ファイルを分割し、失敗した行を再試行してください。
大規模な CSV の取り込みには、hubspot imports が用途に特化した方法です: imports start (CSV ファイル + インポートリクエスト JSON から。--dry-run をサポート)、imports list、imports get <id>、imports cancel <id> (不可逆 — まずドライランし、その後インポート ID を指定して --digest/--confirm)、そして imports errors <id>。リクエストの形式については hubspot imports --help を実行してください。
よくある整形
完全なセットについては resources/json-patterns.md を参照してください。90% の場合に必要になる 2 つは次のとおりです:
# Read → update payload (update is irreversible — dry-run, then re-pipe with the digest/confirm from the preview)
hubspot objects search --type contacts --filter "industry=Tech" \
| jq -c '{id, properties:{lifecyclestage:"marketingqualifiedlead"}}' \
| hubspot objects update --type contacts --dry-run \
| tee /tmp/update.preview.jsonl
digest=$(jq -r 'select(.digest != null) | .digest' /tmp/update.preview.jsonl)
confirm=$(jq -r 'select(.digest != null) | .target.id' /tmp/update.preview.jsonl) # single: record ID; batch of 2+: row count
hubspot objects search --type contacts --filter "industry=Tech" \
| jq -c '{id, properties:{lifecyclestage:"marketingqualifiedlead"}}' \
| hubspot objects update --type contacts --digest "$digest" --confirm "$confirm"
# Search → delete list (delete is irreversible — dry-run, lift, then re-pipe with --digest/--confirm)
hubspot objects search --type contacts --filter "!email" \
| jq -c '{id}' \
| hubspot objects delete --type contacts --dry-run \
| tee /tmp/delete.preview.jsonl
digest=$(jq -r 'select(.digest != null) | .digest' /tmp/delete.preview.jsonl)
confirm=$(jq -r 'select(.digest != null) | .target.id' /tmp/delete.preview.jsonl) # single: record ID; batch of 2+: row count
hubspot objects search --type contacts --filter "!email" \
| jq -c '{id}' \
| hubspot objects delete --type contacts --digest "$digest" --confirm "$confirm"
既知の制約
objects deleteはユーザー OAuth (ブラウザログイン、オブジェクトの書き込みスコープ付き) とサービスキーの両方で動作します。403 は、アクティブなトークンにその書き込みスコープが欠けていることを意味します。例外は--gdprによる完全削除で、これはサービスキー (HUBSPOT_ACCESS_TOKEN) を必要とします — GDPR エンドポイントはユーザー OAuth トークンを受け付けません。objects update/merge/upsertもどちらのトークンタイプも受け付けます。一部の破壊的操作 (例:associationsの create/delete/batch/labels/limits、schemas delete) はサービスキー専用のままで、サーバー側で強制されるため、HUBSPOT_SKIP_AUTH_CHECKを使ってもユーザートークンでそれらを通過することはできません。hubspot owners listは CRM ユーザーを返します。teamsオブジェクトは存在しません。チームレベルの操作には、クライアント側でhubspot_owner_idによりグループ化してください。hubspot segmentsは CRM リスト (Lists API) を提供します:list、get、create、update(メタデータ)、update-filters、delete、restore、およびmembers-list/members-add/members-remove。hubspot sequencesは Sales Hub シーケンスへの読み取り専用アクセスを提供します:list --user-id <id>(ページネーションあり、--nameフィルター)、get <id> --user-id <id>(ステップ + 設定)、およびenrollments <contact_id>(あるコンタクトの登録履歴)。シーケンスは製品 API のサーフェス (Sales Hub Professional 以上、automation.sequences.readスコープ) であり、CRM オブジェクトタイプではありません —objects list --type sequencesは動作せず、create/update/delete/enroll もありません。- このセクションの保守: コマンドサーフェスは増え続けます — これを書いた時点で存在しなかったからといって、API が存在しないと決めつけないでください。新しいコマンドファミリーが追加されたら (
CHANGELOG.mdを参照)、これらの制約を見直してください。今日存在するものについてはhubspot --helpが権威です。