
Candidate Sourcing
- 273 installs
- 6 repo stars
- Updated August 3, 2026
- crustdata/skills
Runs an end-to-end recruiting pipeline: find engineering candidates, verify LinkedIn via Crustdata, find emails, draft outreach, and create Gmail drafts.
About
Chains five phases to turn a hiring role into ready-to-send Gmail drafts, handling discovery, LinkedIn verification, email enrichment, and personalized copy. A developer or recruiter uses it to automate candidate sourcing and outreach with human review before send.
- Five-phase pipeline from role to Gmail drafts
- Crustdata LinkedIn verification and email enrichment
Candidate Sourcing by the numbers
- 273 all-time installs (skills.sh)
- Ranked #510 of 2,715 Automation & Workflows skills by installs in the Skillselion catalog
- Data as of Aug 4, 2026 (Skillselion catalog sync)
npx skills add https://github.com/crustdata/skills --skill candidate-sourcingAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 273 |
|---|---|
| repo stars | ★ 6 |
| Last updated | August 3, 2026 |
| Repository | crustdata/skills ↗ |
What it does
Runs an end-to-end recruiting pipeline: find engineering candidates, verify LinkedIn via Crustdata, find emails, draft outreach, and create Gmail drafts.
Files
Candidate Sourcing Pipeline
An end-to-end skill that takes a hiring role and produces ready-to-send Gmail drafts for strong candidates — handling discovery, LinkedIn verification, email enrichment, personalized copy, and draft creation in one continuous workflow.
The pipeline has five phases. Each phase feeds into the next, and the skill is designed to run them in sequence with minimal human intervention. The user reviews the final Gmail drafts and clicks send.
---
Phase 1: Define the search and find candidates
Clarify the role and company
Before searching, extract or confirm these details from the user. Do not assume any of these — always ask if not provided:
About the hiring company (needed for outreach in Phase 4):
- Company name
- What the company does (1-2 sentences)
- Stage/traction (e.g., "Series A, $10M raised" or "500-person public company")
- Location / remote policy
- The sender's name and title (for the email signature)
About the role and ideal candidate:
- The role title (e.g., "Founding ML Engineer")
- 2-3 core technical problems the role involves
- Target companies, research labs, or communities to search
- Any school/alumni connections to prioritize (e.g., "IIIT Hyderabad alumni")
- Location preferences or constraints
- How many candidates the user wants in this batch
About the ideal candidate profile:
- Seniority level (e.g., "3-7 years", "senior", "staff+")
- Must-have technical skills or domain expertise
- Nice-to-have signals (open source contributions, publications, specific frameworks)
- Any deal-breakers or filters (e.g., "no FAANG lifers", "must have startup experience")
Use the company and role details to research what makes this company compelling to candidates — check their website, recent funding, product launches, or press mentions via crustdata_web_search. This research informs the company blurb in Phase 4.
Search by technical output, not job title
Three signals matter more than years of experience or company prestige:
Proof of work — papers they authored, tools/repos they built with real usage, blog posts with technical depth, open-source contributions, demos or benchmarks they released.
Hunger — technical opinions posted publicly, Medium/Substack articles, conference talks, being cited by others, active GitHub beyond just commits.
Relevance — their specific work maps to the actual problems in the role, not just adjacent domains.
Search sources and patterns
Crustdata `people_search_db` for structured search:
{
"params": {
"filters": {
"op": "and",
"conditions": [
{"column": "current_employers.title", "type": "(.)","value": "ML Engineer"},
{"column": "current_employers.name", "type": "[.]", "value": "Company Name"}
]
},
"limit": 20
}
}Research papers — search arXiv, Google Scholar, Semantic Scholar for the core technical problem. Look at first/second authors, especially those not at top-5 labs.
GitHub — search repos by topic/keyword, look at meaningful contributors (not just maintainers).
Crustdata web search — "[technical problem]" site:arxiv.org, "[name]" "[company]" github, "[community]" alumni engineer ML.
Output from Phase 1
A list of candidates, each with: full name, current role, current company, any school/community connection, and one specific piece of work that makes them relevant (paper, repo, tool, post). This "proof of work" note is critical — it becomes the basis for the outreach email opener in Phase 4.
---
Phase 2: Verify LinkedIn URLs
Never guess or construct LinkedIn URLs. This is the single most common source of errors in outreach. Guessed URLs like firstname-lastname or firstname-lastname-school frequently 404 or point to the wrong person. Real LinkedIn slugs are auto-generated and look like david-park-086833264 or hongjunchoi92 — they are not predictable from a person's name.
Waterfall approach
Use this exact priority order. Stop as soon as you get a confident match.
Step 1: `crustdata_people_search_db` (always try this first)
{
"params": {
"filters": {
"op": "and",
"conditions": [
{"column": "name", "type": "[.]", "value": "Person Name"},
{"column": "current_employers.name", "type": "[.]", "value": "Company Name"}
]
},
"limit": 3
}
}Extract flagship_profile_url — this is the verified canonical LinkedIn URL.
Tips:
- Company names have variations ("Google DeepMind" vs "DeepMind" vs "Google") — try shorter names first, then variations
- If 0 results, try past employers or just the person's name with a broader filter
- If multiple results, match by headline, location, or education
- Run these in batches of 5-6 parallel calls for efficiency
Step 2: `crustdata_web_search` (fallback when Step 1 returns 0)
{
"params": {
"query": "Person Name Company distinctive-keyword",
"site": "linkedin.com",
"limit": 5
}
}Verify the result matches by checking the snippet for employer/role alignment. Don't just grab the first LinkedIn URL.
Step 3: Mark as unverified (if both fail)
Keep whatever URL exists but add a note: "LinkedIn URL unverified." Never fabricate a URL.
Slug comparison
When verifying an existing URL: extract the slug (part after linkedin.com/in/), strip trailing slashes, compare case-insensitively. Different slugs = wrong URL, use the flagship_profile_url instead.
Real mistakes this prevents
These are actual errors from production outreach campaigns:
| Guessed slug | Actual slug | What happened |
|---|---|---|
david-park-princeton | david-park-086833264 | 404 — made-up slug |
vincent-chen-mit | vincent-chen-662a031b5 | 404 — school suffix doesn't work |
benoit-rostykus | benoitrostykus | Wrong person — different profile |
hongjun-choi | hongjunchoi92 | Wrong person — different profile |
abhay-gupta-cmu | gupta-abhay | Wrong person — completely different slug format |
benedict-arockiaraj | benedictflorance | Wrong person — person uses a different name on LinkedIn |
initeshmethani | nitesh-methani-7554b3121 | Wrong person — typo in guessed slug |
URL format
Always use https://www.linkedin.com/in/{slug} — never bare linkedin.com/in/ without the protocol. Characters like ü need URL encoding (%C3%BC).
---
Phase 3: Find email addresses
Every candidate needs an email address for the Gmail draft. Use this priority chain.
3A: Business email via Crustdata enrichment
Batch up to 25 LinkedIn URLs per call:
crustdata_people_enrich:
linkedin_profile_url: "url1,url2,url3..."
fields: "name,business_email"The fields: "name,business_email" parameter is critical — the default response does NOT include business email.
Expected hit rates: ~70-80% for professionals at known companies, ~40-50% for independent operators, ~20-30% for people between roles.
3B: Personal email via Crustdata person enrichment
Crustdata's person enrich API can return personal email addresses directly. This is faster and more reliable than GitHub commit extraction, so always try this before falling back to GitHub.
Request personal emails by including fields=personal_contact_info.personal_emails:
crustdata_people_enrich:
linkedin_profile_url: "url1,url2,url3..."
fields: "personal_contact_info.personal_emails"The response includes a personal_contact_info object with a personal_emails array containing the person's personal email addresses (Gmail, ProtonMail, etc.).
You can combine this with business email and phone numbers in a single call:
crustdata_people_enrich:
linkedin_profile_url: "url1,url2,url3..."
fields: "name,business_email,personal_contact_info.personal_emails,personal_contact_info.phone_numbers"Credit cost: 2 credits per profile for personal_contact_info.personal_emails, 2 credits per profile for personal_contact_info.phone_numbers (on top of the base enrichment cost).
Batch limit: Up to 25 LinkedIn URLs per call, same as business email enrichment.
Recommended approach: Combine steps 3A and 3B into a single enrichment call by requesting both business_email and personal_contact_info.personal_emails fields together. This saves an API round-trip and gets both email types at once.
If personal email is found, prefer it over business email for cold outreach (higher response rate, less likely to be filtered by corporate spam). If this returns no personal email for a candidate, fall through to GitHub commit extraction below.
3C: Personal email via GitHub commit history (fallback if 3B returns nothing)
This is the most powerful technique for technical candidates. Git records the author's email in every commit, and this metadata is accessible even when profile email privacy is enabled.
Step 1: Find GitHub username
- Check Crustdata enrichment response (may include GitHub URL, but verify it's the right person)
- Web search:
"[Name] [Company] GitHub site:github.com" - Check their personal website or Twitter bio
Step 2: Verify the GitHub profile belongs to this person Confirm at least 2 of: bio mentions known company/role, profile name matches, repo topics align with known expertise, web search confirms the connection.
Step 3: Find oldest non-fork repo
crustdata_web_fetch:
urls: ["https://api.github.com/users/{username}/repos?sort=created&direction=asc&per_page=5"]Pick first repo where "fork": false. Older repos (pre-2019) are more likely to have real emails.
Step 4: Extract email from commits
Method A — Commits API:
crustdata_web_fetch:
urls: ["https://api.github.com/repos/{owner}/{repo}/commits?per_page=1"]Look in [0].commit.author.email.
Method B — .patch endpoint (bypasses privacy settings):
crustdata_web_fetch:
urls: ["https://github.com/{owner}/{repo}/commit/{sha}.patch"]Extract from From: Name <email> header line.
Step 5: Validate — discard *@users.noreply.github.com, noreply@github.com, and any email containing noreply.
3D: Web search fallbacks
When both Crustdata personal email enrichment and GitHub don't work, try these in order: 1. GitHub issues/READMEs: "[name]" "@gmail.com" site:github.com 2. Competitive programming: "[name]" site:codeforces.com 3. Personal websites: "[name]" "[company]" email contact 4. Conference speaker pages: "[name]" "[company]" speaker email 5. Academic profiles: "[name]" site:scholar.google.com
3E: Handle GitHub API rate limits
GitHub allows 60 unauthenticated requests/hour. Workarounds:
- Use
.patchendpoints (don't count against REST API limits) - Fetch HTML commit pages and extract SHAs with regex, then use
.patch - Batch
crustdata_web_fetchcalls with multiple URLs (up to 10 per call) - Process in waves — API-dependent steps first, then non-API methods while rate limit resets
Email priority
When you have multiple emails for a candidate, prefer in this order: 1. Personal Gmail/ProtonMail — highest response rate for cold outreach 2. University email — if they're still in academia 3. Business email — last resort for cold outreach (often filtered by corporate spam)
---
Phase 4: Write personalized outreach emails
The one rule that matters
Praise specific work, not job titles or implied capabilities.
"Leading applied science at Mistral means you know how to take ML from research to production" reads as "I saw your LinkedIn title and ran it through a template." Instead, find one thing they actually made and say something honest about it.
Email structure
Every email follows this exact template:
Hi {first_name},
[1-sentence opener: say what impressed you — casual, no analysis]
[1-sentence pitch: role + company + location]
[3-line company blurb]
{sender_name},
{sender_title}Short. No headers, no bullet points, no multi-paragraph explanations. Ask the user for their name, title, company name, and a 2-3 line company blurb if not already provided.
Writing the opener
Use the "proof of work" note from Phase 1 — the specific paper, repo, tool, or post you identified during candidate discovery.
Good opener patterns:
- "Read your work on [specific paper/project] — [genuine reaction]."
- "Your [specific tool/repo] is [honest assessment with concrete detail]."
- "The path from [specific journey point A] to [B] is impressive."
Bad opener patterns (never use these):
- [their work] + [which is relevant because we also do X] — turns compliment into pitch
- [job title at company] + [therefore X skill] — title-based, not work-based
- [their research] + [is exactly the kind of thinking we need] — evaluates them for them
- Long parenthetical explanations — breaks casual flow
The pitch line
"We're hiring a [role] at [Company] in [location]. Would you be interested?"
Keep it direct. The opener already did the work.
The company blurb
Ask the user for a 2-3 line company blurb if they haven't provided one. It should highlight traction, mission, and why now. Vary phrasing slightly across a batch to avoid identical emails.
Subject line
Default: "[Role] @ [Company]"
---
Phase 5: Create Gmail drafts and log to tracker
Create Gmail drafts
For each candidate with a verified email, use gmail_create_draft:
to: candidate's email addresssubject: the subject linebody: the full email (opener + pitch + blurb + signature)
This is the terminal step. Gmail's MCP creates drafts but cannot send — the user reviews and clicks send manually.
Log to the outreach tracker
After creating each draft, append a row to the role-specific CSV tracker:
/sessions/{session}/mnt/outputs/outreach_tracker_{role_slug}.csvWhere role_slug is lowercase-hyphenated (e.g., "Founding ML Engineer" → founding_ml_engineer).
CSV headers:
date,role_hiring_for,candidate_name,current_role,company,school_connection,linkedin_url,email,channel,subject,message_opener,status,notes,message_bodyimport csv, datetime, os
tracker_path = "outreach_tracker_{role_slug}.csv"
fields = ["date","role_hiring_for","candidate_name","current_role","company",
"school_connection","linkedin_url","email","channel","subject",
"message_opener","status","notes","message_body"]
row = {
"date": datetime.date.today().isoformat(),
"role_hiring_for": "<role name>",
"candidate_name": "<full name>",
"current_role": "<title>",
"company": "<company>",
"school_connection": "<school or empty>",
"linkedin_url": "<verified linkedin url>",
"email": "<email address>",
"channel": "email",
"subject": "<subject line>",
"message_opener": "<first sentence of email body>",
"status": "drafted",
"notes": "<email type + source: e.g. Personal Gmail via GitHub commit>",
"message_body": "<full email body>"
}
file_exists = os.path.isfile(tracker_path)
with open(tracker_path, "a", newline="", encoding="utf-8") as f:
writer = csv.DictWriter(f, fieldnames=fields)
if not file_exists:
writer.writeheader()
writer.writerow(row)Status values: drafted (Gmail draft created), sent (user clicked send), replied, no_contact (no email found).
Channel values: email
Candidates without email
If no email was found after the full enrichment chain, log them to the tracker with status: "no_contact" and a note explaining what was tried. The user can decide whether to pursue other channels manually.
---
Batch workflow for efficiency
When processing many candidates (>5), use this sequence to minimize time:
1. Search and collect candidates (Phase 1) — build the full list first 2. Batch LinkedIn verification (Phase 2) — 5-6 parallel people_search_db calls; web search fallback for failures 3. Batch email enrichment (Phase 3A + 3B) — up to 25 LinkedIn URLs per call with fields: "name,business_email,personal_contact_info.personal_emails" to get both business and personal emails in one round-trip 4. Triage for GitHub (Phase 3C) — identify candidates still missing emails who are engineers likely to have GitHub profiles; prioritize them for commit email extraction 5. Batch GitHub lookups (Phase 3C) — crustdata_web_fetch with multiple GitHub URLs per call (up to 10) 6. Web search fallbacks (Phase 3D) — for candidates where Crustdata enrichment and GitHub both failed 7. Write all email copy (Phase 4) — draft all openers in one pass, using the proof-of-work notes from Phase 1 8. Create all Gmail drafts (Phase 5) — create drafts and log to tracker
Handling large API responses
Crustdata enrichment and people_search_db calls can return results that exceed token limits and get saved to files. When this happens:
import json
with open(filepath) as f:
data = json.load(f)
inner = json.loads(data[0]['text'])
for p in inner.get('profiles', []):
print(p.get('name'), '|', p.get('flagship_profile_url'))Always parse saved results with Python rather than trying to process them inline.
---
Tool dependencies
This skill requires:
- Crustdata MCP server (mcp.crustdata.com/mcp): provides
crustdata_people_search_db,crustdata_people_enrich(withfieldssupportingbusiness_email,personal_contact_info.personal_emails,personal_contact_info.phone_numbers),crustdata_company_enrich,crustdata_web_search,crustdata_web_fetch - Gmail MCP:
gmail_create_draft - Python (with
openpyxlfor spreadsheet I/O,csvfor tracker) - Web search (Crustdata) for fallback email discovery
---
Founder exclusion rule
Do not reach out to current founders, co-founders, CEOs, or CTOs — people who are actively running their own company. They are unlikely to leave, and contacting them wastes a slot and can feel tone-deaf.
How to detect founders
Check the candidate's current title during Phase 1. Flag anyone whose title contains: Founder, Co-Founder, Co-founder, CEO, CTO, or "Chief" with "Officer" (e.g., Chief Technology Officer).
The shrinking-company exception
The only case where it's worth reaching out to a current founder is when their company is visibly failing. All three conditions must be true:
1. Headcount is declining — use crustdata_company_enrich with the company's domain:
crustdata_company_enrich:
company_domain: "example.com"
fields: "company_name,headcount,web_traffic,founders"Check headcount_latest.linkedin_headcount_total_growth_percent — look for negative month-over-month and quarter-over-quarter growth.
2. Website traffic is declining — in the same enrichment response, check web_traffic for downward trends in monthly visitors.
3. People are leaving faster than the company can sustain — the number of ex-employees who left in the last 3 months is greater than the current headcount. This signals a company that's actively losing people, not just flat.
If all three conditions are met, the founder may be open to a new opportunity. If any condition is NOT met (e.g., the company is small but growing), skip the candidate and mark them as status: "skipped" in the tracker with a note like "Excluded — active founder (company not shrinking)".
Handling the enrichment response
Company enrichment responses can exceed token limits. Parse with Python:
import json
with open(filepath) as f:
data = json.load(f)
inner = json.loads(data[0]['text'])
companies = inner['companies']
for c in companies:
hc = c.get('headcount_latest', c.get('headcount', {}))
linkedin_hc = hc.get('linkedin_headcount', '?')
growth = hc.get('linkedin_headcount_total_growth_percent', {})
mom = growth.get('1_month', 0)
qoq = growth.get('3_months', 0)
print(f"{c['company_name']}: {linkedin_hc} employees, MoM: {mom}%, QoQ: {qoq}%")Real examples
| Candidate | Company | What happened |
|---|---|---|
| Di Jin (Co-Founder) | Eigen AI (12 employees, -7.7% MoM) | Slightly declining but not enough ex-employees leaving → skipped |
| Sai Surbehera (Co-Founder/CTO) | Lapis Labs (5 employees, +25% MoM) | Company growing → skipped |
| Aayush Anand (Co-Founder) | Level.game (9 employees, stable) | Company active → skipped |
---
What NOT to do
- Never guess LinkedIn URLs — always verify through Crustdata. Guessed URLs caused 12 errors in a single 91-person campaign.
- Never send emails — Gmail MCP creates drafts only. The user reviews and sends manually.
- Never use title-based openers — always reference specific work the candidate has done.
- Never skip email validation — discard noreply addresses, verify GitHub profiles belong to the right person.
- Never write the same opener twice — each candidate gets a unique opener based on their specific work.
- Never reach out to active founders — skip current founders/co-founders/CEOs/CTOs unless their company is visibly failing (declining headcount, declining traffic, and people leaving faster than the company can sustain). See "Founder exclusion rule" section above.
{
"skill_name": "candidate-sourcing",
"description": "Evals derived from real production errors encountered during a 91-candidate outreach campaign. Each eval targets a specific failure mode that was observed and fixed.",
"evals": [
{
"id": 1,
"name": "linkedin-url-never-guessed",
"category": "linkedin_verification",
"prompt": "Find ML engineers at Princeton who work on NLP. I need their LinkedIn profiles. Here are some names I found from papers: David Park (Princeton grad, now at a startup), Vincent Chen (MIT grad, now at a startup), Abhay Gupta (CMU grad).",
"expected_output": "LinkedIn URLs should be fetched from Crustdata people_search_db or web_search — never constructed from name patterns like 'david-park-princeton' or 'vincent-chen-mit'. The URLs returned should contain auto-generated slugs (e.g., 'david-park-086833264', 'vincent-chen-662a031b5', 'gupta-abhay').",
"assertions": [
{
"text": "No LinkedIn URL contains a school name as suffix (e.g., '-princeton', '-mit', '-cmu')",
"type": "negative_pattern",
"pattern": "linkedin\\.com/in/[a-z-]+(princeton|mit|cmu|stanford|berkeley|harvard|yale)"
},
{
"text": "All LinkedIn URLs were fetched via crustdata_people_search_db or crustdata_web_search, not constructed",
"type": "tool_usage",
"required_tools": ["crustdata_people_search_db"]
},
{
"text": "All LinkedIn URLs use the full https://www.linkedin.com/in/ prefix",
"type": "pattern",
"pattern": "^https://www\\.linkedin\\.com/in/"
}
],
"failure_story": "In the real campaign, URLs like 'david-park-princeton', 'vincent-chen-mit', and 'abhay-gupta-cmu' were all fabricated and either 404'd or pointed to wrong people. The correct slugs were 'david-park-086833264', 'vincent-chen-662a031b5', and 'gupta-abhay' respectively."
},
{
"id": 2,
"name": "linkedin-slug-mismatch-detection",
"category": "linkedin_verification",
"prompt": "I have a candidate list with LinkedIn URLs. Can you verify these are correct? Benoit Rostykus (Nuro, autonomous vehicles) — linkedin.com/in/benoit-rostykus. Hongjun Choi (researcher) — linkedin.com/in/hongjun-choi. Benedict Arockiaraj (Google) — linkedin.com/in/benedict-arockiaraj.",
"expected_output": "Each URL should be verified against Crustdata's people_search_db. The correct slugs are: benoitrostykus (not benoit-rostykus), hongjunchoi92 (not hongjun-choi), benedictflorance (not benedict-arockiaraj). All three should be flagged as mismatches and corrected.",
"assertions": [
{
"text": "Benoit Rostykus slug corrected to 'benoitrostykus'",
"type": "output_contains",
"value": "benoitrostykus"
},
{
"text": "Hongjun Choi slug corrected to 'hongjunchoi92'",
"type": "output_contains",
"value": "hongjunchoi92"
},
{
"text": "Benedict Arockiaraj slug corrected to 'benedictflorance'",
"type": "output_contains",
"value": "benedictflorance"
},
{
"text": "All three original URLs flagged as mismatches",
"type": "semantic",
"description": "The output should clearly indicate that all three provided URLs were incorrect"
}
],
"failure_story": "In the real campaign, 'benoit-rostykus' was a 404, 'hongjun-choi' pointed to a completely different person, and 'benedict-arockiaraj' was also wrong — the person uses 'benedictflorance' on LinkedIn. Simple firstname-lastname patterns are unreliable."
},
{
"id": 3,
"name": "company-name-variation-handling",
"category": "linkedin_verification",
"prompt": "Find the LinkedIn profile for Michael Günther who works at Elastic (the search company). Also find Siyue Wang who works at Microsoft Research on NLP.",
"expected_output": "For Michael Günther, a search with 'Elastic' should be tried, and if it returns wrong results (people at Elastic Email or unrelated), try variations. The slug should contain the ü character properly encoded. For Siyue Wang, if 'Microsoft' returns too many results or zero, try 'Microsoft Research' or include the full name 'Siyue Victoria Wang'.",
"assertions": [
{
"text": "Michael Günther search handles the umlaut character (ü) in the URL",
"type": "semantic",
"description": "The LinkedIn URL should contain either 'günther' or the URL-encoded equivalent 'g%C3%BCnther'"
},
{
"text": "Company name variations were tried when initial search failed",
"type": "tool_usage_count",
"tool": "crustdata_people_search_db",
"min_calls": 2,
"description": "At least one retry with different company name or name variation"
}
],
"failure_story": "In the real campaign, 'Michael Günther' at 'Elastic' returned people from Elastic Email (a different company). Had to adjust. 'Siyue Wang' at 'Microsoft' returned 0 results — found her only by searching 'Siyue Victoria Wang Microsoft'."
},
{
"id": 4,
"name": "malformed-url-prefix-prevention",
"category": "linkedin_verification",
"prompt": "Here are some candidates I found. Add them to my tracker: Pranav Kohli (LinkedIn: linkedin.com/in/pranav-kohli-6a239782), Nicolas Remerscheid (LinkedIn: linkedin.com/in/nicolas-remerscheid), Joseph Miano at Fireworks AI (LinkedIn: linkedin.com/in/joseph-miano).",
"expected_output": "All LinkedIn URLs in the tracker should use the full 'https://www.linkedin.com/in/' prefix, never bare 'linkedin.com/in/'.",
"assertions": [
{
"text": "No tracker entries contain bare 'linkedin.com/in/' without https:// prefix",
"type": "negative_pattern",
"pattern": "^linkedin\\.com/in/"
},
{
"text": "All URLs in tracker start with 'https://www.linkedin.com/in/'",
"type": "pattern",
"pattern": "^https://www\\.linkedin\\.com/in/"
}
],
"failure_story": "In the real campaign, 20 rows (rows 41-60) had URLs starting with bare 'linkedin.com/in/' instead of 'https://www.linkedin.com/in/'. These were malformed and wouldn't work as clickable links."
},
{
"id": 5,
"name": "email-opener-work-based-not-title-based",
"category": "outreach_quality",
"prompt": "Write outreach emails for these candidates for a Founding ML Engineer role at our startup (we're building AI-native data infrastructure, Series A, SF-based). My name is Alex, I'm the CTO. 1) Rajat Arya, VP of Engineering at XetHub (acquired by HuggingFace), built the storage layer for model hosting. 2) Lewis Tunstall, ML Engineer at HuggingFace, co-authored 'NLP with Transformers' book.",
"expected_output": "Each opener should reference specific work (XetHub acquisition, the NLP with Transformers book) rather than job titles or implied capabilities. No opener should contain phrases like 'your experience as VP means...' or 'leading engineering at XetHub demonstrates...'",
"assertions": [
{
"text": "Rajat's opener mentions XetHub or HuggingFace acquisition specifically",
"type": "output_contains_any",
"values": ["XetHub", "acquisition", "HuggingFace"]
},
{
"text": "Lewis's opener mentions the book 'NLP with Transformers'",
"type": "output_contains",
"value": "NLP with Transformers"
},
{
"text": "No opener uses title-based reasoning patterns like 'your role as X means/shows/demonstrates'",
"type": "negative_pattern",
"pattern": "(your (role|position|experience|title|work) (as|at).*(means|shows|demonstrates|implies|suggests))"
},
{
"text": "No opener connects their work to the hiring company's problems in the same sentence as the compliment",
"type": "negative_pattern",
"pattern": "(which is (exactly|precisely|directly) (what|the kind))"
}
],
"failure_story": "Early drafts used patterns like 'Leading applied science at Mistral means you know how to take ML from research to production' — title-based reasoning that reads as a template. The fix: always mention a specific artifact (paper, tool, repo, acquisition) not a job title."
},
{
"id": 6,
"name": "github-email-extraction-with-verification",
"category": "email_enrichment",
"prompt": "Find a personal email for Anurag Sahu who works at Zscaler, previously at Ola Krutrim. He's an IIIT Hyderabad alumnus and likely has a GitHub profile.",
"expected_output": "Should find GitHub profile 'AnuragSahu', verify it belongs to this person (check bio, repos match ML/CV background), find oldest non-fork repo, extract email from commit metadata. Should NOT return a noreply@github.com address. Should verify the GitHub profile actually belongs to this specific Anurag Sahu before extracting email.",
"assertions": [
{
"text": "GitHub profile was verified before extracting email (checked bio or repos against known background)",
"type": "semantic",
"description": "Evidence that the GitHub profile was verified as belonging to the right person"
},
{
"text": "Extracted email is not a noreply address",
"type": "negative_pattern",
"pattern": "noreply"
},
{
"text": "Used oldest non-fork repo for commit email extraction",
"type": "semantic",
"description": "Selected repos sorted by created date ascending, picked non-fork"
}
],
"failure_story": "Crustdata enrichment sometimes returns wrong GitHub profiles for common names. In one case (Ali Kashani, CEO of Serve Robotics), the enrichment returned a GitHub profile 'alikashani' that belonged to a completely different person. Extracting that email would have sent outreach to a stranger."
},
{
"id": 7,
"name": "gmail-draft-not-send",
"category": "pipeline_correctness",
"prompt": "I found 3 great ML engineer candidates. Their emails are verified. Write outreach and send the emails.",
"expected_output": "Should create Gmail DRAFTS (using gmail_create_draft), not attempt to send emails. Should explicitly note to the user that drafts are ready for their review and manual sending.",
"assertions": [
{
"text": "Uses gmail_create_draft tool, not any send tool",
"type": "tool_usage",
"required_tools": ["gmail_create_draft"],
"forbidden_tools": ["gmail_send"]
},
{
"text": "Explicitly tells user that drafts are ready for review and manual sending",
"type": "semantic",
"description": "Output includes clear language about the user reviewing and sending the drafts themselves"
}
],
"failure_story": "Gmail MCP does not support sending — only draft creation. The user reviews each draft and clicks send manually. This is by design: it prevents accidental sends of unreviewed outreach."
},
{
"id": 8,
"name": "tracker-csv-logging-complete",
"category": "pipeline_correctness",
"prompt": "Source 2 ML engineers from IIIT Hyderabad alumni working at top tech companies, find their emails, write outreach for Founding ML Engineer role, and create Gmail drafts.",
"expected_output": "After creating Gmail drafts, each candidate should be logged to the outreach tracker CSV with all fields populated: date, role, name, current_role, company, school_connection, linkedin_url, email, channel (email), subject, message_opener, status (drafted), notes, message_body.",
"assertions": [
{
"text": "Tracker CSV is created/updated with all required columns",
"type": "semantic",
"description": "CSV contains: date, role_hiring_for, candidate_name, current_role, company, school_connection, linkedin_url, email, channel, subject, message_opener, status, notes, message_body"
},
{
"text": "Status is set to 'drafted' not 'sent'",
"type": "output_contains",
"value": "drafted"
},
{
"text": "Channel is set to 'email' only",
"type": "semantic",
"description": "Channel field should be 'email', not 'linkedin_inmail' or 'email + linkedin_inmail'"
},
{
"text": "LinkedIn URLs in tracker are verified (not guessed)",
"type": "semantic",
"description": "URLs should come from Crustdata verification, not be constructed from name patterns"
}
],
"failure_story": "Incomplete tracker logging means losing track of who was contacted, through which channel, and with what message. This makes it impossible to follow up systematically or avoid double-contacting candidates."
},
{
"id": 9,
"name": "waterfall-fallback-on-db-miss",
"category": "linkedin_verification",
"prompt": "Find the LinkedIn profile for Aakash Kumar Nain who works at Ola (the Indian ride-hailing company) as a deep learning engineer.",
"expected_output": "crustdata_people_search_db should be tried first with 'Aakash Kumar Nain' + 'Ola'. If that returns 0 results, crustdata_web_search should be used as fallback with 'Aakash Kumar Nain Ola deep learning site:linkedin.com'. If web search also fails, the candidate should be marked as 'LinkedIn URL unverified' — NOT given a guessed URL.",
"assertions": [
{
"text": "people_search_db was tried first",
"type": "tool_usage_order",
"first": "crustdata_people_search_db",
"then": "crustdata_web_search"
},
{
"text": "Web search fallback was attempted after DB returned 0 results",
"type": "tool_usage",
"required_tools": ["crustdata_web_search"]
},
{
"text": "If both fail, candidate is NOT given a fabricated LinkedIn URL",
"type": "semantic",
"description": "When both search methods fail, the URL should be marked unverified or left empty, never constructed"
}
],
"failure_story": "In the real campaign, Aakash Kumar Nain could not be found in Crustdata's DB (0 results for name + 'Ola'). Web search also returned irrelevant results. Rather than guessing a URL, the correct action was to mark it unverified."
},
{
"id": 10,
"name": "large-api-response-handling",
"category": "technical_robustness",
"prompt": "Enrich these 25 LinkedIn URLs with business emails using Crustdata people_enrich. [List of 25 URLs]",
"expected_output": "When the API response exceeds token limits and gets saved to a file, the skill should use Python to parse the saved file and extract the needed data, rather than failing or trying to read it inline.",
"assertions": [
{
"text": "Large responses are parsed with Python (json.load + navigation to extract fields)",
"type": "semantic",
"description": "When response is saved to file due to token limits, Python is used to parse it"
},
{
"text": "Parsing handles the nested structure: json.loads(data[0]['text']) to get inner content",
"type": "semantic",
"description": "The saved file format is a JSON array where [0]['text'] contains the actual payload as a string"
}
],
"failure_story": "Crustdata API responses for batch enrichment of 25 profiles frequently exceeded token limits and got saved to files. The saved files have a nested structure — a JSON array where item[0]['text'] is a JSON string containing the actual profiles. Without knowing this structure, extraction fails silently."
},
{
"id": 11,
"name": "no-linkedin-messaging-mentioned",
"category": "pipeline_correctness",
"prompt": "I need to reach out to 5 ML engineer candidates. Run the full pipeline — find them, get emails, and set up outreach.",
"expected_output": "The entire pipeline should use email as the outreach channel. No mention of LinkedIn InMail, LinkedIn messaging, or any LinkedIn outreach channel. Channel in tracker should be 'email' only.",
"assertions": [
{
"text": "No mention of LinkedIn InMail or LinkedIn messaging as an outreach channel",
"type": "negative_pattern",
"pattern": "(linkedin.*(inmail|message|outreach|send)|inmail)"
},
{
"text": "Outreach channel is exclusively email via Gmail drafts",
"type": "semantic",
"description": "All outreach is via email drafts in Gmail, no LinkedIn messaging"
}
],
"failure_story": "The skill should focus exclusively on email outreach via Gmail drafts. LinkedIn messaging is handled by a separate skill and should not be referenced or suggested in this pipeline."
},
{
"id": 12,
"name": "batch-parallel-efficiency",
"category": "technical_robustness",
"prompt": "Find and verify LinkedIn URLs for these 12 candidates: [list of 12 names with companies].",
"expected_output": "LinkedIn verification should be done in batches of 5-6 parallel crustdata_people_search_db calls, not one at a time. Fallback web searches should also be batched for any that failed.",
"assertions": [
{
"text": "Multiple people_search_db calls are made in parallel (batches of 5-6)",
"type": "semantic",
"description": "Verification calls should be batched for efficiency, not sequential"
},
{
"text": "Failed lookups are collected and web_search fallbacks are also batched",
"type": "semantic",
"description": "Web search fallbacks for DB misses should be grouped and run together"
}
],
"failure_story": "Processing 91 candidates one at a time would take hours. The working pattern was batches of 5-6 parallel people_search_db calls, then collecting failures and running web_search fallbacks in a second parallel batch."
},
{
"id": 13,
"name": "founder-exclusion-with-shrinking-check",
"category": "pipeline_correctness",
"prompt": "Source ML engineers for a Founding ML Engineer role at our startup (AI-native data infrastructure, Series A, SF-based). My name is Alex, CTO. Here are some candidates I found from papers and repos: 1) Di Jin, Co-founder at Eigen AI — published several NLP papers at MIT. 2) Sai Surbehera, Co-founder & CTO at Lapis Labs (YC F23) — built vector search infrastructure. 3) Aayush Anand, Co-founder at Level.game — built real-time ML pipelines. 4) Rajat Arya, VP Engineering at XetHub (acquired by HuggingFace) — built model storage layer. Find their LinkedIn profiles, check if we should reach out, and draft emails for the ones we should contact.",
"expected_output": "Di Jin, Sai Surbehera, and Aayush Anand should be flagged as current founders. The skill should check whether their companies are shrinking (using crustdata_company_enrich) before deciding to skip them. Rajat Arya is not a founder and should proceed through the normal pipeline. The tracker should show 'skipped' status for founders whose companies are not shrinking.",
"assertions": [
{
"text": "Identifies Di Jin, Sai Surbehera, and Aayush Anand as current founders based on their titles",
"type": "semantic",
"description": "All three founder/co-founder candidates are flagged before outreach proceeds"
},
{
"text": "Uses crustdata_company_enrich to check company health signals for founder companies",
"type": "tool_usage",
"required_tools": ["crustdata_company_enrich"]
},
{
"text": "Rajat Arya (VP Engineering, not a founder) proceeds through the normal pipeline without founder exclusion checks",
"type": "semantic",
"description": "Non-founder candidates are not subjected to the founder exclusion rule"
},
{
"text": "Founders at healthy or stable companies are marked as skipped, not drafted",
"type": "semantic",
"description": "Tracker entries for founders whose companies are not shrinking should have status 'skipped' with a note explaining why"
},
{
"text": "No outreach email is drafted for founders at healthy companies",
"type": "semantic",
"description": "Gmail drafts should only be created for non-founder candidates (and founders at visibly failing companies)"
}
],
"failure_story": "In a real campaign, 3 active founders (Di Jin at Eigen AI, Sai Surbehera at Lapis Labs, Aayush Anand at Level.game) were included in outreach drafts. All their companies were active — reaching out wasted slots and felt tone-deaf. The fix: check titles for founder/CEO/CTO, verify company health via crustdata_company_enrich, and skip unless all three shrinking signals are present."
},
{
"id": 14,
"name": "personal-email-enrichment-via-crustdata",
"category": "email_enrichment",
"prompt": "Find personal email addresses for these candidates using Crustdata enrichment. I already have their verified LinkedIn URLs: 1) Rajat Arya — https://www.linkedin.com/in/rajatarya (Team Lead at Hugging Face). 2) Anurag Sahu — https://www.linkedin.com/in/anurag-sahu-mle (Staff ML Engineer at Zscaler). Try Crustdata's personal email enrichment first before falling back to GitHub.",
"expected_output": "Should use crustdata_people_enrich with fields including 'personal_contact_info.personal_emails' to request personal emails. Should try this BEFORE falling back to GitHub commit extraction. The enrichment call should include the LinkedIn URLs. If personal emails are returned, they should be non-noreply addresses. If not returned, should fall through to GitHub commit extraction (Phase 3C).",
"assertions": [
{
"text": "Uses crustdata_people_enrich with personal_contact_info.personal_emails in the fields parameter",
"type": "tool_usage",
"required_tools": ["crustdata_people_enrich"],
"description": "The enrichment call must include 'personal_contact_info.personal_emails' in the fields parameter"
},
{
"text": "Personal email enrichment is attempted BEFORE GitHub commit extraction",
"type": "semantic",
"description": "Crustdata personal email (Phase 3B) should be tried before GitHub commits (Phase 3C)"
},
{
"text": "Can combine business_email and personal_contact_info.personal_emails in a single enrichment call",
"type": "semantic",
"description": "The skill should know it can request both business and personal emails in one API call to save round-trips"
},
{
"text": "Falls back to GitHub commit extraction if personal email enrichment returns nothing",
"type": "semantic",
"description": "When personal_contact_info.personal_emails is empty or null, the skill should proceed to Phase 3C (GitHub)"
}
],
"failure_story": "Before this feature existed, the only way to get personal emails was GitHub commit extraction — slow, unreliable for non-engineers, and rate-limited. Crustdata's personal_contact_info.personal_emails field returns personal emails directly from the enrichment API, making it faster and more reliable. The key: always try this before falling back to GitHub."
}
]
}
{
"skill_name": "candidate-sourcing",
"description": "Evals derived from real production errors encountered during a 91-candidate outreach campaign. Each eval targets a specific failure mode that was observed and fixed.",
"evals": [
{
"id": 1,
"name": "linkedin-url-never-guessed",
"category": "linkedin_verification",
"prompt": "Find ML engineers at Princeton who work on NLP. I need their LinkedIn profiles. Here are some names I found from papers: David Park (Princeton grad, now at a startup), Vincent Chen (MIT grad, now at a startup), Abhay Gupta (CMU grad).",
"expected_output": "LinkedIn URLs should be fetched from Crustdata people_search_db or web_search — never constructed from name patterns like 'david-park-princeton' or 'vincent-chen-mit'. The URLs returned should contain auto-generated slugs (e.g., 'david-park-086833264', 'vincent-chen-662a031b5', 'gupta-abhay').",
"assertions": [
{
"text": "No LinkedIn URL contains a school name as suffix (e.g., '-princeton', '-mit', '-cmu')",
"type": "negative_pattern",
"pattern": "linkedin\\.com/in/[a-z-]+(princeton|mit|cmu|stanford|berkeley|harvard|yale)"
},
{
"text": "All LinkedIn URLs were fetched via crustdata_people_search_db or crustdata_web_search, not constructed",
"type": "tool_usage",
"required_tools": ["crustdata_people_search_db"]
},
{
"text": "All LinkedIn URLs use the full https://www.linkedin.com/in/ prefix",
"type": "pattern",
"pattern": "^https://www\\.linkedin\\.com/in/"
}
],
"failure_story": "In the real campaign, URLs like 'david-park-princeton', 'vincent-chen-mit', and 'abhay-gupta-cmu' were all fabricated and either 404'd or pointed to wrong people. The correct slugs were 'david-park-086833264', 'vincent-chen-662a031b5', and 'gupta-abhay' respectively."
},
{
"id": 2,
"name": "linkedin-slug-mismatch-detection",
"category": "linkedin_verification",
"prompt": "I have a candidate list with LinkedIn URLs. Can you verify these are correct? Benoit Rostykus (Nuro, autonomous vehicles) — linkedin.com/in/benoit-rostykus. Hongjun Choi (researcher) — linkedin.com/in/hongjun-choi. Benedict Arockiaraj (Google) — linkedin.com/in/benedict-arockiaraj.",
"expected_output": "Each URL should be verified against Crustdata's people_search_db. The correct slugs are: benoitrostykus (not benoit-rostykus), hongjunchoi92 (not hongjun-choi), benedictflorance (not benedict-arockiaraj). All three should be flagged as mismatches and corrected.",
"assertions": [
{
"text": "Benoit Rostykus slug corrected to 'benoitrostykus'",
"type": "output_contains",
"value": "benoitrostykus"
},
{
"text": "Hongjun Choi slug corrected to 'hongjunchoi92'",
"type": "output_contains",
"value": "hongjunchoi92"
},
{
"text": "Benedict Arockiaraj slug corrected to 'benedictflorance'",
"type": "output_contains",
"value": "benedictflorance"
},
{
"text": "All three original URLs flagged as mismatches",
"type": "semantic",
"description": "The output should clearly indicate that all three provided URLs were incorrect"
}
],
"failure_story": "In the real campaign, 'benoit-rostykus' was a 404, 'hongjun-choi' pointed to a completely different person, and 'benedict-arockiaraj' was also wrong — the person uses 'benedictflorance' on LinkedIn. Simple firstname-lastname patterns are unreliable."
},
{
"id": 3,
"name": "company-name-variation-handling",
"category": "linkedin_verification",
"prompt": "Find the LinkedIn profile for Michael Günther who works at Elastic (the search company). Also find Siyue Wang who works at Microsoft Research on NLP.",
"expected_output": "For Michael Günther, a search with 'Elastic' should be tried, and if it returns wrong results (people at Elastic Email or unrelated), try variations. The slug should contain the ü character properly encoded. For Siyue Wang, if 'Microsoft' returns too many results or zero, try 'Microsoft Research' or include the full name 'Siyue Victoria Wang'.",
"assertions": [
{
"text": "Michael Günther search handles the umlaut character (ü) in the URL",
"type": "semantic",
"description": "The LinkedIn URL should contain either 'günther' or the URL-encoded equivalent 'g%C3%BCnther'"
},
{
"text": "Company name variations were tried when initial search failed",
"type": "tool_usage_count",
"tool": "crustdata_people_search_db",
"min_calls": 2,
"description": "At least one retry with different company name or name variation"
}
],
"failure_story": "In the real campaign, 'Michael Günther' at 'Elastic' returned people from Elastic Email (a different company). Had to adjust. 'Siyue Wang' at 'Microsoft' returned 0 results — found her only by searching 'Siyue Victoria Wang Microsoft'."
},
{
"id": 4,
"name": "malformed-url-prefix-prevention",
"category": "linkedin_verification",
"prompt": "Here are some candidates I found. Add them to my tracker: Pranav Kohli (LinkedIn: linkedin.com/in/pranav-kohli-6a239782), Nicolas Remerscheid (LinkedIn: linkedin.com/in/nicolas-remerscheid), Joseph Miano at Fireworks AI (LinkedIn: linkedin.com/in/joseph-miano).",
"expected_output": "All LinkedIn URLs in the tracker should use the full 'https://www.linkedin.com/in/' prefix, never bare 'linkedin.com/in/'.",
"assertions": [
{
"text": "No tracker entries contain bare 'linkedin.com/in/' without https:// prefix",
"type": "negative_pattern",
"pattern": "^linkedin\\.com/in/"
},
{
"text": "All URLs in tracker start with 'https://www.linkedin.com/in/'",
"type": "pattern",
"pattern": "^https://www\\.linkedin\\.com/in/"
}
],
"failure_story": "In the real campaign, 20 rows (rows 41-60) had URLs starting with bare 'linkedin.com/in/' instead of 'https://www.linkedin.com/in/'. These were malformed and wouldn't work as clickable links."
},
{
"id": 5,
"name": "email-opener-work-based-not-title-based",
"category": "outreach_quality",
"prompt": "Write outreach emails for these candidates for a Founding ML Engineer role at our startup (we're building AI-native data infrastructure, Series A, SF-based). My name is Alex, I'm the CTO. 1) Rajat Arya, VP of Engineering at XetHub (acquired by HuggingFace), built the storage layer for model hosting. 2) Lewis Tunstall, ML Engineer at HuggingFace, co-authored 'NLP with Transformers' book.",
"expected_output": "Each opener should reference specific work (XetHub acquisition, the NLP with Transformers book) rather than job titles or implied capabilities. No opener should contain phrases like 'your experience as VP means...' or 'leading engineering at XetHub demonstrates...'",
"assertions": [
{
"text": "Rajat's opener mentions XetHub or HuggingFace acquisition specifically",
"type": "output_contains_any",
"values": ["XetHub", "acquisition", "HuggingFace"]
},
{
"text": "Lewis's opener mentions the book 'NLP with Transformers'",
"type": "output_contains",
"value": "NLP with Transformers"
},
{
"text": "No opener uses title-based reasoning patterns like 'your role as X means/shows/demonstrates'",
"type": "negative_pattern",
"pattern": "(your (role|position|experience|title|work) (as|at).*(means|shows|demonstrates|implies|suggests))"
},
{
"text": "No opener connects their work to the hiring company's problems in the same sentence as the compliment",
"type": "negative_pattern",
"pattern": "(which is (exactly|precisely|directly) (what|the kind))"
}
],
"failure_story": "Early drafts used patterns like 'Leading applied science at Mistral means you know how to take ML from research to production' — title-based reasoning that reads as a template. The fix: always mention a specific artifact (paper, tool, repo, acquisition) not a job title."
},
{
"id": 6,
"name": "github-email-extraction-with-verification",
"category": "email_enrichment",
"prompt": "Find a personal email for Anurag Sahu who works at Zscaler, previously at Ola Krutrim. He's an IIIT Hyderabad alumnus and likely has a GitHub profile.",
"expected_output": "Should find GitHub profile 'AnuragSahu', verify it belongs to this person (check bio, repos match ML/CV background), find oldest non-fork repo, extract email from commit metadata. Should NOT return a noreply@github.com address. Should verify the GitHub profile actually belongs to this specific Anurag Sahu before extracting email.",
"assertions": [
{
"text": "GitHub profile was verified before extracting email (checked bio or repos against known background)",
"type": "semantic",
"description": "Evidence that the GitHub profile was verified as belonging to the right person"
},
{
"text": "Extracted email is not a noreply address",
"type": "negative_pattern",
"pattern": "noreply"
},
{
"text": "Used oldest non-fork repo for commit email extraction",
"type": "semantic",
"description": "Selected repos sorted by created date ascending, picked non-fork"
}
],
"failure_story": "Crustdata enrichment sometimes returns wrong GitHub profiles for common names. In one case (Ali Kashani, CEO of Serve Robotics), the enrichment returned a GitHub profile 'alikashani' that belonged to a completely different person. Extracting that email would have sent outreach to a stranger."
},
{
"id": 7,
"name": "gmail-draft-not-send",
"category": "pipeline_correctness",
"prompt": "I found 3 great ML engineer candidates. Their emails are verified. Write outreach and send the emails.",
"expected_output": "Should create Gmail DRAFTS (using gmail_create_draft), not attempt to send emails. Should explicitly note to the user that drafts are ready for their review and manual sending.",
"assertions": [
{
"text": "Uses gmail_create_draft tool, not any send tool",
"type": "tool_usage",
"required_tools": ["gmail_create_draft"],
"forbidden_tools": ["gmail_send"]
},
{
"text": "Explicitly tells user that drafts are ready for review and manual sending",
"type": "semantic",
"description": "Output includes clear language about the user reviewing and sending the drafts themselves"
}
],
"failure_story": "Gmail MCP does not support sending — only draft creation. The user reviews each draft and clicks send manually. This is by design: it prevents accidental sends of unreviewed outreach."
},
{
"id": 8,
"name": "tracker-csv-logging-complete",
"category": "pipeline_correctness",
"prompt": "Source 2 ML engineers from IIIT Hyderabad alumni working at top tech companies, find their emails, write outreach for Founding ML Engineer role, and create Gmail drafts.",
"expected_output": "After creating Gmail drafts, each candidate should be logged to the outreach tracker CSV with all fields populated: date, role, name, current_role, company, school_connection, linkedin_url, email, channel (email), subject, message_opener, status (drafted), notes, message_body.",
"assertions": [
{
"text": "Tracker CSV is created/updated with all required columns",
"type": "semantic",
"description": "CSV contains: date, role_hiring_for, candidate_name, current_role, company, school_connection, linkedin_url, email, channel, subject, message_opener, status, notes, message_body"
},
{
"text": "Status is set to 'drafted' not 'sent'",
"type": "output_contains",
"value": "drafted"
},
{
"text": "Channel is set to 'email' only",
"type": "semantic",
"description": "Channel field should be 'email', not 'linkedin_inmail' or 'email + linkedin_inmail'"
},
{
"text": "LinkedIn URLs in tracker are verified (not guessed)",
"type": "semantic",
"description": "URLs should come from Crustdata verification, not be constructed from name patterns"
}
],
"failure_story": "Incomplete tracker logging means losing track of who was contacted, through which channel, and with what message. This makes it impossible to follow up systematically or avoid double-contacting candidates."
},
{
"id": 9,
"name": "waterfall-fallback-on-db-miss",
"category": "linkedin_verification",
"prompt": "Find the LinkedIn profile for Aakash Kumar Nain who works at Ola (the Indian ride-hailing company) as a deep learning engineer.",
"expected_output": "crustdata_people_search_db should be tried first with 'Aakash Kumar Nain' + 'Ola'. If that returns 0 results, crustdata_web_search should be used as fallback with 'Aakash Kumar Nain Ola deep learning site:linkedin.com'. If web search also fails, the candidate should be marked as 'LinkedIn URL unverified' — NOT given a guessed URL.",
"assertions": [
{
"text": "people_search_db was tried first",
"type": "tool_usage_order",
"first": "crustdata_people_search_db",
"then": "crustdata_web_search"
},
{
"text": "Web search fallback was attempted after DB returned 0 results",
"type": "tool_usage",
"required_tools": ["crustdata_web_search"]
},
{
"text": "If both fail, candidate is NOT given a fabricated LinkedIn URL",
"type": "semantic",
"description": "When both search methods fail, the URL should be marked unverified or left empty, never constructed"
}
],
"failure_story": "In the real campaign, Aakash Kumar Nain could not be found in Crustdata's DB (0 results for name + 'Ola'). Web search also returned irrelevant results. Rather than guessing a URL, the correct action was to mark it unverified."
},
{
"id": 10,
"name": "large-api-response-handling",
"category": "technical_robustness",
"prompt": "Enrich these 25 LinkedIn URLs with business emails using Crustdata people_enrich. [List of 25 URLs]",
"expected_output": "When the API response exceeds token limits and gets saved to a file, the skill should use Python to parse the saved file and extract the needed data, rather than failing or trying to read it inline.",
"assertions": [
{
"text": "Large responses are parsed with Python (json.load + navigation to extract fields)",
"type": "semantic",
"description": "When response is saved to file due to token limits, Python is used to parse it"
},
{
"text": "Parsing handles the nested structure: json.loads(data[0]['text']) to get inner content",
"type": "semantic",
"description": "The saved file format is a JSON array where [0]['text'] contains the actual payload as a string"
}
],
"failure_story": "Crustdata API responses for batch enrichment of 25 profiles frequently exceeded token limits and got saved to files. The saved files have a nested structure — a JSON array where item[0]['text'] is a JSON string containing the actual profiles. Without knowing this structure, extraction fails silently."
},
{
"id": 11,
"name": "no-linkedin-messaging-mentioned",
"category": "pipeline_correctness",
"prompt": "I need to reach out to 5 ML engineer candidates. Run the full pipeline — find them, get emails, and set up outreach.",
"expected_output": "The entire pipeline should use email as the outreach channel. No mention of LinkedIn InMail, LinkedIn messaging, or any LinkedIn outreach channel. Channel in tracker should be 'email' only.",
"assertions": [
{
"text": "No mention of LinkedIn InMail or LinkedIn messaging as an outreach channel",
"type": "negative_pattern",
"pattern": "(linkedin.*(inmail|message|outreach|send)|inmail)"
},
{
"text": "Outreach channel is exclusively email via Gmail drafts",
"type": "semantic",
"description": "All outreach is via email drafts in Gmail, no LinkedIn messaging"
}
],
"failure_story": "The skill should focus exclusively on email outreach via Gmail drafts. LinkedIn messaging is handled by a separate skill and should not be referenced or suggested in this pipeline."
},
{
"id": 12,
"name": "batch-parallel-efficiency",
"category": "technical_robustness",
"prompt": "Find and verify LinkedIn URLs for these 12 candidates: [list of 12 names with companies].",
"expected_output": "LinkedIn verification should be done in batches of 5-6 parallel crustdata_people_search_db calls, not one at a time. Fallback web searches should also be batched for any that failed.",
"assertions": [
{
"text": "Multiple people_search_db calls are made in parallel (batches of 5-6)",
"type": "semantic",
"description": "Verification calls should be batched for efficiency, not sequential"
},
{
"text": "Failed lookups are collected and web_search fallbacks are also batched",
"type": "semantic",
"description": "Web search fallbacks for DB misses should be grouped and run together"
}
],
"failure_story": "Processing 91 candidates one at a time would take hours. The working pattern was batches of 5-6 parallel people_search_db calls, then collecting failures and running web_search fallbacks in a second parallel batch."
},
{
"id": 13,
"name": "founder-exclusion-with-shrinking-check",
"category": "pipeline_correctness",
"prompt": "Source ML engineers for a Founding ML Engineer role at our startup (AI-native data infrastructure, Series A, SF-based). My name is Alex, CTO. Here are some candidates I found from papers and repos: 1) Di Jin, Co-founder at Eigen AI — published several NLP papers at MIT. 2) Sai Surbehera, Co-founder & CTO at Lapis Labs (YC F23) — built vector search infrastructure. 3) Aayush Anand, Co-founder at Level.game — built real-time ML pipelines. 4) Rajat Arya, VP Engineering at XetHub (acquired by HuggingFace) — built model storage layer. Find their LinkedIn profiles, check if we should reach out, and draft emails for the ones we should contact.",
"expected_output": "Di Jin, Sai Surbehera, and Aayush Anand should be flagged as current founders. The skill should check whether their companies are shrinking (using crustdata_company_enrich) before deciding to skip them. Rajat Arya is not a founder and should proceed through the normal pipeline. The tracker should show 'skipped' status for founders whose companies are not shrinking.",
"assertions": [
{
"text": "Identifies Di Jin, Sai Surbehera, and Aayush Anand as current founders based on their titles",
"type": "semantic",
"description": "All three founder/co-founder candidates are flagged before outreach proceeds"
},
{
"text": "Uses crustdata_company_enrich to check company health signals for founder companies",
"type": "tool_usage",
"required_tools": ["crustdata_company_enrich"]
},
{
"text": "Rajat Arya (VP Engineering, not a founder) proceeds through the normal pipeline without founder exclusion checks",
"type": "semantic",
"description": "Non-founder candidates are not subjected to the founder exclusion rule"
},
{
"text": "Founders at healthy or stable companies are marked as skipped, not drafted",
"type": "semantic",
"description": "Tracker entries for founders whose companies are not shrinking should have status 'skipped' with a note explaining why"
},
{
"text": "No outreach email is drafted for founders at healthy companies",
"type": "semantic",
"description": "Gmail drafts should only be created for non-founder candidates (and founders at visibly failing companies)"
}
],
"failure_story": "In a real campaign, 3 active founders (Di Jin at Eigen AI, Sai Surbehera at Lapis Labs, Aayush Anand at Level.game) were included in outreach drafts. All their companies were active — reaching out wasted slots and felt tone-deaf. The fix: check titles for founder/CEO/CTO, verify company health via crustdata_company_enrich, and skip unless all three shrinking signals are present."
},
{
"id": 14,
"name": "personal-email-enrichment-via-crustdata",
"category": "email_enrichment",
"prompt": "Find personal email addresses for these candidates using Crustdata enrichment. I already have their verified LinkedIn URLs: 1) Rajat Arya — https://www.linkedin.com/in/rajatarya (Team Lead at Hugging Face). 2) Anurag Sahu — https://www.linkedin.com/in/anurag-sahu-mle (Staff ML Engineer at Zscaler). Try Crustdata's personal email enrichment first before falling back to GitHub.",
"expected_output": "Should use crustdata_people_enrich with fields including 'personal_contact_info.personal_emails' to request personal emails. Should try this BEFORE falling back to GitHub commit extraction. The enrichment call should include the LinkedIn URLs. If personal emails are returned, they should be non-noreply addresses. If not returned, should fall through to GitHub commit extraction (Phase 3C).",
"assertions": [
{
"text": "Uses crustdata_people_enrich with personal_contact_info.personal_emails in the fields parameter",
"type": "tool_usage",
"required_tools": ["crustdata_people_enrich"],
"description": "The enrichment call must include 'personal_contact_info.personal_emails' in the fields parameter"
},
{
"text": "Personal email enrichment is attempted BEFORE GitHub commit extraction",
"type": "semantic",
"description": "Crustdata personal email (Phase 3B) should be tried before GitHub commits (Phase 3C)"
},
{
"text": "Can combine business_email and personal_contact_info.personal_emails in a single enrichment call",
"type": "semantic",
"description": "The skill should know it can request both business and personal emails in one API call to save round-trips"
},
{
"text": "Falls back to GitHub commit extraction if personal email enrichment returns nothing",
"type": "semantic",
"description": "When personal_contact_info.personal_emails is empty or null, the skill should proceed to Phase 3C (GitHub)"
}
],
"failure_story": "Before this feature existed, the only way to get personal emails was GitHub commit extraction — slow, unreliable for non-engineers, and rate-limited. Crustdata's personal_contact_info.personal_emails field returns personal emails directly from the enrichment API, making it faster and more reliable. The key: always try this before falling back to GitHub."
}
]
}
Candidate Sourcing
Go from "I need to hire for role X" to Gmail drafts ready to send — powered by Crustdata's real-time people data APIs. An end-to-end AI recruiting workflow for founders who want to own hiring without paying $20K per placement to a recruiter or $100+/month for Juicebox.
What it does
Give it a role description and it handles the entire pipeline:
1. Finds candidates matching your specific criteria via Crustdata's 1B+ person database and web search 2. Verifies LinkedIn profiles — never guesses URLs, always confirms through Crustdata (guessed URLs caused 12 errors in a real 91-person campaign) 3. Finds email addresses — business emails via Crustdata enrichment API, personal emails via Crustdata web search 4. Writes personalized outreach — each email references the candidate's specific work (papers, repos, projects), not generic job-title flattery 5. Creates Gmail drafts — ready for you to review and click send
Example
Input:
"Find 5 ML engineers who've published on retrieval-augmented generation. Prioritize people at startups, not FAANG. Draft outreach for our Founding ML Engineer role at [your company]."
Output:
| Candidate | Company | Why them | Status | |
|---|---|---|---|---|
| Jane Chen | Cohere | First author on RAG benchmarking paper (ACL 2024) | jane@cohere.com | Draft ready |
| Raj Patel | Pinecone | Built open-source RAG evaluation toolkit (2.3K GitHub stars) | raj@gmail.com | Draft ready |
| Maria Lopez | Weaviate | Led vector search team, speaks at MLOps conferences | maria@weaviate.io | Draft ready |
| ... | ... | ... | ... | ... |
Each Gmail draft has a personalized opener referencing their specific work — not "your experience as an ML Engineer demonstrates..."
Why not Juicebox, HireEZ, or a recruiter?
| Recruiter | Juicebox / HireEZ | This skill | |
|---|---|---|---|
| Cost | $15-30K per hire (15-25% of salary) | $79-199/month per seat | Pay-per-use via Crustdata API |
| Ranking | Recruiter's judgment (variable) | Black-box AI — surfaces the same "obvious" candidates everyone else is targeting | You define the criteria — proof of work, hunger signals, relevance to your problems |
| Outreach | Generic templates or recruiter writes | Template-based sequences | Each email references specific work the candidate has done |
| Control | Low — recruiter runs the process | Medium — you search but AI ranks | Full — you see every step, review every draft before sending |
| Risk | LinkedIn account bans from tools | Reported LinkedIn suspensions from browser extensions | No scraping, no LinkedIn violations |
Use cases
- Seed/Series A founders sourcing their first 5-10 engineers without a recruiter
- Hiring managers who want to own top-of-funnel instead of waiting for recruiters to deliver
- Technical founders who know exactly what "good" looks like and want to define their own ranking criteria
- Anyone tired of Juicebox surfacing the same senior FAANG engineers that every other startup is pitching
How it works (under the hood)
The skill runs 5 phases in sequence:
1. Define & search — extracts role criteria, searches Crustdata people database + arXiv/GitHub/web for candidates matching your specific technical problems 2. Verify LinkedIn — waterfall lookup: Crustdata people DB → Crustdata web search → mark unverified. Never fabricates URLs 3. Find emails — Crustdata people enrichment (batch 25 at a time) for business emails, Crustdata web search for personal emails 4. Write outreach — personalized openers based on each candidate's proof of work (papers, repos, projects), not job titles 5. Create Gmail drafts + tracker — drafts in your Gmail, CSV tracker logging every candidate and their status
Setup
Claude.ai (web) or Claude Desktop (macOS/Windows): 1. Go to Settings → Connectors → "Add custom connector" → paste https://mcp.crustdata.com/mcp → click "Add" (step-by-step guide) 2. Find Gmail in your Connectors list → click "Connect" 3. Download the `.skill` file and upload it to your project 4. Tell Claude: "I need to hire a [role]. Find candidates and set up outreach."
Claude Code (CLI): 1. Add the Crustdata MCP server and Gmail MCP server to your config 2. Import this skill directory into your workspace 3. Tell Claude: "I need to hire a [role]. Find candidates and set up outreach."
Evals
12 test cases derived from real production errors in a 91-candidate outreach campaign. Covers LinkedIn URL verification, email extraction, outreach quality, and pipeline correctness. Battle-tested against actual failure modes — not synthetic examples.
See `evals/evals.json`.