meeting-prep
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseMeeting Prep
会议准备
One job: the calls and leads in front of you today. A rep opens this between meetings, so every deliverable is one screen, every claim carries a date, and nothing is invented to fill a section.
Three modes:
- Inbound triage — someone filled a form or emailed in. Is this worth your time?
- Pre-call brief — the daily driver. One screen you can read in the two minutes before a call.
- Call plan — a discovery question plan, or a demo flow built only from stated pains.
Ask which one if it isn't obvious from the ask. "Prep my 2pm" is mode 2. "Score this lead" is mode 1. "Build me a discovery plan" is mode 3.
核心目标:搞定您今日待处理的通话与线索。销售代表会在会议间隙使用该工具,因此所有交付内容均为单屏展示,所有信息均标注日期,绝不编造内容填充板块。
三种模式:
- 入站线索分类 — 有人提交了表单或发送了咨询邮件。这条线索值得您投入时间吗?
- 通话前简报 — 日常核心功能。您可在通话前两分钟内读完的单屏内容。
- 通话计划 — 探索性问题计划,或完全基于潜在客户明确提出的痛点构建的演示流程。
若用户需求不明确,可询问具体模式。“为我的下午2点通话做准备”对应模式2,“为这条线索评分”对应模式1,“为我制定探索计划”对应模式3。
Step 0 — context (never blocks)
步骤0 — 上下文信息(绝不阻塞流程)
- If or
config/gtm-config.mdexist in the working directory, read them: ICP, what you sell, buyer titles, customer list, competitor list, voice. These drive the scoring rubric and the opener's voice.config/persona-profile.md - If they don't exist, ask 1-2 questions inline ("Who do you sell to — industry, size, geo? Which titles buy?") or point at the icp-builder skill to build the config properly. A missing config never blocks a run.
- Optional read-only enrichment: if a CRM, call recorder, or email is connected, a read-only sweep for prior threads, notes, and calls with the same domain sharpens any brief — past objections, who already talked to them, why the last deal stalled. Read only, never write, and never build the deliverable around it: the brief has to stand up on public data alone.
- For "prep my 2pm" with no calendar connected, just ask which meeting: company, and who you're meeting.
- 若工作目录中存在或
config/gtm-config.md文件,请读取:ICP、您的销售产品、买方职位、客户列表、竞争对手列表、沟通风格。这些内容将驱动评分规则和开场话术的风格。config/persona-profile.md - 若上述文件不存在,可在线询问1-2个问题(“您的目标客户是哪些行业、规模、地域?哪些职位的人员会采购?”),或引导使用icp-builder技能来正确配置相关信息。缺少配置文件绝不阻塞工具运行。
- 可选只读增强:若已连接CRM、通话记录器或邮箱,可只读扫描同一域名的过往沟通线程、笔记和通话记录,优化简报内容——比如过往异议、已对接人员、上一次交易停滞的原因。仅读取,绝不写入,且简报内容不能完全依赖这些数据:必须仅基于公开数据即可独立成立。
- 若未连接日历,用户提出“为我的下午2点通话做准备”时,只需询问具体会议信息:公司名称及对接人。
Code Mode ground rules (read once, apply everywhere)
代码模式基本规则(阅读一次,全程适用)
All Crustdata calls run inside the tool of the Crustdata MCP server (install.crustdata.com/mcp) as a plain-JavaScript script.
execute- Plain JavaScript only. Author against the typed surface from , but the script body carries zero type annotations — a
get_schema,: Type, or generic is a parse error that fails the whole run before any spend.as - Every script opens with a source-labeled comment: for the literal ask,
// user query: ...for a derived step. Scripts without one are rejected before running.// model query: ... - One I/O primitive: →
const r = await callTool(name, params)or{ ok: true, data }. Always branch on{ ok: false, status, errorType, message }— a failed call does not abort the script, so an unchecked failure silently proceeds on empty data and looks like "no signal".r.ok - is a response whitelist. The result carries only the groups you list; an omitted group reads as
fieldslater and looks like missing data. List every group you read.undefined - Return the smallest projection. Only what the script returns reaches the model — map to compact rows, never raw profiles.
- Fan out independent calls with ; batch first with
await parallelMap(items, fn). Never parallelize cursor pagination or dependent stages — identify → enrich stays sequential, and you parallelize within a stage.chunk(list, 25) - is free but fuzzy. One domain or name can match several companies. Take the top
company_identifymatch. Do not projectconfidence_scoreon identify — it is plan-gated and 403s the whole call.social_profiles - Plan-gated projections fail the whole call with a 403 naming the field. Never project or
professional_network.followersonmetadata, orperson_search/certifications/honorsonupdated_at.person_enrich - Filter paths ≠ response paths. You filter ; the response key is
experience.employment_details.current.company_name. The LinkedIn URL returns at...current[].name— use thesocial_handles.professional_network_identifier.profile_urlaccessor.profileUrl(p) - Categorical values are closed sets. A plausible-but-wrong value silently returns zero rows. Resolve with /
company_autocomplete(free) before filtering on industries, funding round types, or seniority.person_autocomplete - Zero results ≠ no signal. Read the in the execute response before telling a rep an account is quiet.
trajectory
所有Crustdata调用均在Crustdata MCP服务器(install.crustdata.com/mcp)的工具内,以纯JavaScript脚本运行。
execute- 仅使用纯JavaScript。基于返回的类型接口编写,但脚本主体不能包含任何类型注解——
get_schema、: Type或泛型会导致解析错误,使整个运行提前失败。as - 每个脚本开头必须带来源标注注释:对应用户的原始请求,
// 用户查询: ...对应衍生步骤。无该注释的脚本会被直接拒绝运行。// 模型查询: ... - 仅使用一种I/O原语:→ 返回
const r = await callTool(name, params)或{ ok: true, data }。必须始终根据{ ok: false, status, errorType, message }进行分支处理——调用失败不会终止脚本,若未检查失败情况,会基于空数据继续运行,导致“无信号”的假象。r.ok - 是响应白名单。结果仅包含您列出的分组;未列出的分组后续会被视为
fields,看起来像数据缺失。请列出所有需要读取的分组。undefined - 返回最小化的结果投影。只有脚本返回的内容会传递给模型——映射为紧凑的行数据,绝不返回原始配置文件。
- 使用并行处理独立调用;先使用
await parallelMap(items, fn)进行批量处理。绝不要并行处理游标分页或依赖阶段——识别→增强需保持顺序,仅可在同一阶段内并行处理。chunk(list, 25) - 免费但结果模糊。一个域名或公司名称可能匹配多个公司。请选择
company_identify最高的匹配结果。识别时不要获取confidence_score——该字段受计划限制,会导致整个调用返回403错误。social_profiles - 受计划限制的字段会导致整个调用返回403错误并标注字段名。绝不要在中获取
person_search或professional_network.followers,也不要在metadata中获取person_enrich/certifications/honors。updated_at - 过滤路径≠响应路径。您可以过滤;但响应键为
experience.employment_details.current.company_name。LinkedIn URL返回路径为...current[].name——请使用social_handles.professional_network_identifier.profile_url访问器获取。profileUrl(p) - 分类值为封闭集合。看似合理但错误的值会静默返回0条结果。在过滤行业、融资轮次或职级前,请使用/
company_autocomplete(免费)确认值的正确性。person_autocomplete - 零结果≠无信号。在告知销售代表客户无动态前,请先查看execute响应中的字段。
trajectory
Mode 1 — Inbound triage
模式1 — 入站线索分类
Input: whatever the form or email gave you — name, work email, company or domain, self-reported title, and often a free-text "what are you looking for". A LinkedIn URL if you're lucky.
输入信息:表单或邮件提供的内容——姓名、工作邮箱、公司或域名、自报职位,通常还有自由文本“您的需求是什么”。若幸运的话,还会有LinkedIn URL。
Step 1 — resolve and read the firmographics (cheap, and it batches)
步骤1 — 解析并读取企业基本信息(低成本,支持批量处理)
company_identifycompany_searchcompany_enrichjs
// user query: triage today's inbound — 6 form fills
const leads = inputs.leads; // [{ email, name, title, domain }, ...]
const batches = chunk(leads.map(l => l.domain).filter(Boolean), 25);
const identified = await parallelMap(batches, async (batch) => {
const r = await callTool("company_identify", {
domains: batch, // ONE identifier type per call
fields: ["crustdata_company_id", "basic_info"],
});
return r.ok ? r.data : batch.map(d => ({ matched_on: d, matches: [], error: r.message }));
});
const rows = identified.flat();
const unresolved = rows.filter(m => !m.matches?.length).map(m => m.matched_on);
const byDomain = {};
for (const m of rows) {
const top = (m.matches ?? []).slice().sort((a, b) => (b.confidence_score ?? 0) - (a.confidence_score ?? 0))[0];
const cd = top?.company_data;
if (cd) byDomain[m.matched_on] = cd.basic_info?.crustdata_company_id ?? cd.crustdata_company_id;
}
const ids = Object.values(byDomain).filter(Boolean);
checkpoint({ ids, unresolved });
const r = await callTool("company_search", {
filters: in_("crustdata_company_id", ids),
fields: ["crustdata_company_id", "basic_info", "headcount", "funding", "locations"],
limit: ids.length,
});
if (!r.ok) return { error: r.message, unresolved };
return {
unresolved, // personal-email leads and dead domains — surfaced, never silently dropped
companies: r.data.companies.map(c => ({
id: c.crustdata_company_id,
name: c.basic_info?.name,
domain: c.basic_info?.primary_domain,
industries: c.basic_info?.industries,
hc: c.headcount?.total,
growth12m: c.headcount?.growth_percent?.["12m"],
lastRound: c.funding?.last_round_type,
lastRaise: c.funding?.last_fundraise_date,
country: c.locations?.country,
})),
};locations.countryUSA"USA"company_identifycompany_searchcompany_enrichjs
// 用户查询:分类今日入站线索——6条表单提交
const leads = inputs.leads; // [{ email, name, title, domain }, ...]
const batches = chunk(leads.map(l => l.domain).filter(Boolean), 25);
const identified = await parallelMap(batches, async (batch) => {
const r = await callTool("company_identify", {
domains: batch, // 每次调用仅使用一种标识符类型
fields: ["crustdata_company_id", "basic_info"],
});
return r.ok ? r.data : batch.map(d => ({ matched_on: d, matches: [], error: r.message }));
});
const rows = identified.flat();
const unresolved = rows.filter(m => !m.matches?.length).map(m => m.matched_on);
const byDomain = {};
for (const m of rows) {
const top = (m.matches ?? []).slice().sort((a, b) => (b.confidence_score ?? 0) - (a.confidence_score ?? 0))[0];
const cd = top?.company_data;
if (cd) byDomain[m.matched_on] = cd.basic_info?.crustdata_company_id ?? cd.crustdata_company_id;
}
const ids = Object.values(byDomain).filter(Boolean);
checkpoint({ ids, unresolved });
const r = await callTool("company_search", {
filters: in_("crustdata_company_id", ids),
fields: ["crustdata_company_id", "basic_info", "headcount", "funding", "locations"],
limit: ids.length,
});
if (!r.ok) return { error: r.message, unresolved };
return {
unresolved, // 个人邮箱线索和无效域名——需明确展示,绝不静默丢弃
companies: r.data.companies.map(c => ({
id: c.crustdata_company_id,
name: c.basic_info?.name,
domain: c.basic_info?.primary_domain,
industries: c.basic_info?.industries,
hc: c.headcount?.total,
growth12m: c.headcount?.growth_percent?.["12m"],
lastRound: c.funding?.last_round_type,
lastRaise: c.funding?.last_fundraise_date,
country: c.locations?.country,
})),
};locations.countryUSA"USA"Step 2 — timing signals, near-free
步骤2 — 时机信号(近乎免费)
One aggregation with gives open-role counts across every resolved account at once. A company hiring into the function you sell to has budget moving.
job_searchlimit: 0js
// model query: open-role counts per inbound account (timing signal, counts only)
const r = await callTool("job_search", {
filters: in_("company.basic_info.company_id", inputs.ids),
aggregations: [{ type: "group_by", field: "company.basic_info.crustdata_company_id", agg: "count", size: 100 }],
limit: 0,
});
if (!r.ok) return { error: r.message };
return r.data.aggregations;一次的聚合调用,可一次性获取所有已解析客户的空缺职位数量。若客户正在招聘您所销售产品对应的岗位,说明他们有相关预算正在流动。
limit: 0job_searchjs
// 模型查询:每个入站客户的空缺职位数量(时机信号,仅统计数量)
const r = await callTool("job_search", {
filters: in_("company.basic_info.company_id", inputs.ids),
aggregations: [{ type: "group_by", field: "company.basic_info.crustdata_company_id", agg: "count", size: 100 }],
limit: 0,
});
if (!r.ok) return { error: r.message };
return r.data.aggregations;Step 3 — the person
步骤3 — 联系人信息
- LinkedIn URL given → (1 credit) with
person_enrich. Real title, real tenure, real employer. Worth it for a single high-stakes lead; skip it for a batch of 20 unless the user asks.fields: ["basic_profile", "experience", "social_handles"] - No URL → use the self-reported title and label it self-reported in the output. Forms lie in both directions.
- Match the title against the ICP's buyer titles from config. The normalized seniority band rides inside the group that both
experienceandperson_enrichalready return — readperson_search, don't buy a search for it. When you filter on it, the value vocabulary is a closed, autocomplete-first set:cur.seniority_level,Entry Level,Entry Level Manager,Experienced Manager,Senior,Director,Vice President,CXO,Owner / Partner,In Training.Strategic
- 提供LinkedIn URL → 使用(1积分),
person_enrich。可获取真实职位、任职时长、雇主信息。对于单个高价值线索值得投入;若为20条线索的批量处理,除非用户要求,否则跳过该步骤。fields: ["basic_profile", "experience", "social_handles"] - 无LinkedIn URL → 使用自报职位,并在输出中标注“自报”。表单信息可能存在夸大或隐瞒。
- 将职位与配置文件中的ICP买方职位进行匹配。标准化职级信息包含在和
person_enrich均会返回的person_search分组中——读取experience即可,无需额外搜索。过滤职级时,值的词汇为封闭集合,需先通过自动补全确认:cur.seniority_level、Entry Level、Entry Level Manager、Experienced Manager、Senior、Director、Vice President、CXO、Owner / Partner、In Training。Strategic
Step 4 — score it, and show your work
步骤4 — 评分并展示依据
A transparent rubric out of 7. Never output a "close probability %" — without your closed-won history there is nothing to calibrate it against, and a made-up percentage is worse than no number.
| Component | Points | What earns them |
|---|---|---|
| Firmographic fit | 0-3 | Industry, size band, and geography vs the ICP — one point each |
| Buyer-title match | 0-2 | Named buyer title or seniority band = 2; adjacent or influencer = 1; unrelated or well below the band = 0 |
| Timing | 0-2 | One point each, capped at 2: funding inside 6 months, 12-month headcount growth above 20%, open roles in the function you sell to, a relevant news item or post inside 60 days |
This mode is for leads that came to you today. To rank your own book, territory, or a pasted account CSV, use sales-prospecting's rank-my-accounts recipe instead.
Hard disqualifiers override the score entirely. Any one of these means decline, whatever the points say: a competitor (from the config's competitor list if it carries one — if it doesn't, ask for your competitors in the same inline question as the ICP, before applying this disqualifier), an existing customer (route to the account owner instead), a student or job seeker, a personal email address with no resolvable company, a geography you can't serve, or headcount below your floor.
Verdict tiers (chat output; in a rendered artifact these become coloured pills, never emoji):
- 🔥 Route now — fit ≥ 2, title ≥ 1, at least one timing point, no disqualifier
- 🟡 Nurture — real company, wrong moment or wrong person. Say what would flip it
- ⚪ Decline — no fit, or a disqualifier fired. Name which one
Every row shows the reasons and dates the evidence ("Series B, 2026-04-12", "17 open roles, 4 in data engineering"). A signal without a date is a signal a rep can't use.
采用透明的7分制评分规则。绝不要输出“成交概率%”——没有您的成交历史数据无法校准该数值,编造的百分比比没有数值更糟糕。
| 评分项 | 分值 | 得分标准 |
|---|---|---|
| 企业画像匹配度 | 0-3 | 行业、规模区间、地域与ICP匹配——每项匹配得1分 |
| 买方职位匹配度 | 0-2 | 完全匹配买方职位或职级得2分;匹配相关职位或影响者得1分;不匹配或职级远低于标准得0分 |
| 时机信号 | 0-2 | 每项得1分,最高2分:6个月内完成融资、12个月员工增长率超过20%、有对应岗位的空缺职位、60天内有相关新闻或帖子 |
该模式适用于今日主动联系您的线索。若要对您自己的客户库、销售区域或粘贴的客户CSV进行排名,请使用sales-prospecting技能中的“rank-my-accounts”方案。
硬性 disqualifiers(淘汰规则)会直接覆盖评分结果。只要满足以下任一条件,无论得分多少均需拒绝:竞争对手(若配置文件中有竞争对手列表——若没有,在询问ICP的同时询问竞争对手信息,再应用该规则)、现有客户(转交给对应客户负责人)、学生或求职者、无对应可解析公司的个人邮箱、您无法覆盖的地域、员工数量低于您的最低标准。
结论等级(聊天输出;在渲染的成果中会变为彩色标签,绝不使用表情符号):
- 🔥 立即跟进 — 匹配度≥2,职位匹配≥1,至少有一个时机信号,无淘汰项
- 🟡 培育跟进 — 真实公司,但时机不对或对接人不合适。说明可触发跟进的条件
- ⚪ 拒绝 — 不匹配,或触发了淘汰规则。明确说明原因
每一行均需展示评分依据,并标注证据日期(如“B轮融资,2026-04-12”、“17个空缺职位,其中4个为数据工程岗”)。无日期的信号对销售代表毫无用处。
Batch mode
批量模式
Rank a whole day's inbound in one table: Lead — Company — Fit — Title — Timing — Score — Verdict — Why now. Sort by score, group by verdict. Put unresolved domains in their own short list at the bottom with the reason (personal email, dead domain, no match) so nothing disappears. Then: "here are the three to call back today."
在一张表格中对单日所有入站线索进行排名:线索——公司——匹配度——职位——时机信号——得分——结论——跟进理由。按得分排序,按结论分组。将未解析的域名单独放在表格底部的短列表中,并说明原因(个人邮箱、无效域名、无匹配结果),确保无信息丢失。最后提示:“以下是今日需回电的3条线索”。
Mode 2 — Pre-call brief
模式2 — 通话前简报
The daily driver. Input: company (name or domain) and who you're meeting (name, ideally a LinkedIn URL).
日常核心功能。输入信息:公司(名称或域名)及对接人(姓名,最好提供LinkedIn URL)。
The whole brief in one script
单脚本生成完整简报
Identify first (free, fuzzy — take the top confidence match), then fan out the four independent pulls. Person enrichment doesn't depend on the company, and neither does the person's post history, so they run in the same stage.
js
// user query: prep my 2pm with acme.com — meeting their VP Data
const idr = await callTool("company_identify", { domains: ["acme.com"] });
if (!idr.ok) return { error: idr.message };
const top = (idr.data[0]?.matches ?? [])
.slice().sort((a, b) => (b.confidence_score ?? 0) - (a.confidence_score ?? 0))[0];
if (!top) return { error: "no company match for acme.com" };
const cd0 = top.company_data ?? {};
const companyId = cd0.basic_info?.crustdata_company_id ?? cd0.crustdata_company_id;
const logo = cd0.basic_info?.logo_permalink; // free, use it if the brief gets rendered
const calls = [
{ key: "company", name: "company_enrich", params: {
crustdata_company_ids: [companyId], exact_match: true,
fields: ["basic_info", "headcount", "funding", "news"] } }, // 2 cr, exactly one match
{ key: "jobs", name: "job_search", params: {
filters: { op: "and", conditions: [
{ field: "company.basic_info.primary_domain", type: "=", value: "acme.com" } ]},
sorts: [{ field: "metadata.date_added", order: "desc" }], limit: 15 } },
{ key: "person", name: "person_enrich", params: {
professional_network_profile_urls: ["https://www.linkedin.com/in/example"],
fields: ["basic_profile", "experience", "education", "social_handles"] } }, // 1 cr
{ key: "posts", name: "social_post_list_live", params: {
professional_network_profile_url: "https://www.linkedin.com/in/example",
limit: 5 } }, // 1 cr PER POST — cap it
];
const out = await parallelMap(calls, async (c) => ({ key: c.key, r: await callTool(c.name, c.params) }));
checkpoint(out); // ~8-10 credits already spent — bank them before shaping, so a shaping error can't discard them
const get = (k) => out.find(x => x.key === k)?.r;
const ce = get("company");
const cd = ce?.ok ? (ce.data[0]?.matches?.[0]?.company_data ?? {}) : {};
const cutoff = new Date(Date.now() - 60 * 24 * 3600 * 1000).toISOString().slice(0, 10);
const news = (firstArray(cd.news) ?? [])
.map(n => ({ title: n.article_title, url: n.article_url, date: n.article_publish_date }))
.filter(n => n.date && n.date >= cutoff)
.slice(0, 6);
const jr = get("jobs");
const pr = get("person");
const person = pr?.ok ? (pr.data[0]?.matches?.[0]?.person_data ?? {}) : {};
const cur = person.experience?.employment_details?.current?.[0] ?? {};
const posts = get("posts")?.ok
? (get("posts").data.posts ?? []).map(p => ({
date: p.date_posted, text: (p.text ?? "").slice(0, 280),
reactions: p.engagement?.total_reactions }))
: [];
return {
company: {
name: cd.basic_info?.name,
what: cd.basic_info?.description,
hc: cd.headcount?.total,
growth12m: cd.headcount?.growth_percent?.["12m"],
lastRound: cd.funding?.last_round_type,
lastRaise: cd.funding?.last_fundraise_date,
logo,
},
news,
openRoles: jr?.ok
? { total: jr.data.total_count,
newest: jr.data.job_listings.map(j => ({ title: j.job_details?.title, added: j.metadata?.date_added })) }
: { error: jr?.message },
person: {
name: person.basic_profile?.name,
title: person.basic_profile?.current_title,
company: cur.name,
since: cur.start_date,
past: (person.experience?.employment_details?.past ?? []).slice(0, 4).map(e => ({ company: e.name, title: e.title })),
schools: (person.education?.schools ?? []).map(e => e.school).filter(Boolean).slice(0, 3),
linkedin: profileUrl(person),
photo: person.basic_profile?.profile_picture_permalink, // free
},
posts,
};Notes on this script:
- is a whitelist on both enrich calls — the
fieldsgroup is not returned unless you ask for it, andnewson the person is what gives you shared-school common ground.education - costs 1 credit per post. Five is enough to know what someone is talking about. Say the cost before you raise it.
social_post_list_live - If the person pull returns no match, confirm the URL and run the brief company-only. A thin person block is honest; an invented one gets a rep caught.
- Want last-60-days coverage the group missed? One
newsquery (1 credit). Keep it to one, and discard or label anything older than 60 days.web_search_live
先进行识别(免费,结果模糊——选择置信度最高的匹配结果),然后并行执行四个独立的查询任务。联系人信息增强不依赖公司信息,联系人的帖子历史也不依赖公司信息,因此可在同一阶段并行运行。
js
// 用户查询:为我下午2点与acme.com的通话做准备——对接其数据副总裁
const idr = await callTool("company_identify", { domains: ["acme.com"] });
if (!idr.ok) return { error: idr.message };
const top = (idr.data[0]?.matches ?? [])
.slice().sort((a, b) => (b.confidence_score ?? 0) - (a.confidence_score ?? 0))[0];
if (!top) return { error: "未找到与acme.com匹配的公司" };
const cd0 = top.company_data ?? {};
const companyId = cd0.basic_info?.crustdata_company_id ?? cd0.crustdata_company_id;
const logo = cd0.basic_info?.logo_permalink; // 免费资源,若简报需渲染可使用
const calls = [
{ key: "company", name: "company_enrich", params: {
crustdata_company_ids: [companyId], exact_match: true,
fields: ["basic_info", "headcount", "funding", "news"] } }, // 2积分,精确匹配一条结果
{ key: "jobs", name: "job_search", params: {
filters: { op: "and", conditions: [
{ field: "company.basic_info.primary_domain", type: "=", value: "acme.com" } ]},
sorts: [{ field: "metadata.date_added", order: "desc" }], limit: 15 } },
{ key: "person", name: "person_enrich", params: {
professional_network_profile_urls: ["https://www.linkedin.com/in/example"],
fields: ["basic_profile", "experience", "education", "social_handles"] } }, // 1积分
{ key: "posts", name: "social_post_list_live", params: {
professional_network_profile_url: "https://www.linkedin.com/in/example",
limit: 5 } }, // 每条帖子1积分——需设置上限
];
const out = await parallelMap(calls, async (c) => ({ key: c.key, r: await callTool(c.name, c.params) }));
checkpoint(out); // 已消耗约8-10积分——在整理结果前先记录,避免整理错误导致数据丢失
const get = (k) => out.find(x => x.key === k)?.r;
const ce = get("company");
const cd = ce?.ok ? (ce.data[0]?.matches?.[0]?.company_data ?? {}) : {};
const cutoff = new Date(Date.now() - 60 * 24 * 3600 * 1000).toISOString().slice(0, 10);
const news = (firstArray(cd.news) ?? [])
.map(n => ({ title: n.article_title, url: n.article_url, date: n.article_publish_date }))
.filter(n => n.date && n.date >= cutoff)
.slice(0, 6);
const jr = get("jobs");
const pr = get("person");
const person = pr?.ok ? (pr.data[0]?.matches?.[0]?.person_data ?? {}) : {};
const cur = person.experience?.employment_details?.current?.[0] ?? {};
const posts = get("posts")?.ok
? (get("posts").data.posts ?? []).map(p => ({
date: p.date_posted, text: (p.text ?? "").slice(0, 280),
reactions: p.engagement?.total_reactions }))
: [];
return {
company: {
name: cd.basic_info?.name,
what: cd.basic_info?.description,
hc: cd.headcount?.total,
growth12m: cd.headcount?.growth_percent?.["12m"],
lastRound: cd.funding?.last_round_type,
lastRaise: cd.funding?.last_fundraise_date,
logo,
},
news,
openRoles: jr?.ok
? { total: jr.data.total_count,
newest: jr.data.job_listings.map(j => ({ title: j.job_details?.title, added: j.metadata?.date_added })) }
: { error: jr?.message },
person: {
name: person.basic_profile?.name,
title: person.basic_profile?.current_title,
company: cur.name,
since: cur.start_date,
past: (person.experience?.employment_details?.past ?? []).slice(0, 4).map(e => ({ company: e.name, title: e.title })),
schools: (person.education?.schools ?? []).map(e => e.school).filter(Boolean).slice(0, 3),
linkedin: profileUrl(person),
photo: person.basic_profile?.profile_picture_permalink, // 免费资源
},
posts,
};该脚本注意事项:
- 两次增强调用的均为白名单——除非明确请求,否则不会返回
fields分组;news的person分组可提供共同母校等共鸣点。education - 的成本为每条帖子1积分。5条帖子足以了解联系人的关注点。运行前需告知用户成本。
social_post_list_live - 若联系人查询无匹配结果,请确认URL后,仅基于公司信息生成简报。联系人板块内容单薄是诚实的表现;编造内容会让销售代表陷入尴尬。
- 若想覆盖分组未包含的近60天信息?可调用一次
news查询(1积分)。仅调用一次,并丢弃或标注60天前的内容。web_search_live
The deliverable — one screen, in this order
交付内容——单屏展示,按以下顺序排列
Company block
One line on what they do, then headcount and 12-month growth, last round with its date, 3-5 dated signals (news items with their URLs, the open-roles shape, a funding event). Hiring says more than the press release: 20 open data roles is a data problem; a first security hire is a security budget about to open.
Person block
Role and tenure, career path in one line, their last posts with dates, and common ground worth mentioning — shared employer, shared school, same city, a person you both know. Common ground is optional. A forced one is worse than none.
The part a rep actually reads
- Three talking points. Each is one sentence with its evidence and date attached. Not "they're growing fast" — "headcount up 34% in 12 months and 6 open roles on the data team, so the pipeline they built in 2024 is probably straining."
- One opener. Exactly one, written out as a sentence you could say out loud, referencing something real and dated inside 60 days. If nothing recent exists, say so and lean the opener on role context instead. Never invent a recent event to have something to open with.
- Two discovery questions. Open, specific to this account, derived from the signals above.
- The likely objection and the pre-empt. Name the one objection this person, in this role, at this company size, is most likely to raise, and the sentence that takes it off the table early.
Nothing else. If a section has nothing real in it, cut it and say why it's empty.
公司板块
一行描述公司业务,然后是员工数量和12个月增长率、最近一轮融资及日期、3-5个带日期的信号(带URL的新闻、空缺职位情况、融资事件)。招聘信息比新闻稿更有价值:20个数据岗位空缺意味着存在数据问题;首次招聘安全岗意味着安全预算即将到位。
联系人板块
职位和任职时长、一行概括职业路径、最近的带日期帖子,以及值得提及的共鸣点——共同雇主、共同母校、同一城市、共同认识的人。共鸣点为可选内容。生硬编造的共鸣点不如没有。
销售代表实际关注的部分
- 三个讨论要点。每个要点为一句话,并附带证据和日期。不要写“他们增长迅速”——要写“12个月内员工数量增长34%,且有6个数据团队空缺职位,因此他们2024年搭建的流程可能已不堪重负。”
- 一个开场话术。仅提供一个,写成可直接说出的句子,引用60天内真实且带日期的信息。若近期无相关信息,需说明并基于职位背景撰写开场话术。绝不要编造近期事件来凑开场内容。
- 两个探索性问题。开放式问题,针对该客户定制,基于上述信号推导得出。
- 潜在异议及预应对话术。指出该职位、该公司规模的联系人最可能提出的一个异议,并提供可提前化解该异议的话术。
无其他内容。若某板块无真实信息,直接删除并说明原因。
Batch of briefs
批量简报
Several calls today? Run the identify stage for all of them, then one brief per meeting, in calendar order, each still one screen. State the credit total before starting: a brief runs about 8-10 credits, so five meetings is roughly 50.
今日有多个通话?先对所有公司进行识别,然后按日历顺序生成每个会议的简报,每个简报仍为单屏展示。开始前需告知总积分成本:一份简报约消耗8-10积分,因此5个会议约消耗50积分。
Mode 3 — Call plan
模式3 — 通话计划
Say which kind up front: discovery or demo.
需提前说明计划类型:探索型或演示型。
Discovery plan
探索型计划
- Two or three pain hypotheses, each evidenced. Build them from the mode 2 signals — open roles, funding, growth rate, what they post about. Test a hypothesis cheaply before you build questions on it: count queries with the exact-token operator on job titles and descriptions.
[.]
js
// model query: which pain themes show up in acme.com job posts
const themes = ["data quality", "observability", "migration"];
const brands = ["Snowflake", "dbt", "Airflow"];
const probes = [
...themes.map(t => ({ label: t, cond: { field: "content.description", type: "(.)", value: t } })),
...brands.map(b => ({ label: b, cond: exactToken("content.description", b) })), // [.] for brand names
];
const hits = await parallelMap(probes, async (p) => {
const r = await callTool("job_search", {
filters: { op: "and", conditions: [
{ field: "company.basic_info.primary_domain", type: "=", value: "acme.com" },
p.cond,
]},
limit: 1,
});
return { probe: p.label, postings: r.ok ? r.data.total_count : null, error: r.ok ? undefined : r.message };
});
return hits;(.)[.]exactToken- The question plan: 8-12 open questions, not 30. A rep asks maybe eight questions in a first call. Map each one to a slot in a qualification framework — MEDDICC by default, or SPICED if the user works that way, and ask which if the config doesn't say. Every question carries three things: the framework slot it fills, the hypothesis it tests, and why you're asking it now.
| Slot | Question | Tests |
|---|---|---|
| Metrics | "How are you measuring the cost of that today?" | Pain hypothesis 1 is quantified |
| Pain | "Walk me through what happens when a pipeline breaks at 2am." | Hypothesis 1 is real, not assumed |
| Champion | "Who else feels this every week?" | Multithreading path |
Rules for the questions: open, not leading. Never ask what public data already answered — asking a VP how big their team is when their headcount is on the page wastes the one thing you have. Sequence them: situation, then pain, then impact, then process.
-
The committed next step. Write the exact ask, with a date and a name in it. "Would it make sense to get your data lead on a 30-minute working session next Tuesday?" beats "let's find time."
-
Coverage check. List which framework slots this plan will not fill in one call, so the rep knows what's left for call two rather than discovering it at forecast time.
- 2-3个带证据的痛点假设。基于模式2的信号构建——空缺职位、融资、增长率、联系人发帖内容。在基于假设构建问题前,可通过职位名称和描述中的精确令牌运算符进行低成本验证。
[.]
js
// 模型查询:acme.com的职位帖子中体现了哪些痛点主题
const themes = ["data quality", "observability", "migration"];
const brands = ["Snowflake", "dbt", "Airflow"];
const probes = [
...themes.map(t => ({ label: t, cond: { field: "content.description", type: "(.)", value: t } })),
...brands.map(b => ({ label: b, cond: exactToken("content.description", b) })), // 品牌名称使用[.]
];
const hits = await parallelMap(probes, async (p) => {
const r = await callTool("job_search", {
filters: { op: "and", conditions: [
{ field: "company.basic_info.primary_domain", type: "=", value: "acme.com" },
p.cond,
]},
limit: 1,
});
return { probe: p.label, postings: r.ok ? r.data.total_count : null, error: r.ok ? undefined : r.message };
});
return hits;(.)[.]exactToken- 问题计划:8-12个开放式问题,而非30个。销售代表在首次通话中可能仅会问8个问题。将每个问题映射到资格审核框架的对应模块——默认使用MEDDICC,若用户使用SPICED则采用该框架,若配置文件未说明需询问用户。每个问题需包含三个要素:对应的框架模块、验证的假设、当前询问该问题的原因。
| 框架模块 | 问题 | 验证内容 |
|---|---|---|
| Metrics(指标) | “您目前如何衡量该问题的成本?” | 验证痛点假设1是否可量化 |
| Pain(痛点) | “请描述一下凌晨2点流程中断时的情况。” | 验证假设1是否真实存在,而非主观臆断 |
| Champion(支持者) | “还有哪些人每周都会遇到这个问题?” | 验证多线程跟进路径 |
问题规则:开放式,而非诱导式。绝不要询问公开数据已能回答的问题——当员工数量已明确时,询问副总裁团队规模会浪费宝贵的通话时间。按顺序提问:先了解现状,再询问痛点,然后是影响,最后是流程。
-
明确的下一步行动。写出具体的请求,包含日期和姓名。“下周二安排一次30分钟的工作会议,邀请您的数据负责人参加,您觉得可行吗?”比“找个时间再聊”效果更好。
-
覆盖范围检查。列出该计划在单次通话中无法覆盖的框架模块,让销售代表清楚第二次通话需补充的内容,而非在预测阶段才发现遗漏。
Demo flow
演示流程
Built only from pains the prospect actually stated. Sources for "stated": the user pastes their discovery notes, or the call plan follows a discovery call in the same session, or a connected recorder or CRM has the notes (read-only). Public signals generate hypotheses, never stated pains — keep the two apart and label them.
If discovery is thin, say so before you build anything. "You have one stated pain and two hypotheses. This demo will be generic. Want a 15-minute discovery block at the top instead?" Then build the shorter version if they still want it.
Structure:
- Recap open — their pains in their own words, read back. Confirm before showing anything.
- One beat per stated pain, in their priority order, not your feature order: tell what you'll show and why it matters to them, show it, then confirm ("does that match how your team would use it?"). A beat with no stated pain behind it gets cut — that is the whole discipline of this mode.
- Close plan — the specific next step, who else needs to see it, what they need to decide, by when.
Explicitly list what you are not showing and why. A demo that skips six features on purpose reads as confidence.
完全基于潜在客户明确提出的痛点构建。“明确提出”的来源:用户粘贴的探索笔记、同一会话中探索性通话后的计划、已连接的记录器或CRM中的笔记(只读)。公开信号仅用于生成假设,绝非明确痛点——需将两者区分并标注。
若探索信息不足,在构建前需告知用户。“您仅提供了一个明确痛点和两个假设。本次演示会较为通用。是否需要在开头增加15分钟的探索环节?”若用户仍坚持演示,则生成简化版本。
结构:
- 开场回顾——用潜在客户自己的语言复述他们的痛点。在展示任何内容前先确认。
- 每个明确痛点对应一个环节,按潜在客户的优先级排序,而非您的功能顺序:说明要展示的内容及对他们的价值,进行展示,然后确认(“这符合您团队的使用场景吗?”)。无明确痛点支撑的环节需删除——这是该模式的核心原则。
- 收尾计划——具体的下一步行动、需参与的其他人员、他们需做出的决策、截止日期。
明确列出不会展示的内容及原因。主动跳过六个功能的演示会显得更有信心。
Rules
规则
- Grounded only. Every claim in a brief traces to a Crustdata result, a URL, or something the user told you. Unclear means ask or omit. Never invent a recent event, a mutual connection, or a pain.
- Dates on everything. A signal with no date is unusable. Anything older than 60 days is labeled background.
- One screen per deliverable. Reps skim. Cut a section rather than padding it.
- No-slop rule on every word you draft — openers, questions, recaps: no em dashes, never "delve" / "leverage" / "streamline", no filler sentences. Write like a colleague who did the homework.
- Self-reported vs verified. Form data is self-reported until Crustdata confirms it. Label it.
- Free data first. Contact enrichment is opt-in and cost-confirmed. has no base charge, billed per contact type returned per matched person (business email 1, personal 2, phone 2), capped at 5 per person. Narrowing
person_contact_enrichsets the worst case but is not a guaranteed cap — a single-tier request has been observed billing the full 5-credit cap and returning all three tiers, so quote 5/person as the ceiling and readfieldsfor the actual spend. Quote the ceiling before running ("emails for 12 leads = at most ~60 credits, go?").credits_remaining - Read-only on connected systems. If a CRM, recorder, or email is connected, read it to sharpen the brief. This skill never writes, logs, sends, or schedules anything.
- Never output a close-probability percentage. Show the rubric score and the reasons instead.
- 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 name, a 12-word title, 40 inbound rows — change the layout: let the card grow, wrap instead of truncating, drop to one column, raise the cap, or give the wide thing its own scroll container. Never clip a card, ellipsis a name, or silently drop rows. State any unavoidable cap in the UI ("showing the top 50 of 214") and look at the rendered output before handing it over.
- Icons in rendered output: Lucide, inlined as SVG with a stroke. No emojis in artifact UI — the 🔥/🟡/⚪ verdict tiers are chat-only; in a rendered brief they become coloured pills.
currentColor - Logos and photos are free — use them in rendered output. comes back from the free
basic_info.logo_permalink;company_identifyrides inside thebasic_profile.profile_picture_permalinkgroupbasic_profilealready returns. Base64-inline both asperson_enrichURIs — the media CDN serves them asdata:image/jpeg;base64,..., so a remotebinary/octet-streamrenders blank. Monogram fallback when an image is missing.<img src> - Artifact branding: briefs and triage tables are chat-native by default — never render an artifact just to render one. But IF the user wants a brief or a triage board as a rendered page or document, 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). Base64-inline the theme-appropriate variant at ~17px height — never hotlink; rendered artifacts cannot fetch remote images. Brand accent:crustdata-logo-dark.png(#5547E2on dark grounds). Body font: Geist when embeddable, else the system stack.#8387FF - Hand off: a hot inbound that needs a full account plan → account-research; more companies like this one → sales-prospecting; no ICP defined yet → icp-builder; who do we already know there → warm-path-workspace; write the touch this brief sets up → sales-outreach.
- 完全基于事实。简报中的所有信息均需追溯到Crustdata结果、URL或用户提供的内容。若信息不明确,需询问或省略。绝不要编造近期事件、共同联系人或痛点。
- 所有信息均标注日期。无日期的信号毫无用处。60天前的信息需标注为“背景信息”。
- 每个交付内容均为单屏展示。销售代表会快速浏览。宁可删除板块也不要填充无关内容。
- 撰写内容需严谨——开场话术、问题、回顾内容:不要使用破折号,绝不要使用“深入探讨”/“利用”/“优化”等空洞词汇,不要有冗余句子。撰写风格要像完成了功课的同事。
- 区分自报信息与验证信息。表单数据在经Crustdata确认前均为自报信息。需标注清楚。
- 优先使用免费数据。联系人信息增强为可选功能,需确认成本后运行。无基础费用,按匹配联系人返回的联系方式类型计费(商务邮箱1积分,个人邮箱2积分,电话2积分),每人最高5积分。缩小
person_contact_enrich范围可设置最坏情况成本,但不保证封顶——曾出现过单层级请求消耗全额5积分上限并返回所有三类联系方式的情况,因此需按每人5积分的上限报价,并通过fields查看实际消耗。运行前需报价(“为12条线索获取邮箱,最高约60积分,是否继续?”)。credits_remaining - 对已连接系统只读访问。若已连接CRM、记录器或邮箱,可读取数据优化简报。该技能绝不写入、记录、发送或安排任何内容。
- 绝不要输出成交概率百分比。展示评分规则及依据即可。
- 根据内容调整布局——绝不隐藏信息。品牌系统固定,但布局可调整。若真实内容无法适配布局——长公司名称、12字职位、40条入站线索——需更改布局:允许卡片扩展、换行而非截断、改为单列、提高上限、或为宽内容添加滚动容器。绝不要裁剪卡片、省略名称、或静默丢弃行数据。若存在不可避免的上限,需在UI中标注(“显示前50条,共214条”),并在交付前查看渲染结果。
- 渲染输出中的图标:使用Lucide图标,以内联SVG形式嵌入,设置描边。成果UI中不要使用表情符号——🔥/🟡/⚪结论等级仅用于聊天;在渲染的简报中需变为彩色标签。
currentColor - Logo和照片为免费资源——在渲染输出中使用。来自免费的
basic_info.logo_permalink调用;company_identify包含在basic_profile.profile_picture_permalink返回的person_enrich分组中。需将两者以basic_profileURI形式嵌入Base64——媒体CDN以data:image/jpeg;base64,...格式提供,远程binary/octet-stream会显示空白。若图片缺失,使用首字母缩写作为 fallback。<img src> - 成果品牌标识:简报和分类表格默认适配聊天场景——不要为了渲染而渲染成果。但如果用户需要将简报或分类板作为渲染页面或文档,需在页眉或页脚添加Crustdata品牌标识:小型大写字母的“Powered by”前缀加上官方Crustdata文字标志,链接到crustdata.com。文字标志文件包含在该技能的目录中——
assets/(深色文字,适用于浅色背景)和crustdata-logo-light.png(白色文字,适用于深色背景)。将适配主题的变体以约17px高度嵌入Base64——绝不使用热链接;渲染成果无法获取远程图片。品牌强调色:crustdata-logo-dark.png(深色背景下为#5547E2)。正文字体:优先使用Geist(若可嵌入),否则使用系统字体栈。#8387FF - 转交指引:需完整客户计划的高优先级入站线索→account-research;类似公司拓展→sales-prospecting;未定义ICP→icp-builder;查找已对接人员→warm-path-workspace;撰写简报对应的跟进内容→sales-outreach。
Error handling
错误处理
- Branch on in every script. An unchecked failure looks like "no signal" and a rep walks into a call believing it.
r.ok - returns no match → the lead's domain may be personal, parked, or new. Surface it as unresolved with the reason; never guess a company.
company_identify - A 403 naming a field is a plan-gated projection — drop that field and re-run.
- no match → confirm the URL, then run the brief company-only and say the person block is thin.
person_enrich - Empty posts or empty news is not an error. Say "no public activity in the last 60 days" and write the opener from role context.
- Every execute response carries and
credits;credits_remaining(free) reports the balance on demand.account_credits
- 每个脚本均需根据进行分支处理。未检查的失败会导致“无信号”的假象,让销售代表带着错误信息进入通话。
r.ok - 无匹配结果→线索的域名可能是个人域名、已停用或新注册。需将其标记为未解析并说明原因;绝不要猜测公司。
company_identify - 返回403错误并标注字段名→该字段受计划限制——删除该字段后重新运行。
- 无匹配结果→确认URL后,仅基于公司信息生成简报,并说明联系人板块内容单薄。
person_enrich - 无帖子或无新闻并非错误。需说明“近60天无公开活动”,并基于职位背景撰写开场话术。
- 每个execute响应均包含和
credits;credits_remaining(免费)可按需查询余额。account_credits
Cost cheat sheet
成本速查表
| Call | Cost |
|---|---|
| Free |
| ~0.03 cr/result |
| counts only, ~free |
| 1 cr |
| 2 cr for one match |
| 1 cr per post — always set |
| 1 cr/query |
| no base charge; cap 5 cr/person — narrowing |
Typical spend: a triage batch of 10 leads is under 1 credit. A full pre-call brief is about 8-10.
| 调用 | 成本 |
|---|---|
| 免费 |
| 约0.03积分/结果 |
| 仅统计数量,近乎免费 |
| 1积分 |
按ID调用 | 匹配一条结果需2积分 |
| 1积分/帖子——需始终设置 |
| 1积分/查询 |
| 无基础费用;每人最高5积分——缩小 |
典型消耗:10条线索的分类处理消耗不到1积分。一份完整的通话前简报约消耗8-10积分。
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. Tools used here:get_schema,company_identify,company_search,company_enrich,person_search,person_enrich,job_search,social_post_list_live,web_search_live,company_autocomplete,person_autocomplete(opt-in),person_contact_enrichaccount_credits - Optional connectors the user may already have — a CRM, a call recorder, a calendar, email — all read-only, all optional. The skill runs fully on public data without any of them
Ships alongside icp-builder (writes the config this skill reads), sales-prospecting, account-research, and warm-path-workspace.
该技能需要:
- Crustdata MCP服务器(install.crustdata.com/mcp):一个Code Mode MCP,提供、
list_tools和get_schema功能。所有Crustdata数据工具均通过execute纯JavaScript脚本中的execute({ code })调用——基于await callTool(name, params)返回的类型接口编写,但脚本主体不能包含任何类型注解。此处使用的工具:get_schema、company_identify、company_search、company_enrich、person_search、person_enrich、job_search、social_post_list_live、web_search_live、company_autocomplete、person_autocomplete(可选)、person_contact_enrichaccount_credits - 可选连接器用户可能已配置——CRM、通话记录器、日历、邮箱——均为只读,且为可选。该技能可完全基于公开数据运行,无需任何连接器
与icp-builder(编写该技能读取的配置文件)、sales-prospecting、account-research和warm-path-workspace配套使用。