🧑‍💻 開発者向けPython・Claude Code・EC/CRM

Claude Code×HubSpot・Salesforce API【2026】営業データの分析とCRM運用をAIエージェントで

「この商談、最後に連絡したのはいつ?」「金額が空欄の商談が多くて、パイプラインの数字が信用できない」「同じ会社の担当者が3件登録されている」——CRMは入れた瞬間から、データをきれいに保ち、動かし続ける運用が課題になります。この記事では、Claude Code(ターミナルで動くAnthropicのコーディングエージェント)をHubSpot CRM APIとSalesforce(sf CLI・SOQL)につなぎ、停滞商談の抽出から週次サマリー、重複の候補出し、活動記録の下書きまでを任せる構成を、コードと設定ファイル付きで解説します。CRMの更新は人が承認したものだけ、監査ログ付きで行う前提です。

🔬
調査・編集:AIエージェント研究所 編集部
運営=広告運用・生成AIエージェント導入支援の現役コンサル。このサイト自体も、AIエージェントと一緒に作り・公開・運用しています。

API仕様・料金は執筆時点(2026年9月)の情報です。HubSpot・Salesforceともに開発者向けの仕組みの更新が続いているため、利用前に必ず各公式ドキュメントをご確認ください。→ 運営者・編集方針について

公開:2026年9月26日/最終更新:2026年9月26日
この記事の結論(要点まとめ)
  • HubSpotはプライベートアプリのトークンでCRM API v3(/crm/v3/objects/dealsなど)を読む。読み取り用と書き込み用のトークンを分ける
  • Salesforceはsf CLIでログインし、sf data queryでSOQLを実行。認証をCLIに任せられるのでClaude Codeから扱いやすい。まずSandboxで
  • 「停滞」「必須項目」の定義はCLAUDE.mdと設定ファイルに書き、判定はスクリプトで決定的に。Claude Codeは次アクション案や活動記録の下書きを担当
  • 更新・削除・統合はpermissionsでdeny、承認済みの登録だけask+監査ログ。最後に通すのは人

何ができるか:営業・CRM運用の5つのユースケース

営業部門でAIエージェントが効くのは、CRMに溜まったデータを読み、「次に何をすべきか」を整理する作業です。レコードを書き換える作業は、最後まで人の承認を通します。

ユースケース使うデータClaude Codeの役割人の役割
① 停滞している商談の抽出と次アクション案商談(ステージ・最終活動日・金額)担当者別の整理、次アクション案アクションの実行
② パイプラインの週次サマリー商談(ステージ別件数・金額)前週比の整理、所見営業会議での判断
③ 重複コンタクトの検出と統合“候補”リストコンタクト(メール・電話・氏名・会社)候補の妥当性チェック、統合方針の案CRMの標準機能で統合
④ 商談メモからの活動記録の下書き手元の商談メモ要点・次アクションの構造化確認・承認
⑤ データ品質チェック(必須項目の空欄)商談・コンタクト担当者別の集計、入力依頼文の下書き入力・修正
CRMHubSpotSalesforce① 取得トークン/sf CLIスクリプト② 判定停滞・空欄・重複スクリプト③ 整理・下書き次アクション案等Claude Code読むデータ候補下書き④ 承認人が確認してOK人登録承認済みだけスクリプト監査ログ承認記録承認した分だけ書き込む
① 読み取り用トークン/sf CLIでCRMを読み、② 停滞・空欄・重複を定義どおりに抽出するまではスクリプト。③ Claude Codeが次アクション案・活動記録・依頼文を下書きし、④ 人が承認したものだけを登録スクリプトがCRMへ書き込んで監査ログに残します。
助手アイ
助手アイ最近はCRMそのものにもAI機能がありますよね。わざわざClaude Codeでつなぐ意味ってあるんですか?
エージェント博士
エージェント博士もちろん標準機能で足りるならそれが一番じゃ。Claude Codeの出番は、自社独自の「停滞」の定義で判定したい、CRMの外にあるメモや他のデータと合わせたい、判定ロジックをコードとしてレビューしたい——そんなときじゃな。スクリプトはリポジトリに残るから、なぜその商談が「停滞」と判定されたかを後から説明できるのも強みじゃぞ

SalesforceはAIエージェント機能「Agentforce」を、HubSpotはAI機能群「Breeze」を発表しており(いずれも2024年)、CRMベンダー自身もエージェント化を進めています。自前の連携は、それらと競うものではなく、自社ルールでの点検と下書きを、手元で透明に回すための選択肢と考えるのがよいでしょう。APIの基本から確認したい方は、初心者向けのIT基礎シリーズ第4章「API・JSON入門」をどうぞ。

準備と認証:プライベートアプリ・sf CLI・Sandbox

HubSpot:プライベートアプリを「読み取り用」と「書き込み用」に分ける

HubSpotでは、設定画面からプライベートアプリを作成し、必要なスコープを選ぶとアクセストークンが発行されます。リクエストにはAuthorization: Bearer <トークン>ヘッダーを付けます。この記事ではアプリを2つ作ります。

アプリスコープ(例)使う場所
読み取り用crm.objects.deals.read、crm.objects.contacts.read、crm.objects.owners.read取得・分析スクリプト(Claude Codeが日常的に実行)
書き込み用メモ(活動記録)の登録に必要な最小限の書き込みスコープ承認済みの下書きを登録するスクリプトだけ(実行は毎回確認)

トークンを分けておけば、日常的に動く分析スクリプトが誤って書き込むことはAPI側で不可能になります。必要なスコープの正確な名前は、公式ドキュメントで確認してください。

HubSpotの開発者向けの仕組みは刷新が進んでいます HubSpotは開発者プラットフォームの刷新を進めており、プライベートアプリの作成手順や名称、推奨される認証方式が変わっている場合があります(執筆時点)。また、検証用に開発者テストアカウントや、プランによってはサンドボックスが使えます。本番のポータルに接続する前に、検証環境で動作を確認してください。

Salesforce:sf CLIでSandboxにログイン

Salesforceは、公式のSalesforce CLI(sf)を使うのが扱いやすい方法です。ブラウザでログインすると認証情報をCLIが管理してくれるため、スクリプトやClaude Codeがトークンを直接扱わずに済みます。まずは本番と切り離されたSandboxにログインします。

ターミナル(人が実行)Bash
npm install -g @salesforce/cli
# Sandbox にブラウザでログイン(本番は login.salesforce.com / My Domain のURL)
sf org login web --alias sbx --instance-url https://test.salesforce.com
sf org list
  • ログインは人が行う:Claude Codeにはログイン済みのエイリアス(sbx)でクエリだけを実行させます
  • 見える範囲=ログインしたユーザーの権限:Salesforceは共有ルールや項目レベルセキュリティで見えるデータが決まります。分析用には参照権限だけの権限セットを持つユーザーを用意するのが理想です
  • 定期実行では専用の連携ユーザー:無人で動かす場合は、接続アプリ(または外部クライアントアプリ)とJWTなどのOAuthフローで、API専用の統合ユーザーを使う構成を検討します
  • エディションによってはAPIが利用できない場合があります。ご利用の契約で確認してください

Pythonから直接REST APIを呼びたい場合は、コミュニティ製のライブラリsimple-salesforceもよく使われます。HubSpotには公式のPythonクライアントhubspot-api-clientがありますが、この記事では挙動が追いやすいrequestsで書きます(Python 3.11以上を想定)。

プロジェクト構成

営業データ用のリポジトリを1つ作り、そこでClaude Codeを起動します。「読む」「下書く」「承認して書く」の置き場をディレクトリで分けているのがポイントです。

プロジェクト構成text
sales-ops/
├── CLAUDE.md                 # 項目定義・ステージ定義・ルール
├── .env                      # HubSpotトークン(Git・Claudeの対象外)
├── .claude/settings.json     # permissions / hooks
├── .claude/skills/weekly-pipeline/SKILL.md
├── config/stage_rules.json   # ステージごとの「停滞」日数・必須項目
├── scripts/hubspot/          # client.py, fetch_deals.py, fetch_contacts.py
├── scripts/salesforce/       # sf_query.py(sf CLI のラッパー)
├── scripts/analyze/          # stale_deals.py, find_duplicates.py, check_required.py
├── scripts/write/            # register_activity.py(承認済みの下書きだけを登録。ask)
├── reports/                  # 取得・分析結果(Git管理外)
├── notes/inbox/              # 商談メモ(人が置く)
├── drafts/activities/        # 活動記録の下書き(Claudeが書く)
├── approved/                 # 人が確認して移した下書き(Claudeは編集不可)
└── logs/audit.jsonl          # 書き込みの監査ログ

approved/は人だけが置けるディレクトリです(Claude Codeには編集を禁止します)。「Claudeが下書きし、人がapprovedに移したものだけが登録される」という流れを、ファイルの置き場所で表現しています。

HubSpot CRM API v3:商談・コンタクトの取得と分析

クライアント:一覧のページングと検索

一覧はGET /crm/v3/objects/{object}で取得し、レスポンスのpaging.next.afterを次のリクエストに渡して全件をたどります。条件で絞るときはPOST /crm/v3/objects/{object}/searchを使います。

scripts/hubspot/client.pyPython
"""HubSpot CRM API v3 の薄いクライアント(読み取り用トークン)"""
import os
import time

import requests
from dotenv import load_dotenv

load_dotenv()
BASE = "https://api.hubapi.com"
session = requests.Session()
session.headers["Authorization"] = f"Bearer {os.environ['HUBSPOT_READ_TOKEN']}"


def _request(method: str, path: str, **kwargs) -> dict:
    for attempt in range(5):
        resp = session.request(method, BASE + path, timeout=30, **kwargs)
        if resp.status_code == 429 or resp.status_code >= 500:
            time.sleep(2 ** attempt)  # レート制限・一時エラーは待って再試行
            continue
        resp.raise_for_status()
        return resp.json()
    raise RuntimeError(f"HubSpot API: retries exhausted ({path})")


def list_objects(obj: str, properties: list[str]):
    """GET /crm/v3/objects/{obj} を paging.next.after でたどって全件返す"""
    params = {"limit": 100, "properties": ",".join(properties)}
    while True:
        data = _request("GET", f"/crm/v3/objects/{obj}", params=params)
        yield from data["results"]
        after = data.get("paging", {}).get("next", {}).get("after")
        if not after:
            return
        params["after"] = after


def search(obj: str, filters: list[dict], properties: list[str]):
    """POST /crm/v3/objects/{obj}/search(1クエリで取れる件数に上限があるので範囲を絞って使う)"""
    body = {"filterGroups": [{"filters": filters}], "properties": properties, "limit": 100}
    while True:
        data = _request("POST", f"/crm/v3/objects/{obj}/search", json=body)
        yield from data["results"]
        after = data.get("paging", {}).get("next", {}).get("after")
        if not after:
            return
        body["after"] = after
        time.sleep(0.3)  # 検索APIは通常のAPIより制限が厳しい

検索APIは便利ですが、1回の検索でたどれる件数に上限があり、レート制限も通常のAPIより厳しいという特徴があります(具体的な値は公式で確認)。全件を定期的に取るなら一覧API、差分だけ取るなら検索API(例:hs_lastmodifieddateが1週間以内)と使い分けるのが実践的です。

商談とパイプライン定義を取得する

scripts/hubspot/fetch_deals.pyPython
"""商談とパイプライン定義を取得して reports/ に保存する"""
import csv
import json

from client import _request, list_objects

PROPS = ["dealname", "amount", "dealstage", "pipeline", "closedate",
         "hubspot_owner_id", "notes_last_updated", "hs_lastmodifieddate"]

# ステージIDとラベル、クローズ済みかどうか(metadata.isClosed)を保存
pipelines = _request("GET", "/crm/v3/pipelines/deals")["results"]
stages = {s["id"]: {"label": s["label"], "closed": s["metadata"].get("isClosed") == "true"}
          for p in pipelines for s in p["stages"]}
json.dump(stages, open("reports/stages.json", "w", encoding="utf-8"), ensure_ascii=False, indent=2)

owners = {o["id"]: f"{o.get('lastName', '')} {o.get('firstName', '')}".strip()
          for o in _request("GET", "/crm/v3/owners", params={"limit": 500})["results"]}

with open("reports/deals.csv", "w", newline="", encoding="utf-8") as f:
    w = csv.writer(f)
    w.writerow(["id", *PROPS, "stage_label", "is_closed", "owner_name"])
    for d in list_objects("deals", PROPS):
        p = d["properties"]
        stage = stages.get(p.get("dealstage"), {"label": "(不明)", "closed": False})
        w.writerow([d["id"], *[p.get(k) or "" for k in PROPS],
                    stage["label"], stage["closed"], owners.get(p.get("hubspot_owner_id"), "")])

ここでパイプライン定義も一緒に保存しているのが大事な点です。HubSpotのdealstageにはappointmentscheduledのような内部IDが入っていて、画面の表示名とは異なります。カスタムのステージでは数字のIDになることもあります。IDとラベル、クローズ済みかどうか(metadata.isClosed)の対応表をreports/stages.jsonに残しておけば、Claude Codeがレポートを書くときに表示名で書けます。

停滞商談と必須項目の空欄を抽出する

「停滞」の定義は、営業部門と合意して設定ファイルに書きます。ステージによって適切な間隔は違うので、ステージごとに日数を持たせます。

config/stage_rules.jsonJSON
{
  "_comment": "キーは HubSpot のステージID(reports/stages.json で確認)。値は営業部門と合意した定義",
  "appointmentscheduled": { "stale_days": 7,  "required": ["amount", "closedate", "hubspot_owner_id"] },
  "qualifiedtobuy":       { "stale_days": 10, "required": ["amount", "closedate", "hubspot_owner_id"] },
  "presentationscheduled":{ "stale_days": 14, "required": ["amount", "closedate", "hubspot_owner_id"] },
  "contractsent":         { "stale_days": 7,  "required": ["amount", "closedate", "hubspot_owner_id"] }
}
scripts/analyze/stale_deals.pyPython
"""停滞商談と必須項目の空欄を、config/stage_rules.json の定義で抽出する"""
import csv
import json
from datetime import datetime, timezone

rules = json.load(open("config/stage_rules.json", encoding="utf-8"))
now = datetime.now(timezone.utc)


def days_since(value: str) -> int | None:
    if not value:
        return None
    return (now - datetime.fromisoformat(value.replace("Z", "+00:00"))).days


stale, missing = [], []
for d in csv.DictReader(open("reports/deals.csv", encoding="utf-8")):
    rule = rules.get(d["dealstage"])
    if d["is_closed"] == "True" or rule is None:
        continue
    # 最終活動日(notes_last_updated)を優先。なければ最終更新日で代用し、その旨を残す
    basis = "last_activity" if d["notes_last_updated"] else "last_modified"
    idle = days_since(d["notes_last_updated"] or d["hs_lastmodifieddate"])
    if idle is not None and idle >= rule["stale_days"]:
        stale.append([d["id"], d["dealname"], d["stage_label"], d["owner_name"], d["amount"], idle, basis])
    empty = [k for k in rule["required"] if not d.get(k)]
    if empty:
        missing.append([d["id"], d["dealname"], d["stage_label"], d["owner_name"], "|".join(empty)])

with open("reports/stale_deals.csv", "w", newline="", encoding="utf-8") as f:
    csv.writer(f).writerows([["id", "deal", "stage", "owner", "amount", "idle_days", "basis"], *stale])
with open("reports/missing_fields.csv", "w", newline="", encoding="utf-8") as f:
    csv.writer(f).writerows([["id", "deal", "stage", "owner", "missing"], *missing])
print(f"stale={len(stale)} missing={len(missing)}")

判定の基準日は最終活動日(notes_last_updated:メール・通話・会議などの記録日)を優先し、空なら最終更新日で代用して、その旨をbasis列に残します。最終更新日は自動化ルールによる更新でも変わるため、「最近更新されている=動いている」とは限らないからです。こうした判断の理由を列として残すと、Claude Codeがレポートに注記を付けられ、読む人の誤解も防げます。

重複コンタクトの“候補”を出す

コンタクトはfetch_deals.pyと同じ要領で、email, phone, firstname, lastname, company, createdateをreports/contacts.csvに保存しておきます(fetch_contacts.py)。重複の判定は、メール(小文字化)・電話番号(数字だけの下10桁)・氏名+会社名の3つの観点で行います。

scripts/analyze/find_duplicates.pyPython
"""重複コンタクトの「候補」を出す。メールはマスクして出力し、統合はしない"""
import csv
import re
from collections import defaultdict


def norm_email(e: str) -> str:
    return (e or "").strip().lower()


def norm_phone(p: str) -> str:
    digits = re.sub(r"\D", "", p or "")
    return digits[-10:] if len(digits) >= 10 else ""  # 国番号・ハイフンの違いを吸収


def mask(e: str) -> str:
    local, _, domain = e.partition("@")
    return f"{local[:1]}***@{domain}" if domain else ""


groups = defaultdict(set)
# fetch_contacts.py が保存した id, email, phone, firstname, lastname, company, createdate の列
contacts = {c["id"]: c for c in csv.DictReader(open("reports/contacts.csv", encoding="utf-8"))}
for cid, c in contacts.items():
    if norm_email(c["email"]):
        groups[("email", norm_email(c["email"]))].add(cid)
    if norm_phone(c["phone"]):
        groups[("phone", norm_phone(c["phone"]))].add(cid)
    name = (c["lastname"] + c["firstname"]).replace(" ", "").replace(" ", "")
    if name and c["company"]:
        groups[("name+company", name + "/" + c["company"].strip())].add(cid)

with open("reports/duplicate_candidates.csv", "w", newline="", encoding="utf-8") as f:
    w = csv.writer(f)
    w.writerow(["reason", "contact_ids", "masked_emails", "created_dates"])
    for (reason, _), ids in groups.items():
        if len(ids) > 1:
            ids = sorted(ids)
            w.writerow([reason, "|".join(ids),
                        "|".join(mask(norm_email(contacts[i]["email"])) for i in ids),
                        "|".join(contacts[i]["createdate"][:10] for i in ids)])

出力のメールアドレスはt***@example.comのようにマスクしています。重複の妥当性を判断するのにフルのアドレスは不要で、Claude Codeが読むファイルから個人情報を減らせるからです。統合(merge)はAPIでも可能ですが、どちらの値を残すか、関連する商談や活動履歴をどう扱うかは業務判断です。この記事では統合をdenyにし、候補リストと方針の案までをClaude Codeに任せます。HubSpotにもSalesforceにも重複管理の標準機能があるので、実際の統合はそちらで担当者が行います。

Salesforce:sf CLIとSOQLで同じことをする

Salesforceでは、同じ分析をSOQL(Salesforceのデータを取得するためのSQLに似た言語)で書けます。sf data queryに--result-format csvを付ければそのままCSVで保存できるので、単純な抽出ならスクリプトすら不要です。

ターミナルBash
# 停滞している商談(オープンで、14日以上活動がない)
sf data query --target-org sbx --result-format csv --query "
  SELECT Id, Name, StageName, Amount, CloseDate, Owner.Name, LastActivityDate
  FROM Opportunity
  WHERE IsClosed = false
    AND (LastActivityDate = null OR LastActivityDate < LAST_N_DAYS:14)
  ORDER BY Amount DESC NULLS LAST" > reports/sf_stale_opps.csv

# パイプラインの週次サマリー(ステージ別の件数と金額)
sf data query --target-org sbx --query "
  SELECT StageName, COUNT(Id) cnt, SUM(Amount) total
  FROM Opportunity WHERE IsClosed = false GROUP BY StageName"

# 必須項目の空欄チェック
sf data query --target-org sbx --result-format csv --query "
  SELECT Id, Name, StageName, Owner.Name FROM Opportunity
  WHERE IsClosed = false AND (Amount = null OR CloseDate = null OR NextStep = null)" > reports/sf_missing.csv

# 同じメールアドレスのコンタクト(重複候補)
sf data query --target-org sbx --query "
  SELECT Email, COUNT(Id) c FROM Contact
  WHERE Email != null GROUP BY Email HAVING COUNT(Id) > 1"

SOQLにはLAST_N_DAYS:14のような日付リテラルがあり、「14日以上活動がない」を簡潔に書けます。LastActivityDateは活動(ToDo・行動)の記録から自動で決まる項目です。NextStep(次のステップ)は標準項目ですが、組織によってはカスタム項目で管理していることもあるので、CLAUDE.mdの項目定義に合わせて書き換えます。

定型のクエリはqueries/*.soqlとしてファイルに置き、次のようなラッパーで実行すると、クエリ自体をGitでレビューできます。

scripts/salesforce/sf_query.pyPython
"""sf CLI で SOQL を実行し、JSON の records を CSV に保存する(認証は sf CLI に任せる)"""
import csv
import json
import subprocess
import sys

ORG = "sbx"  # 本番に切り替えるときは人が明示的に変更する


def run_soql(soql: str) -> list[dict]:
    out = subprocess.run(
        ["sf", "data", "query", "--target-org", ORG, "--query", soql, "--json"],
        capture_output=True, text=True, check=True,
    )
    records = json.loads(out.stdout)["result"]["records"]
    for r in records:
        r.pop("attributes", None)
        owner = r.pop("Owner", None)  # リレーション項目はネストで返るので平らにする
        if owner:
            r["OwnerName"] = owner.get("Name")
    return records


if __name__ == "__main__":
    soql_file, out_csv = sys.argv[1], sys.argv[2]  # 例: queries/stale.soql reports/sf_stale.csv
    rows = run_soql(open(soql_file, encoding="utf-8").read())
    with open(out_csv, "w", newline="", encoding="utf-8") as f:
        if rows:
            w = csv.DictWriter(f, fieldnames=list(rows[0]))
            w.writeheader()
            w.writerows(rows)
    print(f"{len(rows)} records -> {out_csv}")

リレーション項目(Owner.Name)はJSONでネストして返るため、CSVにする前に平らにしています。件数が多い場合は、Bulk API系のコマンドやオプションの利用を検討してください(仕様は公式で確認)。Salesforceには1日あたりのAPIリクエスト数の上限があり、ほかの連携ツールと共有しています。定期実行の頻度は、組織全体の利用状況を見て決めましょう。

商談メモから活動記録の下書き、承認して登録

商談後のメモ(箇条書きでも音声の文字起こしでも)をnotes/inbox/に置き、Claude Codeに活動記録の下書きを作らせます。下書きはJSONで、CRMに登録する項目の形にそろえます。

drafts/activities/2026-09-25_acme.json(Claude Codeが作る下書き)JSON
{
  "deal_id": "1234567890",
  "timestamp": "2026-09-25T06:00:00Z",
  "body": "【打ち合わせ】先方の情シス部門と導入範囲を確認。\n・決裁は10月の定例会議\n・懸念点:既存システムとのデータ連携\n【次アクション】連携方式の資料を10/2までに送付(担当:営業)",
  "next_step": "連携方式の資料を送付",
  "next_step_due": "2026-10-02",
  "source": "notes/inbox/2026-09-25_acme.md"
}

人が内容を確認し、必要なら直してapproved/に移します。登録スクリプトはapproved/配下のファイルしか受け付けず、書き込み用トークンで登録したあと、誰が承認したかを監査ログに追記します。

scripts/write/register_activity.pyPython
"""approved/ に人が移した活動記録(JSON)だけを HubSpot のメモとして登録し、監査ログを残す"""
import json
import os
import sys
from datetime import datetime, timezone
from pathlib import Path

import requests
from dotenv import load_dotenv

load_dotenv()
draft_path, approver = Path(sys.argv[1]).resolve(), sys.argv[2]  # 例: approved/2026-09-25_acme.json yamada
if Path("approved").resolve() not in draft_path.parents:
    raise SystemExit("approved/ 配下のファイル(人が承認して移したもの)だけ登録できます")
draft = json.load(open(draft_path, encoding="utf-8"))

body = {
    "properties": {"hs_timestamp": draft["timestamp"], "hs_note_body": draft["body"]},
    "associations": [{
        "to": {"id": draft["deal_id"]},
        "types": [{"associationCategory": "HUBSPOT_DEFINED", "associationTypeId": 214}],  # メモ→商談(公式で確認)
    }],
}
resp = requests.post(
    "https://api.hubapi.com/crm/v3/objects/notes",
    headers={"Authorization": f"Bearer {os.environ['HUBSPOT_WRITE_TOKEN']}"},
    json=body, timeout=30,
)
resp.raise_for_status()

with open("logs/audit.jsonl", "a", encoding="utf-8") as log:
    log.write(json.dumps({
        "at": datetime.now(timezone.utc).isoformat(), "action": "create_note",
        "deal_id": draft["deal_id"], "note_id": resp.json()["id"],
        "file": draft_path.name, "approved_by": approver,
    }, ensure_ascii=False) + "\n")
print("registered:", resp.json()["id"])

関連付けタイプID(メモ→商談)はHubSpotが定義している値で、オブジェクトの組み合わせごとに異なります。実装前に公式ドキュメントで確認してください。Salesforceの場合も同じ形で、承認済みファイルからToDo(Task)などを作成するスクリプトを用意し、実行をaskにします。

なぜ「ファイルを移す」で承認するのか 下書きのJSONに"status": "approved"と書かせる方式だと、Claude Code自身が書き換えられてしまいます。Claudeが編集できない場所(approved/)にファイルがあることを承認の証拠にすれば、承認の操作は必ず人の手を通ります。さらに登録スクリプトの実行自体もaskなので、二重の関所になります。

CLAUDE.mdに項目定義・ステージ定義を書く

CRMの分析で一番ずれやすいのは、「この項目は何を意味するか」です。「金額」は税込か税抜か、見込みか受注額か。「最終活動日」には何が含まれるか。CLAUDE.md(プロジェクト直下に置くと毎回読み込まれる手順書。/initで雛形を作れます)に、項目定義とステージ定義を書いておきます。

CLAUDE.mdMarkdown
# CLAUDE.md(営業データ分析・CRM運用)

## 目的
- HubSpot / Salesforce のデータを読み、停滞商談・週次サマリー・重複候補・データ品質の点検と、
  活動記録の下書きを作る。CRMのデータ更新は、人の承認後に scripts/write/ 経由でのみ行う

## データの取り方
- HubSpot: python scripts/hubspot/fetch_deals.py / fetch_contacts.py → scripts/analyze/*.py
- Salesforce: sf data query(--target-org sbx)または scripts/salesforce/sf_query.py
- 件数・金額の集計は reports/ のCSVを使う。暗算で再集計しない

## 項目定義(HubSpot 内部名 / Salesforce API参照名)
- 商談金額: amount / Amount(税抜。見込み金額であり受注額ではない)
- 完了予定日: closedate / CloseDate
- 最終活動日: notes_last_updated / LastActivityDate(メール・通話・会議の記録日)
- 次のステップ: next_step(カスタム)/ NextStep

## ステージ定義(停滞判定は config/stage_rules.json が正本)
- 初回商談設定 → 要件確認 → 提案 → 契約書送付 → 受注 / 失注
- 「停滞」= 最終活動日からの経過日数がステージごとの stale_days 以上
- ステージIDとラベルの対応は reports/stages.json を参照(IDは表示名と異なる)

## 下書きのルール
- 活動記録: notes/inbox/ のメモから drafts/activities/*.json を作る。approved/ へ移すのは人
- メモにない事実(金額・日付・決裁者)を補わない。不明点は「要確認」と書く
- 次アクション案は「誰が・何を・いつまでに」の形で、最大3つ

## 禁止
- レコードの更新・削除・統合(merge)、オーナー変更、一括インポート
- .env の参照、reports/ の個人情報を外部サービスやチャットに貼ること
エージェント博士
エージェント博士営業の現場では、同じ「提案中」でも人によって意味が違ったりするもんじゃ。AIに定義を教えようとすると、チームの定義のあいまいさが浮かび上がる。それ自体が、CRM運用を良くする一歩なんじゃよ

permissionsで「読む・下書く・承認して書く」を分ける

.claude/settings.jsonのpermissionsで、3段階をはっきり分けます。allowは確認なし、askは毎回確認、denyは禁止です。

.claude/settings.jsonJSON
{
  "permissions": {
    "allow": [
      "Bash(python scripts/hubspot/fetch_deals.py:*)",
      "Bash(python scripts/hubspot/fetch_contacts.py:*)",
      "Bash(python scripts/analyze/stale_deals.py:*)",
      "Bash(python scripts/analyze/find_duplicates.py:*)",
      "Bash(python scripts/salesforce/sf_query.py:*)",
      "Bash(sf data query:*)",
      "Read(./reports/**)",
      "Read(./notes/**)",
      "Edit(./drafts/**)"
    ],
    "ask": [
      "Bash(python scripts/write/register_activity.py:*)",
      "Edit(./scripts/**)",
      "Edit(./config/**)"
    ],
    "deny": [
      "Read(./.env)",
      "Bash(curl:*)",
      "Bash(sf data update:*)",
      "Bash(sf data delete:*)",
      "Bash(sf data upsert:*)",
      "Bash(sf data import:*)",
      "Bash(sf data create:*)",
      "Bash(sf apex:*)",
      "Bash(sf project deploy:*)",
      "Bash(sf org display:*)",
      "Edit(./approved/**)",
      "Edit(./logs/**)"
    ]
  }
}
  • allow=読む・下書く:取得・分析スクリプト、sf data query、drafts/への書き込み
  • ask=承認して書く:登録スクリプトの実行。判定ルール(config/)やスクリプトの変更も、結果を変えるのでask
  • deny=させない:sf data update/delete/upsert/import/create、Apexの実行、メタデータのデプロイ、approved/と監査ログの編集
  • sf org displayもdeny:組織の情報とともにアクセストークンが表示されるためです。見落としやすいポイントです

ただし、コマンドの前方一致による制御は万能ではありません。本当の防衛線は、CRM側のユーザー権限とトークンのスコープです。分析に使うユーザー・トークンは参照のみ、書き込み用は別に分ける——この土台の上に、permissionsやhooksで操作の範囲をさらに絞ります。安全設計の全体像はAIエージェントを本番運用する安全設計ガイドで解説しています。

Claude Codeとの対話例

以下は流れのイメージです(実際の出力は毎回異なり、社名・件数は説明用の例です)。

例1:週次サマリーと停滞商談の次アクション案

ターミナル(イメージ)text
> 今週のパイプラインのサマリーを作って。停滞している商談は
  担当者別に並べて、それぞれ次のアクション案も。

● Bash(python scripts/hubspot/fetch_deals.py)
● Bash(python scripts/analyze/stale_deals.py)
  ⎿ stale=14 missing=9
● Read(reports/deals.csv) / Read(reports/stale_deals.csv) / Read(config/stage_rules.json)

  ## パイプライン週次サマリー(数値は reports/ から転記)
  | ステージ | 件数 | 金額合計 | 前週比 | ...

  ## 停滞商談(14件)担当者別
  ■ 佐藤さん(5件)
   - A社 導入案件(提案/活動なし21日)
     次アクション案:提案書への質問期限を確認する連絡を、今週中に
   - B社(契約書送付/活動なし9日)
     次アクション案:法務確認の進捗を先方担当に確認
  ...
  ※ 3件は最終活動日が空欄のため、最終更新日で判定しています(basis=last_modified)
  ※ 必須項目の空欄が9件あります → reports/missing_fields.csv

次アクション案は、担当者が採用するかを決める「提案」です。お客様との関係や社内事情はCRMに書かれていないことも多いので、「AIがそう言ったから」ではなく、担当者の判断材料として使います。

例2:商談メモから活動記録を登録するまで

ターミナル(イメージ)text
> notes/inbox/2026-09-25_acme.md から活動記録の下書きを作って。

● Read(notes/inbox/2026-09-25_acme.md)
● Read(reports/deals.csv)       ← 商談名から deal_id を特定
● Write(drafts/activities/2026-09-25_acme.json)

  下書きを作成しました。メモに決裁者の役職がなかったため「要確認」と
  しています。内容を確認し、approved/ に移してから登録を指示してください。

> 1行直して approved/ に移した。登録して。承認者は yamada。

● Bash(python scripts/write/register_activity.py approved/2026-09-25_acme.json yamada)
  ← ask 設定のため、実行前に確認を求められる
  ⎿ registered: 98765432101
必須項目の空欄は「責める」より「頼む」 データ品質のチェック結果を担当者に共有するときは、Claude Codeに担当者別の入力依頼文を下書きさせると角が立ちにくくなります。空欄の一覧をそのまま全体チャンネルに貼るのではなく、担当者ごとに必要な項目だけを伝えるのがコツです。

MCPサーバーの使いどころ

MCP(Model Context Protocol)はAIアプリと外部ツールをつなぐ共通規格です。Salesforceは開発者向けのSalesforce DX MCP Serverを、HubSpotもCRMデータにアクセスするMCPサーバーを公開しています(いずれも執筆時点。ベータを含む)。

ターミナルBash
# Salesforce DX MCP Server(執筆時点で公開。オプションは公式READMEで確認)
claude mcp add salesforce -- npx -y @salesforce/mcp --orgs sbx
# HubSpot のMCPサーバー(公開形態・URL・認証方式は公式ドキュメントで確認)
claude mcp add --transport http hubspot <公式ドキュメントに記載のURL>

MCPは「この会社の商談の経緯を教えて」のようなその場の調べものに向いています。一方、週次サマリーや停滞判定のような毎回同じ基準で出したい数字は、この記事のようにスクリプトで固定したほうがレビューしやすく、再現性があります。MCPサーバーによってはレコードの作成・更新ツールも含まれるため、Sandboxや参照権限のユーザーで接続し、書き込み系ツールは有効にしないのが原則です。コマンド・URL・オプションは変わりやすいので、導入時に公式ドキュメントで確認してください。

Skills化とヘッドレスでの週次実行

毎週の営業会議の資料づくりは、Skillsにまとめます。.claude/skills/<name>/SKILL.mdに手順を書くと、Claude Codeは関連する依頼のときに読み込んで使います。

.claude/skills/weekly-pipeline/SKILL.mdMarkdown
---
name: weekly-pipeline
description: HubSpot(またはSalesforce)の商談データから、パイプラインの週次サマリーと停滞商談の一覧・次アクション案を作る。週次の営業会議資料、パイプライン確認、停滞商談の洗い出しを頼まれたときに使う。
---
1. fetch_deals.py → stale_deals.py を実行する
2. reports/weekly_YYYYMMDD.md に次の順で書く
   - ステージ別の件数・金額(表)と前週からの変化(前週のファイルがあれば比較)
   - 今週クローズ予定の商談 / 停滞商談(担当者別、金額の大きい順)
   - 必須項目の空欄(担当者別の件数のみ。個別の指摘は担当者に直接)
3. 次アクション案は「誰が・何を・いつまでに」で1商談1〜2個。事実とAIの提案を分けて書く
4. 個人の連絡先(メール・電話)はレポートに書かない

月曜の朝に自動で作るなら、claude -pのヘッドレス実行をcronなどから呼びます。無人で動くので、許可するツールは取得・分析とreports/への書き込みだけに絞り、登録系は含めません。

scripts/weekly_pipeline.sh(cronで毎週月曜朝に実行)Bash
#!/usr/bin/env bash
set -euo pipefail
cd "$(dirname "$0")/.."
claude -p "weekly-pipeline の手順で今週のサマリーを作成して" \
  --allowedTools "Bash(python scripts/hubspot/fetch_deals.py:*)" \
                 "Bash(python scripts/analyze/stale_deals.py:*)" \
                 "Read" "Write(./reports/**)" \
  --output-format json > "logs/weekly_$(date +%Y%m%d).json"

できたサマリーをSlackに投稿したい場合は、Claude Code×Slack・Notion API連携の方法と組み合わせます。チャンネルに流すのは集計と所見までにし、顧客の連絡先や個別の商談メモは載せないようにします。

助手アイ
助手アイ月曜の朝に会議資料ができあがっているのは助かりますね。担当者ごとの「今週やること」も自動で届けられそう!

注意点:個人情報・権限・承認・監査ログ

個人情報

  • 取らない・マスクする:分析に不要な連絡先は取得しない。重複チェックのように必要な場合も、出力ではマスクする
  • 利用目的とデータの取り扱い条件:顧客の個人情報を扱う以上、個人情報保護法上の利用目的の範囲内か、社内規程でAIサービスへの入力が認められているか、利用するClaudeのプラン・契約のデータ取り扱い条件はどうか、を事前に確認します
  • reports/はGitにコミットしない:.gitignoreに入れ、共有範囲を決めておきます

権限

  • 統合ユーザーは「見えすぎ」に注意:全データを参照できるユーザーで分析すると、本来その営業チームが見られない商談までレポートに載ることがあります。レポートの配布先に合わせて取得範囲を絞ります
  • トークンは用途ごとに分け、定期的に見直す:退職・異動・プロジェクト終了時に失効できるよう、発行者と用途を記録しておきます

更新は人が承認し、監査ログを残す

  • 自動化ルールとの干渉:CRMのワークフローやフローは、項目の更新をきっかけに動きます。1件の更新がメール送信や担当者変更を引き起こすこともあるため、書き込みは必ずSandboxで挙動を確認します
  • 監査ログは二重に:CRM側の項目履歴(HubSpotのプロパティ履歴、Salesforceの項目履歴管理や設定変更履歴など。プラン・設定による)に加え、この記事のように自前のログに「誰が承認したか」を残すと、AI経由の更新を後から追跡できます
  • 一括更新はしない:件数の多い更新は、影響範囲の確認と戻し方の準備ができるまで行いません
商談メモや問い合わせ本文に紛れ込む「指示」に注意 CRMには、お客様からのメールや問い合わせフォームの本文など、社外の人が書いたテキストが入っています。AIへの指示のような文字列が紛れ込む可能性(プロンプトインジェクション)を前提に、読み取りの権限で動くClaude Codeには書き込み手段を持たせない構成にしておくことが一番の対策です。
自社の知見

当社が支援の現場で採っている進め方

当社が営業部門へのAIエージェント導入を支援する際は、読み取り専用の週次サマリーから始めることを基本にしています。まず「停滞」や「必須項目」の定義を営業責任者と文章で合意し、それをCLAUDE.mdと設定ファイルに落とします。数週間運用して判定のずれを直したうえで、活動記録の下書きのような「人の承認つきの書き込み」へ進みます。

この「AIが作り、人が通す」流れは、このサイトの運用(Claude Codeが作業ブランチで変更し、人が確認してマージ)と同じ考え方です。詳しくは【自社事例】AIエージェントでメディアサイトを運用する仕組みをご覧ください。

よくある質問

HubSpotとSalesforce、Claude Codeと連携しやすいのはどちらですか?

どちらも連携できます。HubSpotはプライベートアプリのトークンでREST APIを呼ぶ形がシンプルで、Pythonの小さなスクリプトで始めやすいです。Salesforceは公式のsf CLIでログインとSOQLの実行ができるため、Claude Codeからはsf data queryをコマンドとして使うのが扱いやすい方法です。どちらの場合も、取得と集計はスクリプト、解釈と下書きはClaude Codeという役割分担は同じです。

重複コンタクトの統合までClaude Codeに任せてもいいですか?

おすすめしません。統合は元に戻しにくく、どちらのレコードの値を残すか、関連する商談や活動履歴をどう扱うかなど、業務上の判断が必要だからです。Claude Codeには重複の候補リストと統合方針の案までを作らせ、実際の統合はCRMの標準機能で担当者が確認しながら行う運用が安全です。

商談メモや顧客情報をClaude Codeに読ませても大丈夫ですか?

社内規程と、利用しているClaudeのプラン・契約のデータ取り扱い条件を確認したうえで、必要最小限にとどめるのが原則です。分析に不要な連絡先は取得しない、出力ではメールアドレスをマスクする、reportsフォルダをGitにコミットしないといった工夫で、AIが触れる個人情報を減らせます。

Sandboxで試してから本番に切り替えるときの注意点は?

本番では、実際の営業担当者のデータとワークフローや自動化ルールが動いています。まずは読み取りだけで本番に接続し、Sandboxと同じ結果の形になるかを確認します。書き込みを伴う操作は、専用の権限を持つユーザーやトークンを分け、対象と上限、承認者、監査ログを決めてから段階的に広げてください。

まとめ|「定義はチームで、判定はスクリプト、下書きはAI、登録は人」

HubSpot CRM APIとSalesforce(sf CLI・SOQL)をClaude Codeにつなぐと、停滞商談の洗い出し、週次サマリー、重複の候補出し、活動記録の下書き、データ品質チェックといった「CRMを生きたデータに保つ」作業を、毎週の仕組みとして回せます。成功のポイントは、定義を文章で合意し、判定はスクリプトで決定的に行い、Claude Codeには整理と下書きを任せ、CRMへの登録は人の承認と監査ログを通すことです。まずはSandboxや検証用アカウントでfetch_deals.pyとstale_deals.py、またはsf data queryを動かし、対話で週次サマリーを1本作ってみてください。EC・決済データとつなぐ方法はClaude Code×Shopify・Stripe API、シリーズ全体の地図はAIエージェントで開発してみよう|API連携編をご覧ください。

業務自動化のスキルを、キャリアと収入につなげる

APIとAIエージェントで業務を自動化できる人材は、社内でも社外でも求められています。次の一歩に役立つ記事です。

AI・IT業界への転職ガイド【2026】作ったものをポートフォリオに。未経験からの進め方と転職エージェントの選び方
記事を読む

次の一歩

姉妹サイトの記事で、次に気になりやすいことを調べられます。