icp-builder

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

ICP Builder

ICP构建工具

One LinkedIn URL in, a working GTM config out. This skill enriches the user's own profile via Crustdata and writes
config/persona-profile.md
+
config/gtm-config.md
— the files sales-prospecting and account-research read at startup.
Three steps, always in this order:
  1. Stack (optional, fully skippable): which tools they use.
  2. Persona: one LinkedIn URL; Crustdata turns it into who they are, what they sell, an inferred ICP, and their writing voice.
  3. Write config + hand off.
Never interrogate the user. Do not ask "what do you sell", "who's your ICP", or "paste your voice emails". All of that is derived from the LinkedIn URL and their posts. The URL is the entire interview.

只需输入一个LinkedIn URL,即可生成可用的GTM配置。此技能通过Crustdata丰富用户自身资料,并生成
config/persona-profile.md
config/gtm-config.md
文件——销售探矿客户研究技能会在启动时读取这些文件。
始终按照以下三个步骤执行:
  1. 工具栈(可选,可完全跳过):用户使用的工具。
  2. Persona生成:输入一个LinkedIn URL;Crustdata会将其转化为用户身份、公司业务、推断出的ICP以及写作风格。
  3. 写入配置并移交
绝不询问用户额外信息。不要问“你们销售什么”“你的ICP是谁”或“粘贴你的风格邮件”。所有这些信息都可以从LinkedIn URL和用户发布的内容中获取。URL就是全部所需信息。

Step 0: check for an existing config

步骤0:检查现有配置

If
config/gtm-config.md
or
config/persona-profile.md
already exist in the working directory, read them, summarize what's there in two lines, and ask whether to refresh the whole persona or update specific fields. Never silently overwrite a config the user already corrected. On a refresh, carry the existing Stack entries forward unchanged and do not re-ask the stack question unless the user asks to change it. Missing files are the normal case — this skill creates them.
如果工作目录中已存在
config/gtm-config.md
config/persona-profile.md
文件,请读取它们,用两行内容总结现有信息,然后询问用户是要刷新整个Persona还是更新特定字段。绝不要静默覆盖用户已修正的配置。刷新时,保留现有工具栈条目不变,除非用户要求更改,否则不要重新询问工具栈问题。缺失文件是正常情况——此技能会创建这些文件。

Step 1: welcome + optional stack question

步骤1:欢迎语 + 可选工具栈问题

Open with one short welcome line, then ONE optional question: which tools do you use? One quick pass through the slots; the user names a tool or says skip. If they skip the whole question, write
none
everywhere and move on.
  • Data provider — Crustdata, the data source these skills run on (added as a connector; if it's not connected, use the no-data fallback below)
  • CRM — or skip
  • Calendar — or skip
  • Email — or skip
  • Call recorder — or skip
  • Sequencer — or skip
  • Team chat — or skip
Rules for this step:
  • Never assume the stack from connected connectors. A connected connector is not the user's choice. Ask, or write
    none
    .
  • Every slot is skippable; never pressure or re-ask a declined tool.
  • Skipped slot =
    none
    in the config = downstream skills run that slot draft-only: drafts and CSV exports instead of pushing to the tool ("export a CSV for your sequencer", "log to a file instead of the CRM").
以一句简短的欢迎语开场,然后提出一个可选问题:**你使用哪些工具?**快速遍历各个工具类别;用户可以说出工具名称或选择跳过。如果用户跳过整个问题,就在所有类别中填写
none
并继续。
  • 数据提供商 —— Crustdata,这些技能依赖的数据源(作为连接器添加;如果未连接,请使用下文的无数据回退方案)
  • CRM —— 或跳过
  • 日历 —— 或跳过
  • 邮件 —— 或跳过
  • 通话记录器 —— 或跳过
  • 序列器 —— 或跳过
  • 团队聊天工具 —— 或跳过
此步骤规则:
  • 绝不要从已连接的连接器推断工具栈。已连接的连接器不代表用户的选择。要么询问用户,要么填写
    none
  • 每个类别都可跳过;绝不施压或重新询问用户拒绝回答的工具类别。
  • 跳过的类别 = 配置中填写
    none
    = 下游技能仅运行该类别的草稿模式:生成草稿和CSV导出,而非推送到工具(如“为你的序列器导出CSV”“记录到文件而非CRM”)。

Step 2: LinkedIn URL → persona

步骤2:LinkedIn URL → Persona

Ask for one thing: their LinkedIn URL. Then build the persona in one
execute
script. The person lookup comes first; the company enrich and the posts pull both depend on it but not on each other, so fan those two out with
parallelMap
.
Every script must open with a source-labeled query comment (
// user query: ...
or
// model query: ...
) — scripts without one are rejected before running, at zero spend.
js
// 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,
};
Notes on this script:
  • Never set
    preview: true
    on
    person_enrich
    .
    It is plan-dependent and returns a 400 on some accounts. The flow must never depend on it; base cost is 1 credit anyway.
  • Keep
    person_enrich
    fields to
    basic_profile
    +
    experience
    +
    social_handles
    .
    Without
    social_handles
    in the whitelist the
    profileUrl
    accessor reads
    undefined
    and the posts pull falls back to the raw user-typed URL. Some groups (
    certifications
    ,
    honors
    ,
    updated_at
    ) are plan-gated — a gated projection fails the WHOLE call with a 403 that names the field. If that happens, drop the field and re-run.
  • Response paths differ from filter paths: the title lives at
    basic_profile.current_title
    , the current employer at
    experience.employment_details.current[].name
    , the canonical profile URL at
    social_handles.professional_network_identifier.profile_url
    (the
    profileUrl
    accessor reads it for you).
仅要求用户提供一项信息:他们的LinkedIn URL。然后通过一个
execute
脚本生成Persona。首先进行人物信息查询,公司信息丰富和帖子提取都依赖于人物信息,但彼此独立,因此使用
parallelMap
并行处理这两项任务。
每个脚本必须以带来源标签的查询注释开头(
// user query: ...
// model query: ...
)——没有该注释的脚本会在运行前被拒绝,且不产生费用。
js
// 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
    访问器会帮您读取该值)。

Company fallback: no company id on the profile

公司信息回退方案:资料中无公司ID

If the current employment carries no company id, resolve the company by domain (or name) first.
company_identify
is free and fuzzy — one identifier can return several companies — so pick the top
confidence_score
match, then enrich by id with
exact_match: true
. That is the cheapest exact path: free identify + 2 credits for exactly one enriched match.
js
// 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"]);
Do not project
social_profiles
on
company_identify
— it is plan-gated and 403s the whole call.
如果当前职位信息中没有公司ID,请先通过域名(或名称)解析公司。
company_identify
是免费的模糊匹配工具——一个标识符可能返回多个公司——因此选择
confidence_score
最高的匹配项,然后通过ID调用
company_enrich
并设置
exact_match: true
。这是最便宜的精确路径:免费识别 + 2个积分获取一个精确匹配的丰富信息。
js
// 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错误。

Derive the persona from the returned data

从返回数据中生成Persona

  • Identity: name, title, company, tenure (from
    start_date
    ), one-line background from the past roles.
  • Company & what we sell: product and category from
    basic_info
    +
    taxonomy
    ; keywords to monitor from the company description and the user's post topics.
  • Voice: tone and style notes from the actual posts — sentence length, first vs. third person, jargon level, emoji use, how they open. If posts are empty, write "neutral" and move on.
  • Topics they care about: recurring themes across the posts, weighted by engagement.
  • 身份信息:姓名、职位头衔、公司、任职时长(从
    start_date
    计算)、过往职位的一行简介。
  • 公司与业务:从
    basic_info
    +
    taxonomy
    中提取产品和类别;从公司描述和用户帖子主题中提取需监控的关键词。
  • 写作风格:从实际帖子中提取语气和风格说明——句子长度、第一人称/第三人称使用、术语水平、表情符号使用、开头方式。如果没有帖子,填写“中性”并继续。
  • 用户关注的主题:帖子中反复出现的主题,按互动量加权。

Inferred ICP — label it, and make it filter-ready

推断ICP——标记为推断,且可直接用于过滤

Derive the ICP from what the company sells plus who typically buys it: industries, headcount range, geography, funding stage, buyer titles, buyer seniority. Always label it
inferred
— it is a hypothesis for the user to correct, not a fact.
Write ICP values that downstream searches can use directly. Categorical fields are closed sets — a plausible-but-wrong value silently returns zero rows — so resolve them via autocomplete (free) before writing the config:
js
// 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 };
});
Buyer seniority must use the exact vocabulary of
experience.employment_details.current.seniority_level
:
Entry Level
,
Entry Level Manager
,
Experienced Manager
,
Senior
,
Director
,
Vice President
,
CXO
,
Owner / Partner
,
In Training
,
Strategic
. When unsure, resolve through
person_autocomplete
rather than guessing.
从公司业务和典型客户群体推断ICP:行业、员工规模范围、地域、融资阶段、买家职位头衔、买家职级。始终标记为
inferred
——这是供用户修正的假设,而非事实。
写入可直接用于下游搜索的ICP值。分类字段是封闭集合——看似合理但错误的值会导致返回零条结果——因此在写入配置前,通过自动补全(免费)解析这些值:
js
// 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
解析,而非猜测。

Accuracy is non-negotiable

准确性至关重要

This profile drives every downstream skill; wrong info poisons everything.
  • Only write what the source data supports. If something can't be confirmed, say so instead of guessing.
  • Label every inference (the ICP is always labeled
    inferred
    ).
  • Show the persona back before writing files: "Here's who I think you are — correct me if I'm off." Apply corrections, then write.
此资料会驱动所有下游技能;错误信息会影响所有后续操作。
  • 仅写入源数据支持的内容。如果无法确认某信息,请明确说明,不要猜测。
  • 为所有推断内容添加标签(ICP始终标记为
    inferred
    )。
  • 在写入文件前向用户展示Persona:“这是我推断出的您的信息——如有错误请修正。”应用修正后再写入文件。

Step 3: write the config files

步骤3:写入配置文件

Write both files in the working directory.
config/persona-profile.md
is the full persona;
config/gtm-config.md
repeats the Company / ICP / Voice essentials plus the stack so every skill finds them in one read.
在工作目录中写入两个文件。
config/persona-profile.md
是完整的Persona资料;
config/gtm-config.md
重复公司/ICP/写作风格的核心信息以及工具栈,以便所有技能只需读取一次即可获取所需信息。

config/persona-profile.md

config/persona-profile.md

markdown
undefined
markdown
undefined

Persona Profile

Persona Profile

Built by icp-builder on <YYYY-MM-DD>. Read by sales-prospecting, account-research, sales-outreach, and meeting-prep.
Built by icp-builder on <YYYY-MM-DD>. Read by sales-prospecting, account-research, sales-outreach, and meeting-prep.

Identity

Identity

  • Name:
  • Title:
  • Company: <name> (<domain>)
  • Tenure: since <start date>
  • Background: <one line from past roles>
  • Name:
  • Title:
  • Company: <name> (<domain>)
  • Tenure: since <start date>
  • Background: <one line from past roles>

Company & what we sell

Company & what we sell

  • Product:
  • Category:
  • Keywords to monitor:
  • Product:
  • Category:
  • Keywords to monitor:

Inferred ICP

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>
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

Voice

  • Tone:
  • Style notes:
  • Always: no em dashes; never "delve", "leverage", or "streamline"; no filler; write like a colleague.
  • Tone:
  • Style notes:
  • Always: no em dashes; never "delve", "leverage", or "streamline"; no filler; write like a colleague.

Topics they care about

Topics they care about

  • <from posts, weighted by engagement>
undefined
  • <from posts, weighted by engagement>
undefined

config/gtm-config.md

config/gtm-config.md

markdown
undefined
markdown
undefined

GTM Config

GTM Config

Read by sales-prospecting, account-research, sales-outreach, and meeting-prep at startup.
Read by sales-prospecting, account-research, sales-outreach, and meeting-prep at startup.

Stack

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.
  • 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

What we sell

<one or two lines>
<one or two lines>

ICP (inferred)

ICP (inferred)

  • Industries:
  • Headcount:
  • Geography:
  • Funding stage:
  • Buyer titles:
  • Buyer seniority:
  • Industries:
  • Headcount:
  • Geography:
  • Funding stage:
  • Buyer titles:
  • Buyer seniority:

Customers

Customers

none yet — add names or domains as you close; sales-prospecting uses them for lookalikes.
none yet — add names or domains as you close; sales-prospecting uses them for lookalikes.

Voice

Voice

<tone in one line>. No em dashes; never "delve", "leverage", or "streamline"; no filler; write like a colleague.
undefined
<tone in one line>. No em dashes; never "delve", "leverage", or "streamline"; no filler; write like a colleague.
undefined

Hand off

移交

Summarize: stack connected vs skipped, the persona in 2-3 lines, and what was labeled inferred. Then:
You're set up. Try sales-prospecting ("build me a list from my ICP") or account-research ("research <company>") — both read this config automatically.

总结:已连接/跳过的工具栈、用2-3行描述Persona、以及哪些内容标记为推断。然后:
配置已完成。您可以尝试销售探矿(“根据我的ICP构建客户列表”)或客户研究(“研究<公司>”)——这两项技能会自动读取此配置。

No-data fallback

无数据回退方案

If Crustdata isn't connected, or enrichment comes back thin (no match, sparse profile, zero posts):
  • Take 2-3 lines from the user instead: name and role, what the company does, who they sell to. That's the whole interview — never run a long questionnaire.
  • Write both config files from those lines. Voice = neutral plus the no-slop rule. ICP = still labeled
    inferred
    .
  • If enrichment was partial, keep what was verified, say exactly what couldn't be inferred, and let the user add a line for just that.
如果未连接Crustdata,或丰富信息返回结果有限(无匹配、资料稀疏、无帖子):
  • 仅向用户获取2-3行信息:姓名和职位、公司业务、目标客户。这就是全部所需信息——绝不进行冗长的问卷调查
  • 根据这些信息写入两个配置文件。写作风格 = 中性风格加上简洁规则。ICP = 仍标记为
    inferred
  • 如果仅获取部分丰富信息,保留已验证的内容,明确说明无法推断的部分,让用户补充一行相关信息即可。

Costs

费用

  • person_enrich
    with
    basic_profile
    +
    experience
    +
    social_handles
    : 1 credit.
  • company_identify
    ,
    company_autocomplete
    ,
    person_autocomplete
    : free.
  • company_enrich
    by id with
    exact_match: true
    : 2 credits for one match.
  • social_post_list_live
    : 1 credit per post — always set
    limit
    deliberately (10 is plenty for voice).
  • Typical full run: about 13 credits. Every
    execute
    response carries
    credits
    and
    credits_remaining
    ;
    account_credits
    (free) reports the balance.
  • person_enrich
    (包含
    basic_profile
    +
    experience
    +
    social_handles
    ):1个积分。
  • company_identify
    company_autocomplete
    person_autocomplete
    :免费。
  • 通过ID调用
    company_enrich
    并设置
    exact_match: true
    :2个积分获取一个匹配项。
  • social_post_list_live
    :每个帖子1个积分——始终明确设置
    limit
    (10个帖子足以分析写作风格)。
  • 典型完整流程:约13个积分。每个
    execute
    响应都会包含
    credits
    credits_remaining
    account_credits
    (免费)可查询余额。

Error handling

错误处理

  • Branch on
    r.ok
    in every script.
    A failed call does not abort the script; an unchecked failure silently proceeds on empty data and looks like "no results".
  • person_enrich
    returns no match → confirm the URL with the user (typo, vanity slug change), then use the no-data fallback.
  • A 403 that names a field means a plan-gated projection — drop that field and re-run.
  • A failed or empty posts call is not an error: voice goes neutral.
  • Company enrich fails → keep the persona from person data alone and note what's missing.
  • 在每个脚本中根据
    r.ok
    进行分支处理
    。调用失败不会终止脚本;未检查的失败会导致基于空数据继续执行,看起来像是“无结果”。
  • person_enrich
    返回无匹配 → 与用户确认URL是否正确(拼写错误、自定义链接变更),然后使用无数据回退方案。
  • 返回403错误并指出具体字段表示该字段受订阅计划限制——删除该字段并重试。
  • 帖子调用失败或返回空结果不属于错误:写作风格设为中性。
  • 公司信息丰富失败 → 仅保留人物数据生成的Persona,并注明缺失的内容。

Rules

规则

  • Welcome first; one optional stack question; the URL is the entire interview.
  • Never assume the stack from connected connectors. Ask, or write
    none
    .
  • Never ask what they sell, their ICP, or their voice — derive it. If enrichment is thin, take 2-3 lines, never a full interview.
  • Show the persona back for correction before writing files.
  • Label inferences. Write
    none
    for skipped tools. Never invent stack or persona details.
  • Voice always carries the no-slop rule: no em dashes; never "delve", "leverage", or "streamline"; no filler; write like a colleague.
  • A missing config never blocks anything: this skill creates it, and downstream skills point back here when it's absent.
  • Adapt the layout to the content — never let it hide anything. The brand system is fixed; the layout is not. If real content doesn't fit — a long company or person name, a 12-word title, 200 rows — change the layout, not the content: let the card grow, wrap instead of truncating, drop to one column, widen the column, raise the cap, or give the wide thing its own scroll container. Never solve a fit problem by clipping a card, ellipsing a name, or silently dropping rows. Where a cap really is unavoidable, say so in the UI ("showing the top 50 of 214") so the reader knows what they're not seeing. Look at the rendered output and fix what's cut off before you hand it over.
  • Icons in rendered output: Lucide, the dashboard's icon set, inlined as SVG with a
    currentColor
    stroke. No emojis in artifact UI.
  • The persona's own photo is free too
    basic_profile.profile_picture_permalink
    rides in the
    basic_profile
    group the Step 2
    person_enrich
    already returns. A persona one-pager is about a person; base64-inline the photo (same
    binary/octet-stream
    rule) with a monogram fallback.
  • The company logo is free — use it on a rendered persona page.
    basic_info.logo_permalink
    comes from the free
    company_identify
    and from the
    company_enrich
    you already run for the persona. Base64-inline it as a
    data:image/jpeg;base64,...
    URI (the media CDN serves these as
    binary/octet-stream
    , so a remote
    <img src>
    renders blank); monogram fallback when there's none.
  • Artifact branding: the config files stay plain markdown — no branding noise in machine-read files. But IF the persona is rendered as a page or document (a persona one-pager, an ICP summary doc), it carries the Crustdata brand lockup in the header or footer: a small uppercase "Powered by" eyebrow plus the official Crustdata wordmark, linking to crustdata.com. The wordmark pair ships in this skill's
    assets/
    crustdata-logo-light.png
    (dark text, for light backgrounds) and
    crustdata-logo-dark.png
    (white text, for dark backgrounds), the same files app.crustdata.com's header renders. Base64-inline the theme-appropriate variant at ~17px height — never hotlink; rendered artifacts cannot fetch remote images. Brand accent:
    #5547E2
    (the product primary;
    #8387FF
    on dark grounds). Body font: Geist when embeddable, else the system stack. Never render an artifact just to carry the mark.
  • 先欢迎,再提出一个可选工具栈问题,URL就是全部所需信息
  • 绝不要从已连接的连接器推断工具栈。要么询问用户,要么填写
    none
  • 绝不要询问用户销售什么、他们的ICP是什么或写作风格如何——从数据源推导。如果丰富信息有限,仅获取2-3行信息,绝不进行完整问卷调查。
  • 在写入文件前向用户展示Persona供其修正。
  • 为推断内容添加标签。跳过的工具填写
    none
    。绝不要编造工具栈或Persona的细节。
  • 写作风格始终遵循简洁规则:不使用破折号;绝不使用“delve”“leverage”或“streamline”;无冗余内容;像同事一样写作。
  • 缺失配置绝不会阻止任何操作:此技能会创建配置,下游技能在发现配置缺失时会引导用户回到此处。
  • 根据内容调整布局——绝不隐藏任何信息。品牌系统是固定的,但布局可以调整。如果实际内容无法适配——长公司名或人名、12字的职位头衔、200行数据——请调整布局,而非修改内容:让卡片扩展、换行而非截断、改为单列、加宽列、提高限制、或为宽内容添加滚动容器。绝不要通过裁剪卡片、省略名称或静默删除行来解决适配问题。如果确实需要设置限制,请在UI中明确说明(如“显示前50条,共214条”),让用户知道他们未看到的内容。在移交前查看渲染输出并修复所有被截断的内容。
  • 渲染输出中的图标:使用Lucide(仪表板的图标集),以内联SVG形式呈现,使用
    currentColor
    描边。在工件UI中不使用表情符号。
  • Persona的个人照片也是免费的——
    basic_profile.profile_picture_permalink
    包含在步骤2的
    person_enrich
    返回的
    basic_profile
    字段组中。Persona单页是关于个人的;将照片以base64内联形式呈现(遵循相同的
    binary/octet-stream
    规则),并提供字母组合作为回退方案。
  • 公司Logo是免费的——在渲染的Persona页面中使用
    basic_info.logo_permalink
    来自免费的
    company_identify
    以及为生成Persona而运行的
    company_enrich
    。将其以
    data:image/jpeg;base64,...
    URI的形式base64内联呈现(媒体CDN以
    binary/octet-stream
    形式提供这些资源,因此远程
    <img src>
    会显示空白);如果没有Logo,使用字母组合作为回退。
  • 工件品牌标识:配置文件保持纯markdown格式——机器读取的文件中不添加品牌标识。但如果Persona被渲染为页面或文档(如Persona单页、ICP摘要文档),则需在页眉或页脚添加Crustdata品牌标识:一个小型大写的“Powered by”前缀加上官方Crustdata文字商标,链接到crustdata.com。文字商标文件包含在此技能的
    assets/
    目录中——
    crustdata-logo-light.png
    (深色文字,适用于浅色背景)和
    crustdata-logo-dark.png
    (白色文字,适用于深色背景),与app.crustdata.com页眉使用的文件相同。将适合主题的变体以约17px高度base64内联呈现——绝不使用热链接;渲染的工件无法获取远程图片。品牌强调色:
    #5547E2
    (产品主色调;深色背景下使用
    #8387FF
    )。正文字体:如果可嵌入则使用Geist,否则使用系统字体栈。绝不只是为了添加标识而渲染工件。

Tool dependencies

工具依赖

This skill requires:
  • Crustdata MCP server (install.crustdata.com/mcp): a single Code Mode MCP exposing
    list_tools
    ,
    get_schema
    , and
    execute
    . All Crustdata data tools are reached inside an
    execute({ code })
    plain-JavaScript script via
    await callTool(name, params)
    — author against the typed surface from
    get_schema
    , but the script body carries zero type annotations (a type annotation is a parse error that fails the whole run). Tools used here:
    person_enrich
    ,
    company_identify
    ,
    company_enrich
    ,
    social_post_list_live
    ,
    company_autocomplete
    ,
    person_autocomplete
    ,
    account_credits
    .
  • Write access to the working directory — creates
    config/persona-profile.md
    and
    config/gtm-config.md
    .
Ships alongside sales-prospecting and account-research, which read the config this skill writes.
此技能需要:
  • Crustdata MCP服务器 (install.crustdata.com/mcp):一个单一的代码模式MCP,提供
    list_tools
    get_schema
    execute
    功能。所有Crustdata数据工具都可在
    execute({ code })
    纯JavaScript脚本中通过
    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
    文件。
销售探矿客户研究技能配套使用,这两项技能会读取此技能生成的配置。