Loading...
Loading...
Compare original and translation side by side
executeget_schema: Typeas// user query: ...// model query: ...const r = await callTool(name, params){ ok: true, data }{ ok: false, status, errorType, message }r.okfieldsundefinedawait parallelMap(items, fn)chunkcheckpoint(acc)executeget_schema: Typeas// user query: ...// model query: ...const r = await callTool(name, params){ ok: true, data }{ ok: false, status, errorType, message }r.okfieldsundefinedawait parallelMap(items, fn)chunkcheckpoint(acc)config/persona-profile.mdconfig/gtm-config.mdconfig/persona-profile.mdconfig/gtm-config.mdlimit// user query: write a cold email to https://www.linkedin.com/in/example — find a hook first
const url = "https://www.linkedin.com/in/example";
const cutoffMs = Date.now() - 60 * 24 * 3600 * 1000; // hooks older than ~60 days read as stale
const r = await callTool("social_post_list_live", {
professional_network_profile_url: url, // person key; company_domain / crustdata_company_id key the company feed
limit: 5, // 1 CREDIT PER POST — set this deliberately
fields: ["text", "date_posted", "post_type", "engagement", "hyperlinks", "share_url"], // whitelist
});
if (!r.ok) return { hooks: [], error: r.message };
const parse = (d) => { const t = Date.parse(d ?? ""); return Number.isNaN(t) ? null : t; };
const posts = (r.data.posts ?? []).map(p => ({
date: p.date_posted,
ms: parse(p.date_posted),
type: p.post_type,
url: p.share_url, // the link to the post itself — the rep needs it to verify in five seconds
text: (p.text ?? "").slice(0, 400),
reactions: p.engagement?.total_reactions,
comments: p.engagement?.total_comments,
links: p.hyperlinks?.other_urls ?? [],
}));
return {
fresh: posts.filter(p => p.ms !== null && p.ms >= cutoffMs),
stale: posts.filter(p => p.ms !== null && p.ms < cutoffMs).length,
undated: posts.filter(p => p.ms === null), // never silently drop these — read them yourself
};limit// user query: write a cold email to https://www.linkedin.com/in/example — find a hook first
const url = "https://www.linkedin.com/in/example";
const cutoffMs = Date.now() - 60 * 24 * 3600 * 1000; // hooks older than ~60 days read as stale
const r = await callTool("social_post_list_live", {
professional_network_profile_url: url, // person key; company_domain / crustdata_company_id key the company feed
limit: 5, // 1 CREDIT PER POST — set this deliberately
fields: ["text", "date_posted", "post_type", "engagement", "hyperlinks", "share_url"], // whitelist
});
if (!r.ok) return { hooks: [], error: r.message };
const parse = (d) => { const t = Date.parse(d ?? ""); return Number.isNaN(t) ? null : t; };
const posts = (r.data.posts ?? []).map(p => ({
date: p.date_posted,
ms: parse(p.date_posted),
type: p.post_type,
url: p.share_url, // the link to the post itself — the rep needs it to verify in five seconds
text: (p.text ?? "").slice(0, 400),
reactions: p.engagement?.total_reactions,
comments: p.engagement?.total_comments,
links: p.hyperlinks?.other_urls ?? [],
}));
return {
fresh: posts.filter(p => p.ms !== null && p.ms >= cutoffMs),
stale: posts.filter(p => p.ms !== null && p.ms < cutoffMs).length,
undated: posts.filter(p => p.ms === null), // never silently drop these — read them yourself
};company_identifyconfidence_scoreexact_match: true// model query: recent company events at acme.com worth opening an email with
const idr = await callTool("company_identify", { domains: ["acme.com"] }); // free, one identifier type per call
if (!idr.ok) return { error: idr.message };
const matches = idr.data[0]?.matches ?? [];
const best = matches.slice().sort((a, b) => (b.confidence_score ?? 0) - (a.confidence_score ?? 0))[0];
if (!best) return { error: "no company match for acme.com" };
const companyId = best.company_data?.basic_info?.crustdata_company_id ?? best.company_data?.crustdata_company_id;
const er = await callTool("company_enrich", {
crustdata_company_ids: [companyId], // one id = one match = exactly 2 credits
exact_match: true, // narrows the fuzzy match; by-name/by-domain can still return >1 -> N x 2 cr
fields: ["basic_info", "funding", "headcount", "news"], // whitelist: list EVERY group you read
});
if (!er.ok) return { error: er.message };
const cd = er.data[0]?.matches?.[0]?.company_data ?? {};
const news = (firstArray(cd.news) ?? []).slice(0, 10).map(n => ({
title: n.article_title, url: n.article_url, date: n.article_publish_date,
}));
return {
name: cd.basic_info?.name,
lastRound: cd.funding?.last_round_type,
lastRaise: cd.funding?.last_fundraise_date,
lastRoundUsd: cd.funding?.last_round_amount_usd,
investors: cd.funding?.investors,
headcount: cd.headcount?.total,
growth12m: cd.headcount?.growth_percent?.["12m"],
news,
};company_identifyconfidence_scoreexact_match: true// model query: recent company events at acme.com worth opening an email with
const idr = await callTool("company_identify", { domains: ["acme.com"] }); // free, one identifier type per call
if (!idr.ok) return { error: idr.message };
const matches = idr.data[0]?.matches ?? [];
const best = matches.slice().sort((a, b) => (b.confidence_score ?? 0) - (a.confidence_score ?? 0))[0];
if (!best) return { error: "no company match for acme.com" };
const companyId = best.company_data?.basic_info?.crustdata_company_id ?? best.company_data?.crustdata_company_id;
const er = await callTool("company_enrich", {
crustdata_company_ids: [companyId], // one id = one match = exactly 2 credits
exact_match: true, // narrows the fuzzy match; by-name/by-domain can still return >1 -> N x 2 cr
fields: ["basic_info", "funding", "headcount", "news"], // whitelist: list EVERY group you read
});
if (!er.ok) return { error: er.message };
const cd = er.data[0]?.matches?.[0]?.company_data ?? {};
const news = (firstArray(cd.news) ?? []).slice(0, 10).map(n => ({
title: n.article_title, url: n.article_url, date: n.article_publish_date,
}));
return {
name: cd.basic_info?.name,
lastRound: cd.funding?.last_round_type,
lastRaise: cd.funding?.last_fundraise_date,
lastRoundUsd: cd.funding?.last_round_amount_usd,
investors: cd.funding?.investors,
headcount: cd.headcount?.total,
growth12m: cd.headcount?.growth_percent?.["12m"],
news,
};// model query: what acme.com is hiring for, and whether their posts name our category
const domain = "acme.com";
const term = "<the tool you displace>"; // a brand/product/tech name -> ALWAYS [.] exact token, never (.)
const [fresh, named] = await parallelMap([
{
filters: eq("company.basic_info.primary_domain", domain),
sorts: [{ field: "metadata.date_added", order: "desc" }],
fields: ["job_details", "location", "metadata"],
limit: 15,
},
{
filters: and_(
eq("company.basic_info.primary_domain", domain),
exactToken("content.description", term) // [.] guarantees the literal token; (.) is typo-tolerant and pulls lookalikes
),
fields: ["job_details"],
limit: 1, // count query — read total_count, don't download rows
},
], async (params) => await callTool("job_search", params));
if (!fresh.ok) return { error: fresh.message };
return {
newest: fresh.data.job_listings.map(j => ({
title: j.job_details?.title,
city: j.location?.city,
added: j.metadata?.date_added,
})),
total_open: fresh.data.total_count,
postings_naming_term: named.ok ? named.data.total_count : null,
};job_details.category// model query: what acme.com is hiring for, and whether their posts name our category
const domain = "acme.com";
const term = "<the tool you displace>"; // a brand/product/tech name -> ALWAYS [.] exact token, never (.)
const [fresh, named] = await parallelMap([
{
filters: eq("company.basic_info.primary_domain", domain),
sorts: [{ field: "metadata.date_added", order: "desc" }],
fields: ["job_details", "location", "metadata"],
limit: 15,
},
{
filters: and_(
eq("company.basic_info.primary_domain", domain),
exactToken("content.description", term) // [.] guarantees the literal token; (.) is typo-tolerant and pulls lookalikes
),
fields: ["job_details"],
limit: 1, // count query — read total_count, don't download rows
},
], async (params) => await callTool("job_search", params));
if (!fresh.ok) return { error: fresh.message };
return {
newest: fresh.data.job_listings.map(j => ({
title: j.job_details?.title,
city: j.location?.city,
added: j.metadata?.date_added,
})),
total_open: fresh.data.total_count,
postings_naming_term: named.ok ? named.data.total_count : null,
};job_details.category| Signal | The angle | The opening move |
|---|---|---|
| Fresh raise (<3 months) | Scaling pain — what breaks at the next headcount | Congratulate in half a sentence, then the pain the round creates |
| New exec in your function | New mandate, 90-day plan, fresh budget | "New in seat" energy: what are they inheriting |
| Champion moved to a new company | They already know you work | "Congrats — you know how this goes" (warmest touch there is) |
| Hiring surge in a relevant team | They've agreed the problem is real | Reference the roles; ask what happens between now and the hires landing |
| A post with a real opinion | Agree, extend, or politely disagree | Respond to the idea, not to the fact that they posted |
| Tech named in job posts | Stack fit or displacement | Name the tool, name the seam |
| Company news / launch | Timing | Tie the launch to the pain your product removes |
| 信号 | 切入角度 | 开场方式 |
|---|---|---|
| 近期融资(<3个月) | 扩张痛点——下一阶段人员规模增长会遇到的问题 | 用半句话恭喜,然后引出融资带来的痛点 |
| 你的业务领域新上任高管 | 新任务、90天计划、新预算 | 以「新上任」为切入点:他们接手了什么工作 |
| 老客户跳槽到新公司 | 对方已经了解你的产品 | 「恭喜——你知道我们的合作模式」(最有效的暖场触达) |
| 相关团队大规模招聘 | 他们已确认问题真实存在 | 提及招聘岗位,询问从现在到人员到岗期间的计划 |
| 带有真实观点的帖子 | 认同、延伸或礼貌反驳 | 回应观点本身,而非对方发布帖子这件事 |
| 招聘启事中提及特定技术 | 技术栈匹配或替代方案 | 明确提及工具名称和适配场景 |
| 公司新闻/产品发布 | 时机契合 | 将发布事件与你的产品能解决的痛点关联 |
social_post_list_livecompany_domaincompany_domainsocial_post_list_live| Reply type | The move |
|---|---|
| Interested | Propose two concrete times. Nothing else. Do not re-pitch. |
| Question | Answer it directly in one paragraph, then the ask. |
| Objection | Go to talk-tracks (below). |
| Referral out ("talk to X") | Thank them, ask for the intro or permission to name them, then open the new thread with the referral as the hook. |
| Not now | Get the date. "Should I come back in Q1?" Then actually log it. |
| Hard no | One gracious line, exit. No last-ditch pitch. |
| Unsubscribe | Honor immediately. No counter-offer, no "just confirming". Remove from every sequence. |
| 回复类型 | 处理方式 |
|---|---|
| 感兴趣 | 提出两个具体时间。无需其他内容,不要再推销。 |
| 提问 | 直接用一段文字回答,然后提出请求。 |
| 异议 | 使用下方的异议应对话术。 |
| 引荐他人(「联系X」) | 感谢对方,请求引荐或允许提及对方姓名,然后以该引荐为钩子开启新对话。 |
| 现在没时间 | 获取具体日期。比如「我在第一季度再联系您可以吗?」并记录该日期。 |
| 明确拒绝 | 一句礼貌的回复,结束对话。不要做最后一搏的推销。 |
| 取消订阅 | 立即执行。不要提出反要约,不要发送「确认取消」的消息。将对方从所有触达序列中移除。 |
1:1merge-field| Day | Channel | Angle | Personalization |
|---|---|---|---|
| 1 | The hook + the pain it implies | 1:1 | |
| 3 | Connection note, same hook, no pitch | 1:1 | |
| 5 | Call + voicemail | Reason-for-call = the hook | 1:1 |
| 8 | Proof point, in-thread | merge-field | |
| 12 | A different persona's version of the pain | merge-field | |
| 18 | Break-up, one line | merge-field |
1:1merge-field| 天数 | 渠道 | 切入角度 | 个性化程度 |
|---|---|---|---|
| 1 | 邮件 | 钩子+隐含痛点 | 1:1 |
| 3 | 连接请求备注,同一钩子,无推销 | 1:1 | |
| 5 | 电话+语音留言 | 通话理由=钩子 | 1:1 |
| 8 | 邮件 | 证明点,线程内回复 | merge-field |
| 12 | 邮件 | 不同角色的痛点版本 | merge-field |
| 18 | 邮件 | 终止跟进,一句话 | merge-field |
parallelMaplimit: 3// user query: draft outreach for these 40 accounts — pull a hook for each first
const people = inputs.people; // [{ name, profileUrl, company, domain }]
const cutoffMs = Date.now() - 60 * 24 * 3600 * 1000;
const hooks = await parallelMap(people, async (p) => {
const r = await callTool("social_post_list_live", {
professional_network_profile_url: p.profileUrl,
limit: 3, // 1 cr/post x 3 x N people — quote this before running
fields: ["text", "date_posted", "post_type", "share_url"],
});
if (!r.ok) return { name: p.name, hook: null, reason: r.message };
const fresh = (r.data.posts ?? [])
.map(x => ({ date: x.date_posted, ms: Date.parse(x.date_posted ?? ""), url: x.share_url, text: (x.text ?? "").slice(0, 300) }))
.filter(x => !Number.isNaN(x.ms) && x.ms >= cutoffMs);
return { name: p.name, company: p.company, hook: fresh[0] ?? null, reason: fresh.length ? null : "no recent post" };
});
checkpoint(hooks);
return {
withHook: hooks.filter(h => h.hook),
needCompanyFallback: hooks.filter(h => !h.hook).map(h => ({ name: h.name, company: h.company, reason: h.reason })),
};needCompanyFallback"Business emails for 40 people: at most ~40 credits (1 per matched person). Adding personal emails and phones raises the ceiling to 5 per person, so ~200. Business only — go?"
// user query: get business emails for the 40 people on the list (worst case ~40 credits, confirmed)
const urls = inputs.profileUrls;
const batches = chunk(urls, 25); // hard cap: 25 URLs per call
const results = await parallelMap(batches, async (batch) => {
const r = await callTool("person_contact_enrich", {
professional_network_profile_urls: batch,
fields: ["contact.business_emails"], // narrow the request; see the cost note below
});
return r.ok ? r.data : batch.map(u => ({ matched_on: u, matches: [], error: r.message }));
});
const rows = results.flat();
return {
contacts: rows.map(m => ({
url: m.matched_on,
email: m.matches?.[0]?.person_data?.contact?.business_emails?.[0]?.email ?? null,
status: m.matches?.[0]?.person_data?.contact?.business_emails?.[0]?.status ?? null,
})),
unmatched: rows.filter(m => !m.matches?.length).map(m => m.matched_on),
};fieldscredits_remainingemail, first_name, last_name, company, title, linkedin_url, hook_text, hook_date, hook_url, personalization_line, tierparallelMaplimit: 3// user query: draft outreach for these 40 accounts — pull a hook for each first
const people = inputs.people; // [{ name, profileUrl, company, domain }]
const cutoffMs = Date.now() - 60 * 24 * 3600 * 1000;
const hooks = await parallelMap(people, async (p) => {
const r = await callTool("social_post_list_live", {
professional_network_profile_url: p.profileUrl,
limit: 3, // 1 cr/post x 3 x N people — quote this before running
fields: ["text", "date_posted", "post_type", "share_url"],
});
if (!r.ok) return { name: p.name, hook: null, reason: r.message };
const fresh = (r.data.posts ?? [])
.map(x => ({ date: x.date_posted, ms: Date.parse(x.date_posted ?? ""), url: x.share_url, text: (x.text ?? "").slice(0, 300) }))
.filter(x => !Number.isNaN(x.ms) && x.ms >= cutoffMs);
return { name: p.name, company: p.company, hook: fresh[0] ?? null, reason: fresh.length ? null : "no recent post" };
});
checkpoint(hooks);
return {
withHook: hooks.filter(h => h.hook),
needCompanyFallback: hooks.filter(h => !h.hook).map(h => ({ name: h.name, company: h.company, reason: h.reason })),
};needCompanyFallback「为40人获取商务邮箱:最多约40积分(每个匹配的人1积分)。如果添加个人邮箱和电话,最高成本为每人5积分,总计约200积分。仅获取商务邮箱——是否执行?」
// user query: get business emails for the 40 people on the list (worst case ~40 credits, confirmed)
const urls = inputs.profileUrls;
const batches = chunk(urls, 25); // hard cap: 25 URLs per call
const results = await parallelMap(batches, async (batch) => {
const r = await callTool("person_contact_enrich", {
professional_network_profile_urls: batch,
fields: ["contact.business_emails"], // narrow the request; see the cost note below
});
return r.ok ? r.data : batch.map(u => ({ matched_on: u, matches: [], error: r.message }));
});
const rows = results.flat();
return {
contacts: rows.map(m => ({
url: m.matched_on,
email: m.matches?.[0]?.person_data?.contact?.business_emails?.[0]?.email ?? null,
status: m.matches?.[0]?.person_data?.contact?.business_emails?.[0]?.status ?? null,
})),
unmatched: rows.filter(m => !m.matches?.length).map(m => m.matched_on),
};fieldscredits_remainingemail, first_name, last_name, company, title, linkedin_url, hook_text, hook_date, hook_url, personalization_line, tierexecutecreditscredits_remainingaccount_creditscurrentColorbasic_profile.profile_picture_permalinkbasic_profilebasic_info.logo_permalinkcompany_identifycompany_enrichbasic_infodata:image/jpeg;base64,...binary/octet-stream<img src>assets/crustdata-logo-light.pngcrustdata-logo-dark.png#5547E2#8387FFexecutecreditscredits_remainingaccount_creditsstrokecurrentColorbasic_profile.profile_picture_permalinkbasic_profilebasic_info.logo_permalinkcompany_identifycompany_enrichbasic_infodata:image/jpeg;base64,...binary/octet-stream<img src>assets/crustdata-logo-light.pngcrustdata-logo-dark.png#5547E2#8387FFr.ok === falsefieldscompany_identifyconfidence_scorer.ok === falsefieldscompany_identifyconfidence_score| Call | Cost |
|---|---|
| Free |
| 1 cr per post — |
| 2 cr for one match |
| ~0.03 cr/result; |
| no base charge, billed per contact type returned per matched person (business email 1, personal 2, phone 2), capped at 5 per person. Narrowing |
| Writing, classifying, sequencing | Free — it's all model work |
| 调用 | 成本 |
|---|---|
| 免费 |
| 每篇帖子1积分—— |
| 每个匹配结果2积分 |
| 约0.03积分/结果; |
| 无基础费用,按匹配人员返回的联系人类型计费(商务邮箱1积分,个人邮箱2积分,电话2积分),每人最高5积分。缩小 |
| 撰写、分类、序列构建 | 免费——均为模型处理 |
config/persona-profile.mdconfig/gtm-config.mdconfig/persona-profile.mdconfig/gtm-config.mdlist_toolsget_schemaexecuteexecute({ code })await callTool(name, params)get_schemasocial_post_list_livecompany_identifycompany_enrichjob_searchperson_contact_enrichaccount_creditslist_toolsget_schemaexecuteexecute({ code })await callTool(name, params)get_schemasocial_post_list_livecompany_identifycompany_enrichjob_searchperson_contact_enrichaccount_credits