icp-builder
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseICP Builder
ICP构建工具
One LinkedIn URL in, a working GTM config out. This skill enriches the user's own
profile via Crustdata and writes + —
the files sales-prospecting and account-research read at startup.
config/persona-profile.mdconfig/gtm-config.mdThree steps, always in this order:
- Stack (optional, fully skippable): which tools they use.
- Persona: one LinkedIn URL; Crustdata turns it into who they are, what they sell, an inferred ICP, and their writing voice.
- 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.mdconfig/gtm-config.md始终按照以下三个步骤执行:
- 工具栈(可选,可完全跳过):用户使用的工具。
- Persona生成:输入一个LinkedIn URL;Crustdata会将其转化为用户身份、公司业务、推断出的ICP以及写作风格。
- 写入配置并移交。
绝不询问用户额外信息。不要问“你们销售什么”“你的ICP是谁”或“粘贴你的风格邮件”。所有这些信息都可以从LinkedIn URL和用户发布的内容中获取。URL就是全部所需信息。
Step 0: check for an existing config
步骤0:检查现有配置
If or 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.mdconfig/persona-profile.md如果工作目录中已存在或文件,请读取它们,用两行内容总结现有信息,然后询问用户是要刷新整个Persona还是更新特定字段。绝不要静默覆盖用户已修正的配置。刷新时,保留现有工具栈条目不变,除非用户要求更改,否则不要重新询问工具栈问题。缺失文件是正常情况——此技能会创建这些文件。
config/gtm-config.mdconfig/persona-profile.mdStep 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 everywhere and move on.
none- 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 = 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
以一句简短的欢迎语开场,然后提出一个可选问题:**你使用哪些工具?**快速遍历各个工具类别;用户可以说出工具名称或选择跳过。如果用户跳过整个问题,就在所有类别中填写并继续。
none- 数据提供商 —— Crustdata,这些技能依赖的数据源(作为连接器添加;如果未连接,请使用下文的无数据回退方案)
- CRM —— 或跳过
- 日历 —— 或跳过
- 邮件 —— 或跳过
- 通话记录器 —— 或跳过
- 序列器 —— 或跳过
- 团队聊天工具 —— 或跳过
此步骤规则:
- 绝不要从已连接的连接器推断工具栈。已连接的连接器不代表用户的选择。要么询问用户,要么填写。
none - 每个类别都可跳过;绝不施压或重新询问用户拒绝回答的工具类别。
- 跳过的类别 = 配置中填写= 下游技能仅运行该类别的草稿模式:生成草稿和CSV导出,而非推送到工具(如“为你的序列器导出CSV”“记录到文件而非CRM”)。
none
Step 2: LinkedIn URL → persona
步骤2:LinkedIn URL → Persona
Ask for one thing: their LinkedIn URL. Then build the persona in one
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 .
executeparallelMapEvery script must open with a source-labeled query comment ( or
) — scripts without one are rejected before running, at zero spend.
// 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,
};Notes on this script:
- Never set on
preview: true. It is plan-dependent and returns a 400 on some accounts. The flow must never depend on it; base cost is 1 credit anyway.person_enrich - Keep fields to
person_enrich+basic_profile+experience. Withoutsocial_handlesin the whitelist thesocial_handlesaccessor readsprofileUrland the posts pull falls back to the raw user-typed URL. Some groups (undefined,certifications,honors) 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.updated_at - Response paths differ from filter paths: the title lives at
, the current employer at
basic_profile.current_title, the canonical profile URL atexperience.employment_details.current[].name(thesocial_handles.professional_network_identifier.profile_urlaccessor reads it for you).profileUrl
仅要求用户提供一项信息:他们的LinkedIn URL。然后通过一个脚本生成Persona。首先进行人物信息查询,公司信息丰富和帖子提取都依赖于人物信息,但彼此独立,因此使用并行处理这两项任务。
executeparallelMap每个脚本必须以带来源标签的查询注释开头( 或 )——没有该注释的脚本会在运行前被拒绝,且不产生费用。
// 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。这取决于订阅计划,在某些账户上会返回400错误。流程绝不能依赖此设置;基础费用仅为1个积分。preview: true - 将的字段限制为
person_enrich+basic_profile+experience。如果白名单中没有social_handles,social_handles访问器会读取为profileUrl,帖子提取会回退到用户输入的原始URL。某些字段组(如undefined、certifications、honors)受订阅计划限制——包含受限字段组会导致整个调用失败并返回403错误,且会指出具体字段。如果发生这种情况,请删除该字段并重试。updated_at - 响应路径与过滤路径不同:职位头衔位于,当前雇主位于
basic_profile.current_title,标准资料URL位于experience.employment_details.current[].name(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. is free and fuzzy — one identifier can return several
companies — so pick the top match, then enrich by id with
. That is the cheapest exact path: free identify + 2 credits for
exactly one enriched match.
company_identifyconfidence_scoreexact_match: truejs
// 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 on — it is plan-gated and 403s the
whole call.
social_profilescompany_identify如果当前职位信息中没有公司ID,请先通过域名(或名称)解析公司。是免费的模糊匹配工具——一个标识符可能返回多个公司——因此选择最高的匹配项,然后通过ID调用并设置。这是最便宜的精确路径:免费识别 + 2个积分获取一个精确匹配的丰富信息。
company_identifyconfidence_scorecompany_enrichexact_match: truejs
// 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"]);不要在中设置字段——这受订阅计划限制,会导致整个调用返回403错误。
company_identifysocial_profilesDerive the persona from the returned data
从返回数据中生成Persona
- Identity: name, title, company, tenure (from ), one-line background from the past roles.
start_date - Company & what we sell: product and category from +
basic_info; keywords to monitor from the company description and the user's post topics.taxonomy - 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 — it is a hypothesis for the user to correct, not a fact.
inferredWrite 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
: ,
, , , , ,
, , , . When unsure, resolve through
rather than guessing.
experience.employment_details.current.seniority_levelEntry LevelEntry Level ManagerExperienced ManagerSeniorDirectorVice PresidentCXOOwner / PartnerIn TrainingStrategicperson_autocomplete从公司业务和典型客户群体推断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_levelEntry LevelEntry Level ManagerExperienced ManagerSeniorDirectorVice PresidentCXOOwner / PartnerIn TrainingStrategicperson_autocompleteAccuracy 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. is the full
persona; repeats the Company / ICP / Voice essentials plus the
stack so every skill finds them in one read.
config/persona-profile.mdconfig/gtm-config.md在工作目录中写入两个文件。是完整的Persona资料;重复公司/ICP/写作风格的核心信息以及工具栈,以便所有技能只需读取一次即可获取所需信息。
config/persona-profile.mdconfig/gtm-config.mdconfig/persona-profile.md
config/persona-profile.mdconfig/persona-profile.md
config/persona-profile.mdmarkdown
undefinedmarkdown
undefinedPersona 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>
undefinedconfig/gtm-config.md
config/gtm-config.mdconfig/gtm-config.md
config/gtm-config.mdmarkdown
undefinedmarkdown
undefinedGTM 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- Data provider: crustdata | none
- CRM: <tool> | none
- Calendar: <tool> | none
- Email: <tool> | none
- Call recorder: <tool> | none
- Sequencer: <tool> | none
- Team chat: <tool> | none
noneWhat 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.
undefinedHand 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
费用
- with
person_enrich+basic_profile+experience: 1 credit.social_handles - ,
company_identify,company_autocomplete: free.person_autocomplete - by id with
company_enrich: 2 credits for one match.exact_match: true - : 1 credit per post — always set
social_post_list_livedeliberately (10 is plenty for voice).limit - Typical full run: about 13 credits. Every response carries
executeandcredits;credits_remaining(free) reports the balance.account_credits
- (包含
person_enrich+basic_profile+experience):1个积分。social_handles - 、
company_identify、company_autocomplete:免费。person_autocomplete - 通过ID调用并设置
company_enrich:2个积分获取一个匹配项。exact_match: true - :每个帖子1个积分——始终明确设置
social_post_list_live(10个帖子足以分析写作风格)。limit - 典型完整流程:约13个积分。每个响应都会包含
execute和credits;credits_remaining(免费)可查询余额。account_credits
Error handling
错误处理
- Branch on in every script. A failed call does not abort the script; an unchecked failure silently proceeds on empty data and looks like "no results".
r.ok - returns no match → confirm the URL with the user (typo, vanity slug change), then use the no-data fallback.
person_enrich - 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 - 返回无匹配 → 与用户确认URL是否正确(拼写错误、自定义链接变更),然后使用无数据回退方案。
person_enrich - 返回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 for skipped tools. Never invent stack or persona details.
none - 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
stroke. No emojis in artifact UI.
currentColor - The persona's own photo is free too — rides in the
basic_profile.profile_picture_permalinkgroup the Step 2basic_profilealready returns. A persona one-pager is about a person; base64-inline the photo (sameperson_enrichrule) with a monogram fallback.binary/octet-stream - The company logo is free — use it on a rendered persona page. comes from the free
basic_info.logo_permalinkand from thecompany_identifyyou already run for the persona. Base64-inline it as acompany_enrichURI (the media CDN serves these asdata:image/jpeg;base64,..., so a remotebinary/octet-streamrenders blank); monogram fallback when there's none.<img src> - 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/(dark text, for light backgrounds) andcrustdata-logo-light.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:crustdata-logo-dark.png(the product primary;#5547E2on dark grounds). Body font: Geist when embeddable, else the system stack. Never render an artifact just to carry the mark.#8387FF
- 先欢迎,再提出一个可选工具栈问题,URL就是全部所需信息。
- 绝不要从已连接的连接器推断工具栈。要么询问用户,要么填写。
none - 绝不要询问用户销售什么、他们的ICP是什么或写作风格如何——从数据源推导。如果丰富信息有限,仅获取2-3行信息,绝不进行完整问卷调查。
- 在写入文件前向用户展示Persona供其修正。
- 为推断内容添加标签。跳过的工具填写。绝不要编造工具栈或Persona的细节。
none - 写作风格始终遵循简洁规则:不使用破折号;绝不使用“delve”“leverage”或“streamline”;无冗余内容;像同事一样写作。
- 缺失配置绝不会阻止任何操作:此技能会创建配置,下游技能在发现配置缺失时会引导用户回到此处。
- 根据内容调整布局——绝不隐藏任何信息。品牌系统是固定的,但布局可以调整。如果实际内容无法适配——长公司名或人名、12字的职位头衔、200行数据——请调整布局,而非修改内容:让卡片扩展、换行而非截断、改为单列、加宽列、提高限制、或为宽内容添加滚动容器。绝不要通过裁剪卡片、省略名称或静默删除行来解决适配问题。如果确实需要设置限制,请在UI中明确说明(如“显示前50条,共214条”),让用户知道他们未看到的内容。在移交前查看渲染输出并修复所有被截断的内容。
- 渲染输出中的图标:使用Lucide(仪表板的图标集),以内联SVG形式呈现,使用描边。在工件UI中不使用表情符号。
currentColor - Persona的个人照片也是免费的——包含在步骤2的
basic_profile.profile_picture_permalink返回的person_enrich字段组中。Persona单页是关于个人的;将照片以base64内联形式呈现(遵循相同的basic_profile规则),并提供字母组合作为回退方案。binary/octet-stream - 公司Logo是免费的——在渲染的Persona页面中使用。来自免费的
basic_info.logo_permalink以及为生成Persona而运行的company_identify。将其以company_enrichURI的形式base64内联呈现(媒体CDN以data:image/jpeg;base64,...形式提供这些资源,因此远程binary/octet-stream会显示空白);如果没有Logo,使用字母组合作为回退。<img src> - 工件品牌标识:配置文件保持纯markdown格式——机器读取的文件中不添加品牌标识。但如果Persona被渲染为页面或文档(如Persona单页、ICP摘要文档),则需在页眉或页脚添加Crustdata品牌标识:一个小型大写的“Powered by”前缀加上官方Crustdata文字商标,链接到crustdata.com。文字商标文件包含在此技能的目录中——
assets/(深色文字,适用于浅色背景)和crustdata-logo-light.png(白色文字,适用于深色背景),与app.crustdata.com页眉使用的文件相同。将适合主题的变体以约17px高度base64内联呈现——绝不使用热链接;渲染的工件无法获取远程图片。品牌强调色:crustdata-logo-dark.png(产品主色调;深色背景下使用#5547E2)。正文字体:如果可嵌入则使用Geist,否则使用系统字体栈。绝不只是为了添加标识而渲染工件。#8387FF
Tool dependencies
工具依赖
This skill requires:
- Crustdata MCP server (install.crustdata.com/mcp):
a single Code Mode MCP exposing ,
list_tools, andget_schema. All Crustdata data tools are reached inside anexecuteplain-JavaScript script viaexecute({ code })— author against the typed surface fromawait callTool(name, params), but the script body carries zero type annotations (a type annotation is a parse error that fails the whole run). Tools used here:get_schema,person_enrich,company_identify,company_enrich,social_post_list_live,company_autocomplete,person_autocomplete.account_credits - Write access to the working directory — creates and
config/persona-profile.md.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功能。所有Crustdata数据工具都可在execute纯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
与销售探矿和客户研究技能配套使用,这两项技能会读取此技能生成的配置。