Apps Script で HubSpot Sheets を自動同期 — 1.3 万件を 4 分で
最終更新日: / 公開日:
最終更新日: / 公開日:
HubSpot のデータを Google Sheets に自動で流し込み、Looker Studio や Pivot で自由に分析する。これを Zapier や Make.com に月額 $30 払わず、Apps Script だけで完結させる方法を、私たち humbulls がある業務用美容機器メーカーで実装した約 1,000 行のコードをベースに公開します。Pillar 記事「HubSpot Starter で Professional 級ダッシュボード」のシリーズ #2 (本記事末尾にナビ)、データ層を担う実装ハウツーです。
1. なぜ Apps Script で Sheets 同期が必要か — Zapier 月額 $30 を削る
HubSpot ↔ Sheets 連携は「お金で買う」か「Apps Script で作る」かの二択。Claude Code が後者の障壁をほぼゼロにしたので、月額を払う前に一度試す価値があります。
HubSpot 公式の Sheets 連携機能は Marketing Hub Professional 以上が前提で、Starter プランからは利用できません。代わりに使われるのが Zapier や Make.com (旧 Integromat) の 3rd-party 連携で、Contacts や Deals の同期だけでも月額 $20-30 (年間 $240-360) ほど追加でかかります。※ 本記事の円換算はすべて 1 ドル = 150 円 (2026 年 5 月時点) を基準に計算しています。
実務でこれをどう処理しているかというと、ほとんどの組織が 3 パターンに分かれます。Zapier に月額を払って小さな同期を量産するか、CSV エクスポートを毎週手動で回すか、内製エンジニアに依頼して REST API のスクリプトを書いてもらうか。Zapier は便利な反面、無料枠を超えるとレコード数の増加でコストが直線的に伸びます。手動エクスポートは毎週 30 分から 1 時間の作業時間を奪い、内製依頼はリードタイムが読めません。
私たち humbulls が選んだのは 4 つ目の道、つまり Apps Script + HubSpot REST API + Claude Code の組み合わせです。Apps Script は Google アカウントさえあれば完全無料、HubSpot REST API も Starter プランから全エンドポイントが叩けます。書くべきコードは Claude Code がほぼ全部出してくれるので、Apps Script 未経験者でも数時間で動く環境が組めます。実装した結果、約 1,300 件 (Companies + Contacts + Deals 合計) の同期が毎朝 4 分以内に完走し、ランニングコストは $0/月 に圧縮できました。
注意点がひとつ。Zapier や Make.com には監視 UI / リトライ機構 / 接続コネクタの作り置きという「運用しやすさ」の価値があります。営業現場の非エンジニアがフローを編集する想定なら Zapier 系を残す方が安全です。「マーケ担当 1 人が Claude Code でメンテする」体制が組めるならば、Apps Script に振った方が自由度とコスト両面で有利です。
🤖 AI で実行する — 使ったプロンプト / ワークフロー
目的: Zapier / Make.com / Apps Script の 3 構成を月額・運用負荷・自由度で比較 所要時間: 20 分 ツール: Claude (Opus 4.7) / Google Sheets
プロンプト:
あなたは BtoB SaaS のシステム統合アドバイザーです。HubSpot ↔ Google Sheets 同期を「Zapier」「Make.com」「Apps Script 自作」の 3 構成で比較し、以下の観点で表にしてください。
【比較観点】
- 月額コスト (1.3 万レコード / 月次同期前提)
- 初期構築工数 (人日換算)
- 運用負荷 (障害対応 / 接続切れ / リトライ)
- 自由度 (任意プロパティ追加 / 整形ロジック差し込み)
- メンテ担当に求めるスキル
【出力形式】 マークダウン表 + 各構成の推奨ケース 1 行
運用 Tips: - 同期対象が 5,000 件 / 月以上、かつ Claude Code 等の AI 補助が使える組織なら Apps Script が優位 - マーケ担当が非エンジニアで監視 UI が欲しいなら Zapier 継続も妥当
📚 参考文献
- HubSpot Private Apps Overview — HubSpot Developer, 取得日 2026-05
- Zapier — Pricing — Zapier 公式, 取得日 2026-05
- Apps Script Overview — Google Developer
2. 実装のキモ — Private App Token / Search API / Pagination
ここを押さえれば 9 割は動きます。Token の作り方、Search API の使い分け、Pagination の
paging.next.afterカーソル方式の 3 点をまとめます。
HubSpot REST API を Apps Script から叩く場合、押さえるべき技術ポイントは大きく 3 つです。Private App Token の発行、Search API と List API の使い分け、そして Search API (Pagination = ページ送り) の仕様。公式ドキュメントは英語で散らばっており、初見だと「どの API でどう Pagination するか」で 1 日溶けることが珍しくありません。

従来このセットアップは、エンジニアが公式ドキュメントを読み込み、Postman で叩いて挙動を確認し、Apps Script に移植する、という流れで半日から 1 日仕事でした。limit / offset ベースの API と paging.next.after ベースの API が混在しているので、最初の 1 本目で必ず一度はハマります。
私たち humbulls は Claude Code に「HubSpot の Contacts を Private App Token を使って 100 件ずつ Pagination で全件取得する Apps Script を書いて」と指示し、出てきたコードをそのまま採用しました。出力されたのが以下の骨格です:
const HUBSPOT_TOKEN = PropertiesService.getScriptProperties().getProperty('HUBSPOT_TOKEN');
const BASE_URL = 'https://api.hubapi.com';
function fetchAllContacts() {
const properties = ['email', 'firstname', 'lastname', 'company', 'createdate', 'lifecyclestage'];
let after = undefined;
const results = [];
do {
const payload = {
limit: 100,
properties: properties,
filterGroups: [{
filters: [{ propertyName: 'createdate', operator: 'GT', value: '2024-01-01' }]
}],
after: after
};
const res = UrlFetchApp.fetch(`${BASE_URL}/crm/v3/objects/contacts/search`, {
method: 'post',
contentType: 'application/json',
headers: { Authorization: `Bearer ${HUBSPOT_TOKEN}` },
payload: JSON.stringify(payload),
muteHttpExceptions: true
});
const body = JSON.parse(res.getContentText());
results.push(...body.results);
after = body.paging?.next?.after;
} while (after);
return results;
}
ポイントは 3 つ。Private App Token は PropertiesService に格納してコード本体に書かないこと (= Apps Script のスクリプトプロパティ機能を使う)。Search API は POST で叩き、filterGroups で条件を指定できるので「過去 1 年以内に作成された Contacts のみ」のような絞り込みが可能。Pagination は paging.next.after をループ条件にすれば、件数を知らなくても全件取得できます。
注意点がひとつ。Search API には「同一 query で 10,000 件まで」という上限があります。1 万件を超える同期が必要なら createdate でレンジを切って複数 query に分割するか、List API (/crm/v3/objects/contacts) に切り替える設計が必要です。
🤖 AI で実行する — 使ったプロンプト / ワークフロー
目的: HubSpot 任意オブジェクトの全件取得コードを Apps Script で生成 所要時間: 10 分 ツール: Claude Code
プロンプト:
HubSpot の {objectType} を Private App Token で全件取得する Apps Script を書いてください。
【条件】
- Search API (POST /crm/v3/objects/{objectType}/search) を使用
- Pagination は paging.next.after カーソル方式
- limit は 100、properties は [list] を取得
- Token は PropertiesService.getScriptProperties() から読み込み
- レスポンスを 2 次元配列に整形し、setValues() で Sheet に書き込み
【オブジェクト】 contacts / companies / deals のいずれか
【properties】 例: email, firstname, lastname, company, createdate, lifecyclestage
運用 Tips:
- 1 万件を超える対象は createdate で四半期ごとに分割 query
- muteHttpExceptions: true を必ず付けて、4xx エラー時もログ出力させる
📚 参考文献
- HubSpot CRM Search API — HubSpot Developer
- HubSpot Private Apps — Token 作成手順
- Apps Script PropertiesService — Google Developer
3. ハマりポイント — Date 自動変換 / Number coerce / Pagination 漏れ
動いた後に必ず踏む 3 つの罠。コードが出てから半日溶かしたくないなら、最初から対策を入れておくと安全です。
HubSpot REST API は型情報を文字列で返してきますが、Google Sheets は 書き込み時に自動で型推論 をします。これが原因で発生するハマりが 3 種類あり、対策を入れていないと Looker Studio 側で「日付が動かない」「集計が出ない」という症状に化けます。
従来の対処は、運用開始後に Sheet を眺めて「あれ、なんでこの列だけ計算できないんだ」と気付き、Apps Script の setValues() の前後にデバッグログを仕込んで原因を特定する、という後追いパターンでした。半日から 1 日溶ける割に再発しやすく、辛い作業です。
私たち humbulls の運用では、最初から以下の 3 対策をコードに組み込んでいます:
| ハマり | 症状 | 対策 |
|---|---|---|
| Date 自動変換 | 2026-04 等の文字列を勝手に Date 化 → toString() で Mon Apr 01 2026... に化ける |
range.setNumberFormat('@') を setValues の 前 に呼び、列全体を Plain Text 化 |
| Number coerce | HubSpot は金額を文字列で返す ("3500000") → Looker Studio で SUM 不可 |
Apps Script 側で Number(value) 変換してから書き込み |
| Pagination 漏れ | paging.next.after のループ条件を body.paging?.next に書くと undefined チェックが甘く 1 ページで止まる |
while (after) で明示的にカーソル変数を判定 |
コードに落とすとこうなります:
function writeToSheet(sheet, headers, rows) {
sheet.clear();
const range = sheet.getRange(1, 1, rows.length + 1, headers.length);
range.setNumberFormat('@'); // ← setValues の前に Plain Text 強制
const numericColumns = ['amount', 'days_in_stage'];
const normalized = rows.map(row => row.map((cell, i) => {
return numericColumns.includes(headers[i]) ? Number(cell) || 0 : cell;
}));
sheet.getRange(1, 1, 1, headers.length).setValues([headers]);
sheet.getRange(2, 1, normalized.length, headers.length).setValues(normalized);
}
特に setNumberFormat('@') の呼び出しタイミング がポイントで、setValues() の 後 に呼ぶと既に型推論された後なので意味がありません。順序を間違うとデバッグの沼にハマります。

注意点がひとつ。Plain Text 化すると Sheet の関数 (=SUM(...)) が動かなくなるように見えますが、Looker Studio 側では「Number 型」として読ませる設定 (= データソース定義) で問題ありません。Sheet 内で =SUM を多用したいなら数値列だけ setNumberFormat('0') に切り替えてください。
🤖 AI で実行する — 使ったプロンプト / ワークフロー
目的: Apps Script の setValues() ハマり対策コードを自社プロパティに合わせて生成 所要時間: 15 分 ツール: Claude Code
プロンプト:
Apps Script で HubSpot REST API のレスポンスを Google Sheets に書き込むコードを書きます。以下の 3 ハマりに対応してください。
【ハマり 1】 Date 自動変換 → setNumberFormat('@') を setValues の前に呼ぶ
【ハマり 2】 Number coerce → 指定列のみ Number() 変換
【ハマり 3】 Pagination 漏れ → paging.next.after の undefined ループ判定
【入力】
- headers: ["email", "amount", "createdate", "stage"]
- numericColumns: ["amount", "days_in_stage"]
- 書き込み先 Sheet 名: "deals_raw"
【出力】 writeToSheet(sheet, headers, rows) 関数 1 つ + テストデータでの呼び出し例
運用 Tips:
- numericColumns は組織ごとにカスタム化、社員が後から触れるよう先頭に const で宣言
- range.clear() ではなく sheet.clearContents() を使うと書式設定だけ残せて再運用が楽
📚 参考文献
- Apps Script Range#setNumberFormat — Google Developer
- HubSpot CRM Properties — Numbers — Number 型は文字列で返ってくる仕様
4. 毎朝 1 回の Trigger 設定 — 4 分で 1.3 万件
同期コードが動いたら次は自動化。Apps Script Trigger を 1 つ置くだけで「人がボタンを押す」運用から脱却できます。
Apps Script には「6 分制限」と呼ばれる実行時間の上限があり、1 関数の処理が 6 分を超えると強制終了されます。これを意識せずに同期コードを書くと、Contacts と Deals を 1 関数にまとめた瞬間にタイムアウトで止まり、Sheet が中途半端な状態で残ります。
従来の回避策は、エンジニアが「処理を分割しろ」とアドバイスして、Companies / Contacts / Deals を別関数に切り、Trigger を 3 本設定する、という手順でした。それでも 1 回の同期が 5 分台で完走するなら良いのですが、データが増えると 6 分を超えてリトライが必要になる、という運用負債を抱えがちです。
私たち humbulls の運用では、以下の設計で 約 4 分で 1.3 万件 (Companies 約 5,000 + Contacts 約 6,000 + Deals 約 2,000) を完走 させています:
| 関数 | 対象 | 1 回の処理時間 |
|---|---|---|
syncCompaniesToSheet |
Companies 全件 | 約 1 分 20 秒 |
syncContactsToSheet |
Contacts 全件 | 約 1 分 30 秒 |
syncDealsToSheet |
Deals 全件 | 約 1 分 10 秒 |
syncOrchestrator |
上記 3 関数を順次呼び出し | 約 4 分 |
設定の核は syncOrchestrator を 1 本だけ Trigger に登録すること。これで「毎朝 6:00 に 1 回だけ実行」というシンプルな運用になります。
function syncOrchestrator() {
const ss = SpreadsheetApp.openById('YOUR_SHEET_ID');
syncCompaniesToSheet(ss.getSheetByName('companies_raw'));
syncContactsToSheet(ss.getSheetByName('contacts_raw'));
syncDealsToSheet(ss.getSheetByName('deals_raw'));
Logger.log(`Sync completed at ${new Date().toISOString()}`);
}
Trigger の登録は Apps Script Editor の左メニュー「トリガー」から「トリガーを追加」を選び、関数 syncOrchestrator / イベントソース「時間主導型」/ タイプ「日次」/ 時刻「6:00-7:00」を指定するだけです。

注意点がひとつ。Trigger 実行のログは「実行数」画面でしか追えないので、私たちは Logger.log を成功 / 失敗判定と一緒に Sheet にも追記しています (= 別 Sheet タブ sync_log)。Looker Studio で「最終同期日時」を表示しておくと、止まったときに気付きやすくなります。
🤖 AI で実行する — 使ったプロンプト / ワークフロー
目的: 6 分制限を回避するオーケストレーター関数 + ログ Sheet への記録を生成 所要時間: 20 分 ツール: Claude Code
プロンプト:
Apps Script で複数の同期関数 (例: syncCompaniesToSheet, syncContactsToSheet, syncDealsToSheet) を順次実行するオーケストレーター関数を書いてください。
【要件】
- 1 関数の実行時間を計測し、合計が 5 分を超えたら次回 Trigger にスキップ
- 成功 / 失敗 / 件数を Sheet 「sync_log」に追記 (timestamp / function_name / status / count)
- 失敗時は HubSpot からの Error Response も sync_log に残す
【出力】 syncOrchestrator() + logSync() の 2 関数
運用 Tips:
- 同期失敗を Slack 通知したい場合は UrlFetchApp.fetch(SLACK_WEBHOOK, ...) を logSync() に追加
- 6 分上限が厳しい場合は Deals を半分に分割 (createdate で年で切る)
📚 参考文献
- Apps Script Triggers — Google Developer
- Apps Script Quotas — 6 分制限の公式記述
5. 実運用の落とし穴 + 改善余地
動かして 2-3 ヶ月で必ず出てくる「ちょっとした不便」を、最初から対策しておきましょう。
実運用に入って 1 ヶ月もすると「Owner ID が数字で出てくるけど誰?」「Pipeline Stage の ID って日本語にならないの?」というフィードバックが営業現場から出てきます。技術的には「ID の方が機械処理しやすい」のですが、Looker Studio で営業に共有するダッシュボードに 12345678 と出ても誰も読めません。
従来の対応は、人力で Looker Studio 側の「計算フィールド」に CASE WHEN owner_id = "12345678" THEN "山田" ... と書き並べる方法でした。Owner が 5 名なら耐えられますが、20 名を超えると保守不能になります。
私たち humbulls は Sheet 内マッピング テーブル + VLOOKUP で解決しました。Apps Script で owners_master / pipelines_master のシートを別途同期し、Looker Studio 側で 1 度だけ Join 設定する、という構造です:
| マッピング Sheet | データ元 | 同期頻度 |
|---|---|---|
owners_master |
HubSpot Owners API (/crm/v3/owners) |
週 1 回 |
pipelines_master |
HubSpot Pipelines API (/crm/v3/pipelines/deals) |
週 1 回 |
stages_master |
Pipelines API のレスポンス内 | 週 1 回 |
これらのマッピングは更新頻度が低い (= 人事異動や Pipeline 改定でしか変わらない) ので、毎日叩く必要はなく、週 1 回 Trigger で十分です。
改善余地として残しているのが「増分同期」と「リトライ」です。今は毎朝 全件 を上書きする「全件方式」ですが、データが 5 万件を超えてくると毎日 4 分では完走しなくなるので、hs_lastmodifieddate を使った増分同期に切り替える必要が出てきます。サブ記事 #6 (Kintone と HubSpot の並行運用) で詳細を解説しています。
注意点がひとつ。マッピング Sheet を作ると Apps Script のメンテ対象が増えるので、運用ルールをドキュメント化しておくと安全です。例えば「Owner が増えた / 減った時は週次 Trigger を待つか、手動で syncOwnersMaster を実行する」のようなオペレーション手順書を 1 ページで残しておくと、引き継ぎが楽になります。
🤖 AI で実行する — 使ったプロンプト / ワークフロー
目的: Owner / Pipeline / Stage マッピング Sheet の同期コードを生成 所要時間: 25 分 ツール: Claude Code
プロンプト:
HubSpot の Owners / Pipelines / Stages を Google Sheets にマッピングテーブルとして同期する Apps Script を書いてください。
【要件】
- Owners: /crm/v3/owners から id, firstName, lastName, email を取得 → owners_master シート
- Pipelines: /crm/v3/pipelines/deals から id, label を取得 → pipelines_master シート
- Stages: 上記 Pipelines レスポンス内の stages を平坦化 → stages_master シート (pipeline_id, stage_id, label, displayOrder)
【出力】 syncOwnersMaster() / syncPipelinesAndStagesMaster() の 2 関数 + 週次 Trigger 用 orchestrator
運用 Tips: - Looker Studio 側ではマッピングシートを「データソース」として別途登録 → Blend で Join - Owner email も取得しておくと、後から Slack 通知連携に流用できる
📚 参考文献
- HubSpot Owners API — HubSpot Developer
- HubSpot Pipelines API — HubSpot Developer
- Looker Studio Data Blending — Google Support
サブ記事ナビゲーション
Pillar 記事「HubSpot Starter で Professional 級ダッシュボード」を起点に、シリーズ全 6 本でデータ層から可視化層までを解説しています。
| # | タイトル | 役割 |
|---|---|---|
| #1 (Pillar) | HubSpot Starter を Claude Code で限界突破!上位プランでしかできなかったレポートダッシュボードを再現 | 全体設計 |
| #2 (本記事) | Apps Script で HubSpot ↔ Google Sheets を自動同期 | データ層 |
| #3 | Looker Studio で HubSpot ダッシュボードを 30 分構築 | 可視化層 |
| #4 | Stage History を Apps Script で日次 Snapshot | Professional 専用機能の代替 |
| #5 | 月次コホート × 経過日数 × 勝率 — AI で 1 時間設計 | 分析フレームワーク |
| #6 | Kintone と HubSpot の並行運用 — 増分同期で破綻させない | 既存 CRM 統合 |
humbulls の Growth Partner サービス では、こうしたデータ層の設計から Looker Studio の構築まで伴走しています。
実装テンプレを一括でダウンロードしたい方は、BtoB マーケ AI 活用ガイド もご活用ください。