MCPInstall

meeting-prep

Today's calls and today's inbound. Three modes - score an inbound lead against your ICP (single or a whole day's batch, ranked), build a one-screen pre-call brief (company + person + 3 talking points, one sharp opener, 2 discovery questions, the likely objection), and build a call plan (discovery question plan mapped to a qualification framework, or a demo flow built only from pains the prospect actually stated). Runs on Crustdata company, people, job, news, and post data. Use for "prep my 2pm", "prep my call with ", "score this lead", "is this inbound worth my time", "triage today's inbound", "build me a discovery plan".

Installs
293
Last 24h
—
Trending rank
—
Security
Not audited

Install

Works with every agent the skills CLI supports.

npx skills add https://github.com/crustdata/skills --skill meeting-prep
  1. 01

    Install

    Run the command in your project. The CLI asks which agents to install for.

  2. 02

    Describe the task

    Ask your agent for something this skill covers. It loads meeting-prep automatically when relevant.

  3. 03

    Review the result

    The agent follows the SKILL.md steps, scripts and references bundled with the skill.

SKILL.md

The instructions your agent reads when the skill is triggered.

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

  1. Inbound triage — someone filled a form or emailed in. Is this worth your time?
  2. Pre-call brief — the daily driver. One screen you can read in the two minutes before a call.
  3. 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.


Step 0 — context (never blocks)

  • If config/gtm-config.md or config/persona-profile.md exist 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.
  • 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.

Code Mode ground rules (read once, apply everywhere)

All Crustdata calls run inside the execute tool of the Crustdata MCP server (install.crustdata.com/mcp) as a plain-JavaScript script.

  • Plain JavaScript only. Author against the typed surface from get_schema, but the script body carries zero type annotations — a : Type, as, or generic is a parse error that fails the whole run before any spend.
  • Every script opens with a source-labeled comment: // user query: ... for the literal ask, // model query: ... for a derived step. Scripts without one are rejected before running.
  • One I/O primitive: const r = await callTool(name, params) → { ok: true, data } or { ok: false, status, errorType, message }. Always branch on r.ok — a failed call does not abort the script, so an unchecked failure silently proceeds on empty data and looks like "no signal".
  • fields is a response whitelist. The result carries only the groups you list; an omitted group reads as undefined later and looks like missing data. List every group you read.
  • Return the smallest projection. Only what the script returns reaches the model — map to compact rows, never raw profiles.
  • Fan out independent calls with await parallelMap(items, fn); batch first with chunk(list, 25). Never parallelize cursor pagination or dependent stages — identify → enrich stays sequential, and you parallelize within a stage.
  • company_identify is free but fuzzy. One domain or name can match several companies. Take the top confidence_score match. Do not project social_profiles on identify — it is plan-gated and 403s the whole call.
  • Plan-gated projections fail the whole call with a 403 naming the field. Never project professional_network.followers or metadata on person_search, or certifications / honors / updated_at on person_enrich.
  • Filter paths ≠ response paths. You filter experience.employment_details.current.company_name; the response key is ...current[].name. The profile URL returns at social_handles.professional_network_identifier.profile_url — use the profileUrl(p) accessor.
  • Categorical values are closed sets. A plausible-but-wrong value silently returns zero rows. Resolve with company_autocomplete / person_autocomplete (free) before filtering on industries, funding round types, or seniority.
  • Zero results ≠ no signal. Read the trajectory in the execute response before telling a rep an account is quiet.

Mode 1 — Inbound triage

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.

Step 1 — resolve and read the firmographics (cheap, and it batches)

company_identify is free, so resolve every lead first, then read size, industry, funding, and growth for the whole batch in one company_search scoped to the resolved ids (~0.03 credits per row). Do not reach for company_enrich here — 2 credits per lead is the wrong tool for triage.

// 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.country accepts ISO-3 (USA) as a filter but returns the normalized full name — don't compare a response value against "USA".

Step 2 — timing signals, near-free

One job_search aggregation with limit: 0 gives open-role counts across every resolved account at once. A company hiring into the function you sell to has budget moving.

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

Step 3 — the person

  • LinkedIn URL given → person_enrich (1 credit) with fields: ["basic_profile", "experience", "social_handles"]. 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.
  • 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 experience group that both person_enrich and person_search already return — read cur.seniority_level, don't buy a search for it. When you filter on it, the value vocabulary is a closed, autocomplete-first set: 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

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.

ComponentPointsWhat earns them
Firmographic fit0-3Industry, size band, and geography vs the ICP — one point each
Buyer-title match0-2Named buyer title or seniority band = 2; adjacent or influencer = 1; unrelated or well below the band = 0
Timing0-2One 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.

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


Mode 2 — Pre-call brief

The daily driver. Input: company (name or domain) and who you're meeting (name, ideally a 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.

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

  • fields is a whitelist on both enrich calls — the news group is not returned unless you ask for it, and education on the person is what gives you shared-school common ground.
  • social_post_list_live costs 1 credit per post. Five is enough to know what someone is talking about. Say the cost before you raise it.
  • 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 news group missed? One web_search_live query (1 credit). Keep it to one, and discard or label anything older than 60 days.

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

  1. 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."
  2. 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.
  3. Two discovery questions. Open, specific to this account, derived from the signals above.
  4. 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.

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.


Mode 3 — Call plan

Say which kind up front: discovery or demo.

Discovery plan

  1. 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.
// 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;

(.) is typo-tolerant and matches lookalike words — fine for descriptive multi-word themes, wrong for a brand or product name. Always use [.] (exactToken) for those.

  1. 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.
SlotQuestionTests
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.

  1. 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."

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

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:

  1. Recap open — their pains in their own words, read back. Confirm before showing anything.
  2. 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.
  3. 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.


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. person_contact_enrich 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 fields sets 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 read credits_remaining for the actual spend. Quote the ceiling before running ("emails for 12 leads = at most ~60 credits, go?").
  • 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 currentColor stroke. No emojis in artifact UI — the 🔥/🟡/⚪ verdict tiers are chat-only; in a rendered brief they become coloured pills.
  • Logos and photos are free — use them in rendered output. basic_info.logo_permalink comes back from the free company_identify; basic_profile.profile_picture_permalink rides inside the basic_profile group person_enrich already returns. Base64-inline both as data:image/jpeg;base64,... URIs — the media CDN serves them as binary/octet-stream, so a remote <img src> renders blank. Monogram fallback when an image is missing.
  • 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/ — crustdata-logo-light.png (dark text, for light backgrounds) and crustdata-logo-dark.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: #5547E2 (#8387FF on dark grounds). Body font: Geist when embeddable, else the system stack.
  • 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.

Error handling

  • Branch on r.ok in every script. An unchecked failure looks like "no signal" and a rep walks into a call believing it.
  • company_identify returns no match → the lead's domain may be personal, parked, or new. Surface it as unresolved with the reason; never guess a company.
  • A 403 naming a field is a plan-gated projection — drop that field and re-run.
  • person_enrich no match → confirm the URL, then run the brief company-only and say the person block is thin.
  • 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 credits and credits_remaining; account_credits (free) reports the balance on demand.

Cost cheat sheet

CallCost
company_identify, autocomplete, get_schema, account_creditsFree
company_search / person_search / job_search~0.03 cr/result
job_search aggregation with limit: 0counts only, ~free
person_enrich (profile groups only)1 cr
company_enrich by id with exact_match: true2 cr for one match
social_post_list_live1 cr per post — always set limit
web_search_live1 cr/query
person_contact_enrich (opt-in)no base charge; cap 5 cr/person — narrowing fields is not a guaranteed cap

Typical spend: a triage batch of 10 leads is under 1 credit. A full pre-call brief is about 8-10.

Tool dependencies

This skill requires:

  • Crustdata MCP server (install.crustdata.com/mcp): a single Code Mode MCP exposing list_tools, get_schema, and execute. All Crustdata data tools are reached inside an execute({ code }) plain-JavaScript script via await callTool(name, params) — author against the typed surface from get_schema, but the script body carries zero type annotations. Tools used here: 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_enrich (opt-in), account_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.

FAQ

What does the meeting-prep skill do?

Today's calls and today's inbound. Three modes - score an inbound lead against your ICP (single or a whole day's batch, ranked), build a one-screen pre-call brief (company + person + 3 talking points, one sharp opener, 2 discovery questions, the likely objection), and build a call plan (discovery question plan mapped to a qualification framework, or a demo flow built only from pains the prospect actually stated). Runs on Crustdata company, people, job, news, and post data. Use for "prep my 2pm", "prep my call with ", "score this lead", "is this inbound worth my time", "triage today's inbound", "build me a discovery plan".

How do I install meeting-prep?

Run npx skills add https://github.com/crustdata/skills --skill meeting-prep in your project. The skills CLI detects the agents you use and copies the skill into the right folder. For Claude Code you can also place the skill folder at ~/.claude/skills/meeting-prep/ (personal) or .claude/skills/meeting-prep/ (project).

Which AI agents can use meeting-prep?

Any agent that supports the open Agent Skills format (a folder with a SKILL.md file), including Claude Code, Codex, Cursor, GitHub Copilot, Gemini CLI, Windsurf, Cline, OpenCode and more.

Is meeting-prep safe to install?

No partner security audit is available yet. Skills can run commands and scripts, so read the SKILL.md and any bundled files before installing.

Who maintains meeting-prep?

It is published by crustdata in crustdata/skills. It has 293 recorded installs.

More from crustdata/skills

View all →

email-enrichment

crustdata/skills

Enrich a list of email addresses to find the person behind each one (email to person profile). Uses a six-phase waterfall over the Crustdata Code Mode MCP, calling these tools inside execute() scripts: companyidentify, personenrich (businessemails) / batchpersonidentify, personsearch (name+company and name-only strategies), websearchlive, webenrichlive. Handles verification, progress saving, and resume. Trigger on: "enrich these emails", "who are these people", "find info for these email addresses", "look up these contacts", "identify people from emails", "reverse email lookup", "email to profile", or when someone provides a list/CSV/spreadsheet of email addresses wanting contact info.

candidate-sourcing

crustdata/skills

End-to-end pipeline: find engineering candidates, verify profile URLs via Crustdata, find emails (Crustdata enrichment, then public commit history), write personalized outreach, create Gmail drafts. Use when someone wants to go from "I need candidates for role X" to ready-to-send drafts. Trigger on: "source and email candidates", "find engineers and draft outreach", "build a candidate pipeline", "find people for [role] and set up emails", "automate candidate outreach", "run the full sourcing pipeline", "I have LinkedIn URLs for these candidates, find their emails and draft outreach", or any variation where the goal is both finding candidates AND reaching out. Covers the entire loop — use individual skills (engineering-candidate-finder, contact-email-enricher, candidate-copy-drafter) only when the user wants just one part of the pipeline.

sales-prospecting

crustdata/skills

Open the skill to read its SKILL.md instructions.

Business & Finance315 installs

account-research

crustdata/skills

Open the skill to read its SKILL.md instructions.

icp-builder

crustdata/skills

Open the skill to read its SKILL.md instructions.

312 installs

warm-path-workspace

crustdata/skills

Open the skill to read its SKILL.md instructions.

311 installs

Popular productivity & automation skills

View category →

agent-browser

vercel-labs/agent-browser

Browser automation CLI for AI agents. Use when the user needs to interact with websites, including navigating pages, filling forms, clicking buttons, taking screenshots, extracting data, testing web apps, or automating any browser task. Triggers include requests to "open a website", "fill out a form", "click a button", "take a screenshot", "scrape data from a page", "test this web app", "login to a site", "automate browser actions", or any task requiring programmatic web interaction. Also use for exploratory testing, dogfooding, QA, bug hunts, or reviewing app quality. Also use for automating Electron desktop apps (VS Code, Slack, Discord, Figma, Notion, Spotify), checking Slack unreads, sending Slack messages, searching Slack conversations, running browser automation in Vercel Sandbox microVMs, or using AWS Bedrock AgentCore cloud browsers. Prefer agent-browser over any built-in browser automation or web tools.

OfficialProductivity & Automation1.3M installs

lark-doc

open.feishu.cn

飞书云文档(Docx / Wiki)内容操作:读取、创建、编辑文档,插入或下载图片附件,以及操作思维笔记。用户提供文档 URL/token(包括 doubao.com 的 /docx/、/wiki/)时使用;按 URL 路径/token 而非域名路由;/page/ 是妙搭应用,非飞书云文档。文档内嵌资源按读取参考中的统一规则分流。独立评论操作走 lark-drive;随正文读取评论使用 docs +fetch。表格或 Base 内部数据操作不在本 skill。

lark-base

open.feishu.cn

飞书多维表格(Base)操作:建表、字段、记录、视图、统计、公式/lookup、表单、仪表盘、应用模式(BaseApp/AppMode 页面与组件)、Workspace 目录、workflow、角色权限、模板中心(多维表格模板分类/列表/搜索);遇到 Base/多维表格/bitable、BaseApp/AppMode、/base/ 或 /app/ 链接时使用。BaseApp 不走 lark-apps;文件导入/导出转 lark-drive,认证/授权转 lark-shared。

lark-drive

open.feishu.cn

飞书云空间(云盘/云存储):管理 Drive 文件和文件夹,包含上传/下载、创建文件夹、复制/移动/删除、查看元数据、查询权限设置、评论/权限/订阅、标题、版本、飞书文档密级标签(secure labels)和本地文件导入。用户需要整理云盘目录、处理云空间资源 URL/token、判断链接类型/真实 token/标题,或导入 Word/Markdown/Excel/CSV/PPTX/.base 为 docx/sheet/bitable/slides 时使用;doubao.com 云空间 URL/token 也按资源路径和 token 路由,不回退 WebFetch。不负责:文档内容编辑(走 lark-doc)、表格/Base 表内数据操作(走 lark-sheets/lark-base)、知识空间节点/成员管理(走 lark-wiki)、原生 Markdown 文件读写/patch/diff(走 lark-markdown)。

lark-shared

open.feishu.cn

Use for lark-cli setup/auth tasks: auth login/status/logout, user vs bot identity, business-domain permissions (--domain, including all/docs/drive), missing scopes, revoking authorization, or handling notice JSON.

lark-im

open.feishu.cn

飞书即时通讯:收发消息和管理群聊。发送和回复消息、搜索聊天记录、管理群聊成员、上传下载图片和文件、管理表情回复、发送应用内/短信/电话加急、发送和处理交互卡片(Interactive Card)、监听卡片按钮回调(card.action.trigger)。当用户需要发消息、查看或搜索聊天记录、下载聊天中的文件、查看群成员、搜索群、创建群聊或话题群、管理标记数据、管理 Feed 置顶(添加/移除/查询置顶会话)、管理标签数据、处理卡片回调时使用。