Loading...
Loading...
One-time GTM setup: paste one LinkedIn URL and Crustdata enriches it into a persona profile — who you are, what your company sells, your inferred ICP, your writing voice — saved as config/gtm-config.md and config/persona-profile.md, the files the sales-prospecting, account-research, sales-outreach, and meeting-prep skills read at startup. Use when someone says "build my ICP", "set up my GTM config", "create my persona profile", "onboard me", "get me started", or when another GTM skill reports the config is missing.
npx skill4agent add crustdata/skills icp-builderconfig/persona-profile.mdconfig/gtm-config.mdconfig/gtm-config.mdconfig/persona-profile.mdnonenonenoneexecuteparallelMap// user query: ...// model query: ...// user query: set up my GTM config — my LinkedIn is https://www.linkedin.com/in/example
const url = "https://www.linkedin.com/in/example";
// Stage 1: the person. Base cost 1 credit. `fields` is a response WHITELIST —
// the result carries ONLY the groups listed here; an omitted group reads as
// undefined later and looks like missing data. basic_profile + experience covers
// the persona; social_handles carries the canonical profile URL the posts pull
// is keyed on; contact groups only add cost.
const pr = await callTool("person_enrich", {
professional_network_profile_urls: [url],
fields: ["basic_profile", "experience", "social_handles"],
});
if (!pr.ok) return { error: pr.message };
const person = pr.data[0]?.matches?.[0]?.person_data;
if (!person) return { error: "no_match" }; // → confirm the URL, then no-data fallback
const canonicalUrl = profileUrl(person) ?? url; // preloaded accessor
const current = person.experience?.employment_details?.current?.[0] ?? {};
const companyId = currentCompanyIds(person)[0]; // preloaded accessor
// Stage 2: company + posts are independent of each other — fan them out.
const calls = [
{ name: "social_post_list_live",
params: { professional_network_profile_url: canonicalUrl, limit: 10 } }, // 1 cr/post — cap deliberately
];
if (companyId) {
calls.push({ name: "company_enrich",
params: { crustdata_company_ids: [companyId], exact_match: true,
fields: ["basic_info", "taxonomy"] } }); // 2 cr, exactly one match
}
const results = await parallelMap(calls, async (c) => ({ name: c.name, r: await callTool(c.name, c.params) }));
const postsR = results.find(x => x.name === "social_post_list_live")?.r;
const companyR = results.find(x => x.name === "company_enrich")?.r;
// Posts are optional: a failed or empty pull means neutral voice, not a failed run.
const posts = postsR && postsR.ok
? (postsR.data.posts ?? []).map(p => ({
text: p.text,
date: p.date_posted,
reactions: p.engagement?.total_reactions,
comments: p.engagement?.total_comments,
}))
: [];
const company = companyR && companyR.ok
? pick(companyR.data[0]?.matches?.[0]?.company_data ?? {}, ["basic_info", "taxonomy"])
: null;
// Return the smallest projection — only what the script returns reaches the model.
return {
identity: {
name: person.basic_profile?.name,
title: person.basic_profile?.current_title,
location: person.basic_profile?.location,
company: current.name,
company_domain: current.company_website_domain,
start_date: current.start_date, // tenure = today minus this
},
past_roles: (person.experience?.employment_details?.past ?? []).slice(0, 5)
.map(e => ({ company: e.name, title: e.title })),
company,
posts,
};preview: trueperson_enrichperson_enrichbasic_profileexperiencesocial_handlessocial_handlesprofileUrlundefinedcertificationshonorsupdated_atbasic_profile.current_titleexperience.employment_details.current[].namesocial_handles.professional_network_identifier.profile_urlprofileUrlcompany_identifyconfidence_scoreexact_match: true// 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"]);social_profilescompany_identifystart_datebasic_infotaxonomyinferred// 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_autocompleteinferredconfig/persona-profile.mdconfig/gtm-config.mdconfig/persona-profile.md# Persona Profile
Built by icp-builder on <YYYY-MM-DD>. Read by sales-prospecting, account-research, sales-outreach, and meeting-prep.
## Identity
- Name:
- Title:
- Company: <name> (<domain>)
- Tenure: since <start date>
- Background: <one line from past roles>
## Company & what we sell
- Product:
- Category:
- Keywords to monitor:
## Inferred ICP
Label: inferred from <what the company sells + typical buyers>. User-confirmed: <yes/no>
- Industries: <filter-ready values>
- Headcount:
- Geography:
- Funding stage:
- Buyer titles:
- Buyer seniority: <exact seniority vocabulary values>
## Voice
- Tone:
- Style notes:
- Always: no em dashes; never "delve", "leverage", or "streamline"; no filler;
write like a colleague.
## Topics they care about
- <from posts, weighted by engagement>config/gtm-config.md# GTM Config
Read by sales-prospecting, account-research, sales-outreach, and meeting-prep at startup.
## Stack
- Data provider: crustdata | none
- CRM: <tool> | none
- Calendar: <tool> | none
- Email: <tool> | none
- Call recorder: <tool> | none
- Sequencer: <tool> | none
- Team chat: <tool> | none
`none` = that slot runs draft-only: drafts and CSV exports instead of pushing to the tool.
## What we sell
<one or two lines>
## ICP (inferred)
- Industries:
- Headcount:
- Geography:
- Funding stage:
- Buyer titles:
- Buyer seniority:
## Customers
none yet — add names or domains as you close; sales-prospecting uses them for lookalikes.
## Voice
<tone in one line>. No em dashes; never "delve", "leverage", or "streamline"; no filler;
write like a colleague.You're set up. Try sales-prospecting ("build me a list from my ICP") or account-research ("research <company>") — both read this config automatically.
inferredperson_enrichbasic_profileexperiencesocial_handlescompany_identifycompany_autocompleteperson_autocompletecompany_enrichexact_match: truesocial_post_list_livelimitexecutecreditscredits_remainingaccount_creditsr.okperson_enrichnonenonecurrentColorbasic_profile.profile_picture_permalinkbasic_profileperson_enrichbinary/octet-streambasic_info.logo_permalinkcompany_identifycompany_enrichdata:image/jpeg;base64,...binary/octet-stream<img src>assets/crustdata-logo-light.pngcrustdata-logo-dark.png#5547E2#8387FFlist_toolsget_schemaexecuteexecute({ code })await callTool(name, params)get_schemaperson_enrichcompany_identifycompany_enrichsocial_post_list_livecompany_autocompleteperson_autocompleteaccount_creditsconfig/persona-profile.mdconfig/gtm-config.md