icp-builder について
LinkedInプロフィールからペルソナと推定ICPの設定ファイルを作り、関連する営業スキルで使えるようにするスキルです。Crustdataのデータと投稿から情報を導き、再利用可能なGTM設定を書き込みます。
主な機能
- 01LinkedInに基づくペルソナ作成
- 02プロフィールと企業情報からのICP推定
- 03投稿からの文体分析
- 04任意の営業ツール構成
- 05再利用可能なGTM設定ファイルの作成
活用シーン
- 01営業開拓ワークフロー用のGTM情報設定
- 02営業担当者のプロフィールから推定ICPを作成
- 03投稿からユーザーの文体を把握
- 04アカウント調査スキル向け設定ファイルの準備
MCP 対応
`execute`スクリプトを通じてCrustdata MCPツールを使い、人物と企業の情報を補完してソーシャル投稿を取得します。
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ crustdata/ skills --skill icp-builderインストール後、Claude Code で/icp-builderを入力して読み込まれたか確認します。
同梱ファイルevals/evals.jsonREADME.md
詳細
ICP ビルダー
LinkedIn URL を 1 つ入力するだけで、すぐに使える GTM 設定が作成されます。このスキルは、Crustdata を使ってユーザー自身のプロフィールを充実させ、config/persona-profile.md と config/gtm-config.md を作成します。これらのファイルは sales-prospecting と account-research が起動時に読み込みます。
手順は必ず次の順に進めます。
- スタック(任意。完全にスキップ可能):使用しているツールを確認します。
- ペルソナ:LinkedIn URL を 1 つ入力すると、Crustdata がユーザーの人物像、販売しているもの、推定 ICP、文体を割り出します。
- 設定を書き込み、引き継ぎます。
ユーザーに質問攻めをしないでください。「何を販売していますか」「ICP は誰ですか」「文体がわかるメールを貼り付けてください」などとは尋ねないでください。これらはすべて LinkedIn URL と投稿から導き出します。URL がインタビューのすべてです。
Step 0:既存の設定を確認する
作業ディレクトリに config/gtm-config.md または config/persona-profile.md がすでに存在する場合は、それらを読み込み、内容を 2 行で要約してから、ペルソナ全体を更新するか、特定のフィールドを更新するかを尋ねてください。ユーザーがすでに修正した設定を、黙って上書きしてはいけません。更新時は、既存のスタック項目を変更せずに引き継ぎ、ユーザーから変更を求められない限り、スタックについて再度尋ねないでください。ファイルがないのが通常の状態です。このスキルでファイルを作成します。
Step 1:歓迎の言葉と任意のスタックに関する質問
短い歓迎の言葉を 1 行で伝えてから、任意の質問を 1 つだけします。使用しているツールは何ですか? 各項目を一度確認し、ユーザーにツール名を挙げてもらうか、スキップしてもらいます。質問全体をスキップした場合は、すべての項目に none と記入して先に進みます。
- データプロバイダー — このスキルが使用するデータソースである Crustdata(コネクターとして追加します。接続されていない場合は、以下のデータなしのフォールバックを使用します)
- CRM — またはスキップ
- カレンダー — またはスキップ
- メール — またはスキップ
- 通話録音ツール — またはスキップ
- シーケンサー — またはスキップ
- チームチャット — またはスキップ
この手順のルール:
- 接続済みのコネクターを見て、スタックを推測してはいけません。 コネクターが接続済みでも、それがユーザーの選択とは限りません。質問するか、
noneと記入してください。 - どの項目もスキップできます。辞退されたツールについて、強く勧めたり、再度尋ねたりしてはいけません。
- スキップした項目は設定で
noneとなり、後続のスキルはその項目を下書きのみで実行します。ツールへの登録ではなく、下書きと CSV エクスポートを行います(「シーケンサー用に CSV をエクスポートする」「CRM ではなくファイルに記録する」)。
Step 2:LinkedIn URL → ペルソナ
1 つだけ尋ねます。ユーザーの LinkedIn URL です。次に、1 つの execute スクリプトでペルソナを作成します。人物の検索を最初に行います。企業情報の充実と投稿の取得はどちらも人物検索に依存しますが、互いには依存しないため、この 2 つを parallelMap で並列実行します。
すべてのスクリプトは、ソースを明記したクエリコメント(// user query: ... または // model query: ...)から始める必要があります。これがないスクリプトは、実行前に却下され、費用は発生しません。
// user query: set up my GTM config — my LinkedIn is https://www.linkedin.com/in/example
const url = "https://www.linkedin.com/in/example";
// Stage 1: the person. Base cost 1 credit. `fields` is a response WHITELIST —
// the result carries ONLY the groups listed here; an omitted group reads as
// undefined later and looks like missing data. basic_profile + experience covers
// the persona; social_handles carries the canonical profile URL the posts pull
// is keyed on; contact groups only add cost.
const pr = await callTool("person_enrich", {
professional_network_profile_urls: [url],
fields: ["basic_profile", "experience", "social_handles"],
});
if (!pr.ok) return { error: pr.message };
const person = pr.data[0]?.matches?.[0]?.person_data;
if (!person) return { error: "no_match" }; // → confirm the URL, then no-data fallback
const canonicalUrl = profileUrl(person) ?? url; // preloaded accessor
const current = person.experience?.employment_details?.current?.[0] ?? {};
const companyId = currentCompanyIds(person)[0]; // preloaded accessor
// Stage 2: company + posts are independent of each other — fan them out.
const calls = [
{ name: "social_post_list_live",
params: { professional_network_profile_url: canonicalUrl, limit: 10 } }, // 1 cr/post — cap deliberately
];
if (companyId) {
calls.push({ name: "company_enrich",
params: { crustdata_company_ids: [companyId], exact_match: true,
fields: ["basic_info", "taxonomy"] } }); // 2 cr, exactly one match
}
const results = await parallelMap(calls, async (c) => ({ name: c.name, r: await callTool(c.name, c.params) }));
const postsR = results.find(x => x.name === "social_post_list_live")?.r;
const companyR = results.find(x => x.name === "company_enrich")?.r;
// Posts are optional: a failed or empty pull means neutral voice, not a failed run.
const posts = postsR && postsR.ok
? (postsR.data.posts ?? []).map(p => ({
text: p.text,
date: p.date_posted,
reactions: p.engagement?.total_reactions,
comments: p.engagement?.total_comments,
}))
: [];
const company = companyR && companyR.ok
? pick(companyR.data[0]?.matches?.[0]?.company_data ?? {}, ["basic_info", "taxonomy"])
: null;
// Return the smallest projection — only what the script returns reaches the model.
return {
identity: {
name: person.basic_profile?.name,
title: person.basic_profile?.current_title,
location: person.basic_profile?.location,
company: current.name,
company_domain: current.company_website_domain,
start_date: current.start_date, // tenure = today minus this
},
past_roles: (person.experience?.employment_details?.past ?? []).slice(0, 5)
.map(e => ({ company: e.name, title: e.title })),
company,
posts,
};
このスクリプトに関する注意事項:
person_enrichでpreview: trueを設定しないでください。 プランに依存するため、一部のアカウントでは 400 エラーが返されます。このフローがこれに依存してはいけません。いずれにしても基本コストは 1 クレジットです。person_enrichのフィールドはbasic_profile、experience、social_handlesのみにしてください。 許可リストにsocial_handlesがないと、profileUrlアクセサーがundefinedを読み込み、投稿の取得でユーザーが直接入力した URL にフォールバックします。一部のグループ(certifications、honors、updated_at)はプランによって制限されています。制限対象のフィールドを指定すると、そのフィールド名を示す 403 エラーが発生し、呼び出し全体が失敗します。その場合はフィールドを削除して、再実行してください。- レスポンスのパスはフィルターのパスとは異なります。役職は
basic_profile.current_title、現在の勤務先はexperience.employment_details.current[].name、正規のプロフィール URL はsocial_handles.professional_network_identifier.profile_urlにあります(profileUrlアクセサーがこれを読み込みます)。
会社のフォールバック:プロフィールに会社 ID がない場合
現在の職歴に会社 ID がない場合は、まずドメイン(または名前)で会社を特定します。company_identify は無料であいまい検索を行うため、1 つの識別子から複数の会社が返されることがあります。confidence_score が最も高い候補を選び、exact_match: true を指定して ID で詳細情報を取得します。これが最も安価で正確な方法です。無料の特定と、正確に 1 件の詳細情報取得に 2 クレジットを使います。
// model query: resolve and enrich the user's current company by domain
const idr = await callTool("company_identify", { domains: ["example.com"] }); // ONE identifier type per call
if (!idr.ok) return { error: idr.message };
const matches = idr.data[0]?.matches ?? [];
const top = matches.slice().sort((a, b) => (b.confidence_score ?? 0) - (a.confidence_score ?? 0))[0];
if (!top) return { error: "no_company_match" };
const id = top.company_data?.basic_info?.crustdata_company_id ?? top.company_data?.crustdata_company_id;
const er = await callTool("company_enrich", {
crustdata_company_ids: [id],
exact_match: true,
fields: ["basic_info", "taxonomy"],
});
if (!er.ok) return { error: er.message };
return pick(er.data[0]?.matches?.[0]?.company_data ?? {}, ["basic_info", "taxonomy"]);
company_identify では social_profiles を指定しないでください。プランの制限対象であり、指定すると呼び出し全体が 403 エラーになります。
返されたデータからペルソナを導き出す
- アイデンティティ:名前、役職、会社、在籍期間(
start_dateから)、過去の職歴に基づく経歴の要約(1 行)。 - 会社と販売するもの:
basic_infoとtaxonomyから製品とカテゴリを特定します。会社の説明とユーザーの投稿トピックから、モニタリングするキーワードを決めます。 - 声:実際の投稿から、文の長さ、一人称か三人称か、専門用語のレベル、絵文字の使用、書き出し方など、口調や文体に関するメモを作成します。投稿が空の場合は「ニュートラル」と記入して先に進みます。
- 関心のあるトピック:投稿で繰り返し扱われるテーマを、エンゲージメントを考慮してまとめます。
推定 ICP — 推定であることを明記し、フィルターに使える形にする
会社が販売するものと、一般的な購入者像から ICP を導き出します。業界、従業員数の範囲、地域、資金調達ステージ、購入者の役職、購入者の職位を含めます。必ず inferred とラベル付けしてください。これはユーザーが修正するための仮説であり、事実ではありません。
後続の検索でそのまま使える ICP の値を記入します。カテゴリフィールドは選択肢が限定されており、もっともらしくても間違った値を指定すると、エラーなく結果が 0 件になることがあります。そのため、設定に記入する前にオートコンプリート(無料)で値を確認してください。
// model query: resolve filter-ready values for the inferred ICP
const probes = [
{ tool: "company_autocomplete", params: { field: "basic_info.industries", query: "software" } },
{ tool: "person_autocomplete", params: { field: "experience.employment_details.current.seniority_level", query: "vice" } },
];
return await parallelMap(probes, async (p) => {
const r = await callTool(p.tool, p.params);
// Returns shape is { suggestions: [{ value }] } — project to the value strings.
return { field: p.params.field, values: r.ok ? (r.data.suggestions ?? []).map(s => s.value) : [], error: r.ok ? null : r.message };
});
購入者の職位には、experience.employment_details.current.seniority_level の正確な語彙を使う必要があります:Entry Level、Entry Level Manager、Experienced Manager、Senior、Director、Vice President、CXO、Owner / Partner、In Training、Strategic。確信が持てない場合は推測せず、person_autocomplete で確認してください。
正確さは絶対条件です
このプロフィールは後続のすべてのスキルに影響するため、誤った情報がすべてを損ないます。
- ソースデータで裏付けられる内容だけを記入します。確認できないことは推測せず、その旨を伝えます。
- 推定にはすべてラベルを付けます(ICP には必ず
inferredとラベル付けします)。 - ファイルを書き込む前に、ペルソナをユーザーに確認します:「あなたについて、こう理解しました。違っているところがあれば教えてください。」修正を反映してから書き込みます。
ステップ 3:設定ファイルを書き込む
作業ディレクトリに両方のファイルを書き込みます。config/persona-profile.md はペルソナの全体版です。config/gtm-config.md には会社/ICP/声の要点に加えて技術スタックを記載し、どのスキルからも一度の読み込みで確認できるようにします。
config/persona-profile.md
# Persona Profile
Built by icp-builder on <YYYY-MM-DD>. Read by sales-prospecting, account-research, sales-outreach, and meeting-prep.
## Identity
- Name:
- Title:
- Company: <name> (<domain>)
- Tenure: since <start date>
- Background: <one line from past roles>
## Company & what we sell
- Product:
- Category:
- Keywords to monitor:
## Inferred ICP
Label: inferred from <what the company sells + typical buyers>. User-confirmed: <yes/no>
- Industries: <filter-ready values>
- Headcount:
- Geography:
- Funding stage:
- Buyer titles:
- Buyer seniority: <exact seniority vocabulary values>
## Voice
- Tone:
- Style notes:
- Always: no em dashes; never "delve", "leverage", or "streamline"; no filler;
write like a colleague.
## Topics they care about
- <from posts, weighted by engagement>
config/gtm-config.md
# GTM Config
Read by sales-prospecting, account-research, sales-outreach, and meeting-prep at startup.
## Stack
- Data provider: crustdata | none
- CRM: <tool> | none
- Calendar: <tool> | none
- Email: <tool> | none
- Call recorder: <tool> | none
- Sequencer: <tool> | none
- Team chat: <tool> | none
`none` = that slot runs draft-only: drafts and CSV exports instead of pushing to the tool.
## What we sell
<one or two lines>
## ICP (inferred)
- Industries:
- Headcount:
- Geography:
- Funding stage:
- Buyer titles:
- Buyer seniority:
## Customers
none yet — add names or domains as you close; sales-prospecting uses them for lookalikes.
## Voice
<tone in one line>. No em dashes; never "delve", "leverage", or "streamline"; no filler;
write like a colleague.
引き継ぎ
技術スタックのうち接続したものとスキップしたもの、ペルソナの要約(2~3 行)、inferred とラベル付けした項目をまとめます。その後、次のように伝えます。
セットアップが完了しました。sales-prospecting(「私の ICP に合うリストを作って」)または account-research(「 を調査して」)を試してください。どちらもこの設定を自動的に読み込みます。
データがない場合のフォールバック
Crustdata が接続されていない場合、または詳細情報が乏しい場合(一致なし、プロフィール情報が少ない、投稿が 0 件)は、次のように対応します。
- ユーザーから 2~3 行だけ聞きます。名前と役割、会社が何をしているか、誰に販売しているかを確認します。聞くのはこれだけです。長い質問票は絶対に実施しないでください。
- その内容をもとに両方の設定ファイルを書き込みます。声は「ニュートラル」と、スロップを避けるルールにします。ICP には引き続き
inferredとラベル付けします。 - 詳細情報が一部しか得られなかった場合は、確認できた内容を残し、推定できなかった内容を具体的に伝え、その部分だけユーザーに追記してもらいます。
コスト
person_enrichでbasic_profile+experience+social_handlesを指定:1 クレジット。company_identify、company_autocomplete、person_autocomplete:無料。exact_match: trueを指定して ID でcompany_enrichを実行:1 件の一致につき 2 クレジット。social_post_list_live:投稿 1 件につき 1 クレジット。limitは必ず意図的に設定してください (文体の把握には 10 件で十分です)。- 一般的なフル実行:約 13 クレジット。すべての
executeレスポンスにはcreditsとcredits_remainingが含まれます。account_credits(無料)で残高を確認できます。
エラー処理
- すべてのスクリプトで
r.okに応じて分岐してください。 呼び出しに失敗してもスクリプトは中断されません。失敗を確認しないまま処理が進むと、空のデータが使われ、「結果なし」のように見えます。 person_enrichで一致するものがない場合 → URL に誤りがないか(タイプミス、バニティスラッグの変更)ユーザーに確認してから、データがない場合のフォールバックを使用します。- フィールド名が示された 403 エラーは、プランによって制限されたプロジェクションを意味します。そのフィールドを除外して再実行してください。
- 投稿の呼び出しが失敗した場合や、投稿が空の場合もエラーではありません。文体はニュートラルになります。
company_enrichに失敗した場合 → 人物データだけを使ってペルソナを維持し、不足している情報を記載してください。
ルール
- まず歓迎の言葉を伝えます。任意のスタックに関する質問は1つまで。インタビューはURLだけで完結させます。
- 接続済みのコネクターからスタックを推測してはいけません。 尋ねるか、
noneと記述します。 - 何を販売しているか、ICP、ブランドの口調について尋ねてはいけません。 そこから推測します。エンリッチメントの情報が乏しい場合は、2~3行にまとめ、インタビュー全体にはしません。
- ファイルを書き込む前に、ペルソナの内容を本人に示して確認・修正してもらいます。
- 推測にはラベルを付けます。 スキップしたツールには
noneと記述します。スタックやペルソナの詳細を捏造してはいけません。 - 文体には常に「粗雑な文章にしない」ルールを適用します: emダッシュは使わない。「delve」「leverage」「streamline」は使わない。余計な言葉を避け、同僚に話しかけるように書きます。
- 設定がなくても、作業を妨げてはいけません。このスキルが設定を作成し、設定がない場合は後続のスキルからこのスキルを参照します。
- コンテンツに合わせてレイアウトを調整し、何も隠さないようにします。 ブランドシステムは固定ですが、レイアウトは固定ではありません。実際のコンテンツが収まらない場合、たとえば会社名や人名が長い場合、タイトルが12語の場合、行数が200行の場合は、コンテンツではなくレイアウトを変更します。カードを広げる、切り捨てずに折り返す、1列にする、列幅を広げる、上限を引き上げる、または横幅の広い要素専用のスクロール領域を設けるなどの方法があります。カードを切り取る、名前を省略記号で切り詰める、行を黙って削除するといった方法で、表示上の問題を解決してはいけません。上限がどうしても避けられない場合は、読者が表示されていない内容を把握できるよう、UIに「214件中、上位50件を表示」のように明記します。レンダリング結果を確認し、欠けている部分を修正してから引き渡します。
- レンダリング結果のアイコン:ダッシュボードのアイコンセットであるLucideを、
currentColorのストロークを持つインラインSVGとして使います。成果物のUIに絵文字は使いません。 - ペルソナ本人の写真も無料です:
basic_profile.profile_picture_permalinkは、Step 2のperson_enrichがすでに返すbasic_profileグループに含まれています。ペルソナの概要ページは人物についてのものです。写真はBase64でインライン化し(同じbinary/octet-streamのルールを適用)、写真がない場合はモノグラムを表示します。 - 会社ロゴは無料です。レンダリングされたペルソナページに使用します。
basic_info.logo_permalinkは、無料のcompany_identifyと、ペルソナ向けにすでに実行するcompany_enrichから取得できます。メディアCDNがこれらをbinary/octet-streamとして配信するため、リモートの<img src>は空白で表示されます。data:image/jpeg;base64,...URIとしてBase64でインライン化し、ロゴがない場合はモノグラムを表示します。 - 成果物のブランディング:設定ファイルはプレーンなMarkdownのままにし、機械が読み取るファイルにブランディングのノイズを加えません。ただし、ペルソナをページや文書(ペルソナの概要ページ、ICPの要約文書など)としてレンダリングする場合は、ヘッダーまたはフッターにCrustdataのブランドロックアップを入れます。小さな大文字の「Powered by」を上に添え、crustdata.comへのリンクを設定した公式のCrustdataワードマークを配置します。ワードマークのペアは、このスキルの
assets/にあります。crustdata-logo-light.png(明るい背景用の濃い文字)とcrustdata-logo-dark.png(暗い背景用の白い文字)で、app.crustdata.comのヘッダーに表示されるものと同じです。テーマに適したバリエーションを高さ約17pxでBase64インライン化し、ホットリンクは使いません。レンダリングされた成果物はリモート画像を取得できません。ブランドアクセントカラーは#5547E2(製品のプライマリカラー。暗い背景では#8387FF)です。埋め込み可能なら本文フォントにGeistを使い、それ以外の場合はシステムフォントスタックを使います。マークを入れるだけのために成果物をレンダリングしてはいけません。
ツールの依存関係
このスキルには、以下が必要です。
- Crustdata MCP サーバー(install.crustdata.com/mcp):
list_tools、get_schema、executeを公開する単一の Code Mode MCP です。すべての Crustdata データツールには、プレーンな JavaScript スクリプトのexecute({ code })内でawait callTool(name, params)を呼び出してアクセスします。get_schemaの型付きサーフェスを基に 作成しますが、スクリプト本文には型注釈を一切含めないでください(型注釈があると パースエラーになり、実行全体が失敗します)。ここで使用するツール:person_enrich、company_identify、company_enrich、social_post_list_live、company_autocomplete、person_autocomplete、account_credits。 - 作業ディレクトリへの書き込み権限 —
config/persona-profile.mdとconfig/gtm-config.mdを作成します。
sales-prospecting および account-research とともに提供され、これらは本スキルが書き込む設定を読み込みます。