The Problem
You get a new signup. Maybe it's someone from a large enterprise, maybe a startup founder deploying their first agent. You want to reach out and help them - but manually researching every signup doesn't scale.
What if an AI agent could do that research for you, the moment someone signs up?
How It Works
The Lead Research Agent is a template built with 21st SDK and powered by Claude Code. Here's the flow:
- A new event happens - someone deploys an agent, signs up, or submits a form
- The agent is called with a message like "research this email"
- Claude Code researches the user using built-in internet search tools (or optionally Exa API for richer people/company data)
- Lead criteria are evaluated - you define what makes a lead interesting (company type, role, industry, green/red flags)
- If the lead is interesting - the agent sends a Slack or Telegram message with the research summary
The whole thing runs autonomously. You just define your criteria and let it work.
What You Can Customize
Lead Criteria
The template includes a customizable skill file where you define exactly what matters to you:
- Company types - startups, enterprise, agencies, dev shops
- Target roles - founders, CTOs, engineering leads, PMs
- Industries - AI/ML, dev tools, SaaS, fintech, healthtech
- Green flags - company email domain, recent agent deploy, technical background + startup combo
- Red flags - personal email for B2B outreach, enterprise without clear contact person
Notification Channels
- Slack - uses incoming webhooks, easy to set up
- Telegram - native bot API, great for mobile alerts
You can use both or just one.
Search Tools
The agent uses Claude Code's built-in WebSearch and WebFetch tools by default. If you add an EXA_API_KEY, it gets access to Exa's neural search which is particularly good at finding information about people and companies.
Getting Started
Clone the template and deploy in under a minute:
git clone https://github.com/21st-dev/21st-sdk-examples.git
cd 21st-sdk-examples/lead-research-agent
npm install
npx @21st-sdk/cli login
npx @21st-sdk/cli deploy
Set Up Slack
- Go to api.slack.com/apps → Create New App → From scratch
- Enable Incoming Webhooks → Add to Workspace
- In the 21st dashboard, add the
SLACK_WEBHOOK_URLenvironment variable - Redeploy
Set Up Telegram (Optional)
- Message @BotFather on Telegram to create a bot
- Get your chat ID
- Add
TELEGRAM_BOT_TOKENandTELEGRAM_CHAT_IDas environment variables - Redeploy
Add Exa (Optional)
Add EXA_API_KEY to your environment variables for richer search results on people and companies.
Example Prompts
Once deployed, you can call the agent with messages like:
"Research john@acme.ai - just deployed their first agent""Look up Jane Doe from Stripe""Is sarah@startup.io worth reaching out to?"
The agent will research the person, evaluate against your criteria, and send you an alert if they're worth reaching out to. If it's a placeholder or not interesting - it just skips quietly.
Build Your Own
This template is a starting point. Fork it, change the criteria, add your own notification channels, or wire it into your signup webhook. The 21st SDK makes it straightforward to deploy and manage.
Check the full source on GitHub and build your agent with 21st SDK.
Agent Template
The full agent source - drop it into your project and customize the criteria, tools, and notification channels.
import { agent, tool } from "@21st-sdk/agent"
import Exa from "exa-js"
import { readFileSync, existsSync } from "fs"
import { z } from "zod"
function getSlackWebhookUrl(): string {
if (process.env.SLACK_WEBHOOK_URL) return process.env.SLACK_WEBHOOK_URL
try {
const envPath = "/home/user/.env"
if (existsSync(envPath)) {
const raw = readFileSync(envPath, "utf-8")
for (const line of raw.split("\n")) {
const match = line.match(/^([^#=]+)=(.*)$/)
if (match) process.env[match[1].trim()] ??= match[2].trim()
}
}
} catch {}
return process.env.SLACK_WEBHOOK_URL || ""
}
function readLeadCriteria(): string {
const paths = [
"/home/user/skills/lead-criteria.md",
"skills/lead-criteria.md",
]
for (const p of paths) {
try {
if (existsSync(p)) return readFileSync(p, "utf-8")
} catch {}
}
return "Consider: company domain email, technical role, startup/tech company, recent agent deploy."
}
export default agent({
model: "claude-sonnet-4-6",
runtime: "claude-code",
permissionMode: "bypassPermissions",
maxTurns: 25,
systemPrompt: `You are a lead research agent. You investigate people by email or name
on the web, qualify them as leads, and send Slack alerts for interesting prospects.
WORKFLOW:
1. Call readLeadCriteria to get qualification rules.
2. Search: use exaSearch if available, otherwise use the built-in WebSearch.
3. Use built-in WebFetch to open promising URLs (LinkedIn, company site, GitHub).
4. Decide if the lead is "interesting" based on criteria.
5. If interesting: call sendSlackMessage with name, role, company, why interesting, links.
If not interesting: summarize only, do NOT call sendSlackMessage.`,
tools: {
exaSearch: tool({
description: "Search the web via Exa.ai (better for people, companies).",
inputSchema: z.object({
query: z.string().describe("Search query"),
}),
execute: async ({ query }) => {
const key = process.env.EXA_API_KEY
if (!key) return { content: [{ type: "text", text: JSON.stringify({ available: false }) }] }
const exa = new Exa(key)
const result = await exa.search(query, { numResults: 8, contents: { text: true } })
return { content: [{ type: "text", text: JSON.stringify({ results: result.results }) }] }
},
}),
sendSlackMessage: tool({
description: "Send a Slack alert. Call ONLY when the lead qualifies.",
inputSchema: z.object({
text: z.string().describe("Alert message: name, role, company, why interesting, links"),
}),
execute: async ({ text }) => {
const webhookUrl = getSlackWebhookUrl()
if (!webhookUrl) return { content: [{ type: "text", text: JSON.stringify({ sent: false }) }], isError: true }
const res = await fetch(webhookUrl, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ text }),
})
return { content: [{ type: "text", text: JSON.stringify({ sent: res.ok }) }] }
},
}),
readLeadCriteria: tool({
description: "Read the lead qualification criteria from skills/lead-criteria.md.",
inputSchema: z.object({}),
execute: async () => {
return { content: [{ type: "text", text: JSON.stringify({ criteria: readLeadCriteria() }) }] }
},
}),
},
})


