event-prospecting
browserbase/skills
該工具會接收會議或活動講者的網址,從中提取人員資料,依據使用者的目標客戶群(ICP)篩選其所屬公司,並僅針對來自符合 ICP 標準公司的講者進行深度研究。最終輸出以人為本的 HTML 報告,並針對每位講者提供「為何應聯繫」的理由說明。
...展開全部活動潛在客戶開發
輸入會議網址 → 取得業務代表應聯繫的人員排名清單,並針對每位人員提供「聯繫理由」。
必備條件:環境變數BROWSERBASE_API_KEY及已安裝的browse命令列工具(npm install -g browse)。使用browse cloud ...進行 API 呼叫,並使用browse open/browse get markdown處理以 JavaScript 為主的講者頁面。
路徑規則:在所有 Bash 指令中務必使用完整的字面路徑 — 切勿使用~或$HOME(兩者都會觸發「殼層擴展語法」的確認提示)。 將主目錄解析一次後,在所有地方皆使用該路徑。在建構子代理程式提示時,請將{SKILL_DIR}替換為完整的字面路徑(通常為/Users/jay/skills/skills/event-prospecting )。
輸出目錄:所有活動潛在客戶開發的輸出皆存至~/Desktop/{event_slug}_prospects_{YYYY-MM-DD-HHMM}/。 最終交付成果為index.html(按公司分組,並依公司 ICP 排序),另提供companies.html和people.html(可篩選)作為替代檢視方式,以及供冷呼出系統匯入用的results.csv 檔案。
重要 — 工具限制(適用於主代理程式及所有子代理程式):
- 所有網頁搜尋:請使用「
瀏覽雲端搜尋」。切勿使用 WebSearch。 - 所有頁面內容擷取:請使用
node {SKILL_DIR}/scripts/extract_page.mjs "。 此腳本透過" 「瀏覽雲端擷取 --output」進行抓取,解析標題、元標籤及可見的正文內容,並在抓取失敗或返回由 JavaScript 渲染的簡略內容時,自動回退至「瀏覽雲端擷取 markdown」。切勿手動編寫「瀏覽雲端擷取 | sed」處理流程。切勿使用 WebFetch。 - 所有研究成果:子代理程式應針對每家公司或每人,使用 bash heredoc 將內容寫入
{OUTPUT_DIR}/companies/{slug}.md或{OUTPUT_DIR}/people/{slug}.md中的單一 Markdown 檔案。切勿使用 Write 工具或python3 -c。 請參閱references/example-research.md以了解兩種檔案格式。 - 報告彙編:請使用
node {SKILL_DIR}/scripts/compile_report.mjs {OUTPUT_DIR} --open。 - 子代理必須僅使用 Bash 工具。不允許使用其他工具。
- 嚴格的工具呼叫上限:ICP 篩選 = 每家公司 1 次呼叫;深度研究 = 每家公司 5 次呼叫;人物資料豐富化 = 每人 4 次呼叫。執行細節請參閱
references/workflow.md。
關鍵 — 反幻覺規則(適用於主代理程式及所有子代理程式):
- 切勿根據網站的字型、框架、設計系統或排版來推斷
產品描述、產業類別或個人的職務原因。這些僅屬表面特徵,無法反映該公司銷售的產品或該人員的職務內容。 - 絕不可讓使用者自身的 ICP 滲入目標對象的描述中。若您不清楚目標對象的業務內容,請寫上「
未知」——切勿將其與 ICP 進行模式比對。 product_description必須直接引用或改寫自extract_page.mjs輸出中的特定短語。若 TITLE/META/OG/HEADINGS/BODY 均未產生可辨識的產品陳述,請寫上「未知」——表示無法存取首頁內容,並將icp_fit_score上限設為 3。- 個人
鉤子(hook)必須直接引用或改寫來自瀏覽雲端搜尋結果的特定發現(例如:播客標題、部落格標題、GitHub 儲存庫、演講摘要)。若過去 6 個月內不存在公開訊號,則回退至活動脈絡(該人士在此活動中的演講標題)。
關鍵 — 盡量減少權限提示:
- 子代理程式必須使用鏈式 heredocs,將所有檔案寫入作業批次處理為單一 Bash 呼叫。一次 Bash 呼叫 = 一次權限提示。
- 使用
&&鏈接將所有搜尋與所有資料擷取批次整合為單一 Bash 呼叫。
流程概覽
請依序執行以下 10 個步驟。請勿跳過步驟或更改順序。
- 設定— 輸出目錄 + 清空狀態
- 載入設定檔— 讀取
profiles/{user_slug}.json - 偵測— 識別事件平台
- 提取人員—
people.jsonl - 依公司分組—
seed_companies.txt - ICP 篩選— 快速公司層級評分(每家公司 1 次呼叫)
- 篩選— ICP 契合度分數 (
icp_fit_score) 大於或等於 --icp-threshold的公司 - 深度研究— 針對 ICP 契合度執行完整的「規劃→研究→綜合分析」流程
- 豐富講者資訊— 詢問使用者:僅限 ICP 契合度(預設)或所有講者
- 彙編報告— HTML + CSV,於瀏覽器中開啟
使用者會透過類似/event-prospecting EVENT_URL。預設值:DEPTH=deep、ICP_THRESHOLD=6。USER_SLUG(ICP 設定檔)將在步驟 1 中根據本地端現有的任何設定檔自動解析 — 沒有內建的預設設定檔。請勿要求使用者確認 URL — 他們已經提供給您了。
步驟 0:設定輸出目錄
根據使用者提供的 URL 推導出輸出目錄。請勿硬編碼任何事件名稱。
# EVENT_URL 來自呼叫訊息(即使用者在 `/event-prospecting` 之後輸入的內容)
EVENT_SLUG=$(node -e 'const h = new URL(process.argv[1]).hostname.replace(/^www\./,""); console.log(h.split(".")[0])' "$EVENT_URL")
TIMESTAMP=$(date +%Y-%m-%d-%H%M)
OUTPUT_DIR=/Users/jay/Desktop/${EVENT_SLUG}_prospects_${TIMESTAMP}
mkdir -p "$OUTPUT_DIR/companies" "$OUTPUT_DIR/people"
請使用完整的實字路徑 — 切勿使用~或$HOME。將{OUTPUT_DIR}作為完整的實字路徑傳遞給所有子代理程式提示。
步驟 1:載入使用者設定檔
此設定檔定義了 ICP,ICP 分流與深度研究即以此為基準進行評分。請從{SKILL_DIR}/profiles/{user_slug}.json載入(此檔案可在所有 GTM 技能間通用 — 結構與 company-research 相同)。example.json僅為範本,並非真實設定檔 — 切勿使用。
請勿在{SKILL_DIR}/profiles/目錄之外搜尋設定檔 — 切勿存取其他技能的目錄。若需在其他位置使用設定檔,使用者應明確將其複製過去。
解析順序:
- 若使用者透過
--user-company呼叫技能,則使用該 slug。 - 否則,列出
profiles/*.json(不包含example.json)。若僅存在一個個人檔案,則使用該檔案(並告知使用者是哪一個);若存在多個,則透過純文字對話詢問使用者要選用哪一個。 - 若不存在任何個人檔案,則明確顯示錯誤訊息,並指示使用者建立一個(將
profiles/example.json複製到profiles/並填寫內容,或執行「公司研究」技能以自動建立一個)。.json
PROFILES=$(ls {SKILL_DIR}/profiles/*.json 2>/dev/null | xargs -n1 basename | sed 's/\.json$//' | grep -v '^example$')
COUNT=$(echo "$PROFILES" | grep -c .)
if [ -z "$USER_SLUG" ]; then
if [ "$COUNT" -eq 0 ]; then
echo "在 {SKILL_DIR}/profiles/ 中未找到任何設定檔。請將 profiles/example.json 複製到 profiles/.json 並填寫內容,或執行 company-research 技能來建立一個。"
exit 1
elif [ "$COUNT" -eq 1 ]; then
USER_SLUG=$PROFILES
echo "正在使用唯一可用的個人資料:${USER_SLUG}"
else
echo "找到多個個人資料:"
echo "$PROFILES" | sed 's/^/ - /'
echo "請使用 --user-company 重新執行以選擇其中一個。"
exit 1
fi
fi
test -f {SKILL_DIR}/profiles/${USER_SLUG}.json || {
echo "找不到設定檔:profiles/${USER_SLUG}.json"
exit 1
}
cat {SKILL_DIR}/profiles/${USER_SLUG}.json
該設定檔包含:company、product、ICP_description、existing_customers。這些內容將原樣嵌入後續每個子代理的提示字串中。
步驟 2:偵測
偵測事件平台與資料擷取策略。單一指令:
node {SKILL_DIR}/scripts/recon.mjs {EVENT_URL} {OUTPUT_DIR}
將平台、策略以及(針對 Next.js)nextDataPaths 寫入{OUTPUT_DIR}/recon.json。請參閱references/event-platforms.md以了解平台目錄與偵測優先級。
預期結果:
- Stripe Sessions 類別(Next.js):
platform: "next-data",1-3 個路徑 - Sessionize:
platform: "sessionize" - Lu.ma / Eventbrite:
platform: "luma" | "eventbrite" - 其他情況:
platform: "custom",strategy: "markdown"(盡力而為的備用方案)
步驟 3:提取人員資料
node {SKILL_DIR}/scripts/extract_event.mjs {OUTPUT_DIR} --user-company {USER_SLUG}
讀取recon.json,將資料分派至特定平台的萃取器,並寫入people.jsonl(每行一位講者)和seed_companies.txt(已去重之公司)。
--user-company參數還會將主辦組織自身的員工(例如由 Stripe 主辦的活動會排除 Stripe 員工)以及使用者自身公司的員工從講者清單中移除——這些人並非潛在客戶。
對輸出結果進行合理性檢查:
wc -l {OUTPUT_DIR}/people.jsonl {OUTPUT_DIR}/seed_companies.txt
head -3 {OUTPUT_DIR}/people.jsonl
若people.jsonl為空或少於 10 行,表示 recon 選錯了平台 — 請參閱references/event-platforms.md並調整策略後重新執行。
步驟 4:按公司分組
extract_event.mjs已輸出seed_companies.txt(每行一家公司,已去重並排序)。此步驟僅供參考——在進行扇形擴展前,請先確認數量是否合理:
wc -l {OUTPUT_DIR}/seed_companies.txt
預期值:約為講者人數的 0.4-0.6 倍(多數活動平均每家公司約有 2 位講者,部分公司派出 5 位以上,許多公司則僅派 1 位)。
步驟 5:ICP 篩選
快速篩選 — 每家公司僅執行一次工具調用,不進行深入研究。針對`seed_companies.txt` 中的每家公司,根據使用者的 ICP 進行評分,並在`companies/{slug}.md` 中寫入簡短的篩選摘要。ICP 契合分數 (icp_fit_score) ≥ --icp-threshold(預設值 6)的公司將進入步驟 7 的深度研究;其餘則保留為篩選摘要。
調度模式:將seed_companies.txt分成約 10 個批次,並在單一 Agent 批次中分派 N 個子代理(單一訊息中包含多個 Agent 工具呼叫)。每個子代理執行references/workflow.md→ 「ICP 篩選」章節中的提示。 硬性限制:每家公司僅限一次工具呼叫(僅針對首頁執行extract_page.mjs),透過# browse 呼叫 N/1註解模式來強制執行。
# 建立批次檔案:每行批次內容為「名稱|推測首頁|slug」。
# extract_event.mjs 僅輸出公司名稱(不含 URL),因此我們會進行 slug 化並推測
# https://{slug-without-spaces}.com 作為標準首頁。 分流子代理
# 若推測的 URL 返回 404 錯誤,則允許寫入 product_description: "未知 — 無法存取首頁內容"
# 並將評分上限設為 3 — 這是
# workflow.md 中記載的預設處理方式(ICP 分流提示的規則 3)。 若執行實際的瀏覽雲端搜尋來
# 找出該 URL,將會嚴重違反「每家公司僅限一次呼叫」的硬性限制。
node -e '
const fs = require("fs");
const slugify = (s) => (s || "").toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
const seed = fs.readFileSync("{OUTPUT_DIR}/seed_companies.txt", "utf-8").split("\n").filter(Boolean);
const lines = seed.map(c => {
const slug = slugify(c);
const guessedHost = c.toLowerCase().replace(/[^a-z0-9]/g, "");
return `${c}|https://${guessedHost}.com|${slug}`;
});
fs.writeFileSync("{OUTPUT_DIR}/_seed_with_urls.txt", lines.join("\n") + "\n");
'
# 拆分為約 10 家公司的批次
split -l 10 {OUTPUT_DIR}/_seed_with_urls.txt {OUTPUT_DIR}/_batch_triage_
# 計算批次數 → 需派遣的子代理數量(每則訊息上限 6 個;其餘則安排第二波)
ls {OUTPUT_DIR}/_batch_triage_* | wc -l
接著在單一訊息中,針對每個批次派遣一個 Agent 呼叫(最多可並行處理 6 個;待第一波返回後再處理後續批次)。每個 Agent 會從references/workflow.md取得提示 → 「ICP 分流」,並在發送前進行以下替換:
{SKILL_DIR}→ 完整字面技能路徑(例如/Users/jay/skills/skills/event-prospecting){OUTPUT_DIR}→ 完整的輸出路徑{USER_COMPANY}、{USER_PRODUCT}、{ICP_DESCRIPTION}→ 取自已載入的個人檔案{EVENT_NAME}→recon.json中的.title{COMPANY_LIST}→ 批次檔案的內容(例如:cat {OUTPUT_DIR}/_batch_triage_aa){TOTAL}→ 此批次中的行數(代入# browse 呼叫 N/{TOTAL})
代理發送(骨架,每批次重複一次並封裝於單一訊息中):
Agent(
description: "ICP 分流批次 aa",
prompt:,
subagent_type: "通用型"
)
Agent(
description: "ICP 分流批次 ab",
prompt:,
subagent_type: "general-purpose"
)
... 每則訊息最多 6 個
待所有子代理返回後,請驗證seed_companies.txt中的每間公司是否都有對應的companies/{slug}.md 檔案:
ls {OUTPUT_DIR}/companies/*.md | wc -l
# 結果應等於 `wc -l {OUTPUT_DIR}/seed_companies.txt`
清理批次檔案:rm {OUTPUT_DIR}/_batch_triage_*.
步驟 6:根據 ICP 閾值進行篩選
讀取每個companies/*.md的前置資訊,保留icp_fit_score >= 6(或任何--icp-threshold設定值)的項目。將篩選通過的公司 slug 寫入{OUTPUT_DIR}/icp_fits.txt:
THRESHOLD=6 # 來自 --icp-threshold 參數
for f in {OUTPUT_DIR}/companies/*.md; do
score=$(awk '/^icp_fit_score:/{print $2; exit}' "$f")
if [ -n "$score" ] && [ "$score" -ge "$THRESHOLD" ]; then
basename "$f" .md
fi
done > {OUTPUT_DIR}/icp_fits.txt
wc -l {OUTPUT_DIR}/icp_fits.txt
預期結果:約為seed_companies.txt 檔案中 20-40% 的比例。若存活率低於 10%,則閾值可能過高,或 ICP 描述過於狹窄 — 應向使用者顯示警告訊息。
步驟 7:深度研究
僅針對符合 ICP 標準的公司執行「完整計畫→研究→整合」流程。硬性上限:每家公司最多 5 次工具調用(官網資料擷取 + 2-3 次子問題搜尋 + 1-2 次補充資料擷取)。 子代理程式將現有的companies/{slug}.md篩選草稿覆寫為內容更豐富的深度研究版本(前置資訊中將triage_only 設為 false)。
分派模式:將icp_fits.txt拆分為約 5 筆的批次(深度模式預設),並以單一訊息將每批次分配給一個代理程式(每則訊息最多 6 個代理程式)。每個代理程式從references/workflow.md→ "深度研究" 取得提示,並進行以下替換:
{SKILL_DIR}、{OUTPUT_DIR}、{USER_COMPANY}、{USER_PRODUCT}、{ICP_DESCRIPTION}{EVENT_NAME}(取自recon.json中的.title),{EVENT_CONTEXT}(追蹤項目/主題,從事件首頁手動推斷){COMPANY_LIST}→ 批次檔案的內容(每行格式為slug|website)
# 透過讀取每個分流存根的前置資訊來建立 {公司-slug|網站} 對
while read slug; do
website=$(awk '/^website:/{print $2; exit}' {OUTPUT_DIR}/companies/${slug}.md)
echo "${slug}|${website}"
done < {OUTPUT_DIR}/icp_fits.txt > {OUTPUT_DIR}/_deep_targets.txt
# 拆分為約 5 家公司的批次 (深度模式)
split -l 5 {OUTPUT_DIR}/_deep_targets.txt {OUTPUT_DIR}/_batch_deep_
ls {OUTPUT_DIR}/_batch_deep_* | wc -l
代理派遣(骨架,每批處理重複一次並封裝於單一訊息中):
Agent(
description: "深度研究批次 aa",
prompt:,
subagent_type: "通用型"
)
Agent(
description: "深度研究批次 ab",
prompt:,
subagent_type: "general-purpose"
)
... 每則訊息最多 6 個;待第一波回傳後發送第二波
待所有子代理返回後,驗證深度研究檔案是否存在且其`triage_only` 設定為false:
grep -l "triage_only: false" {OUTPUT_DIR}/companies/*.md | wc -l
# 結果應等於 wc -l icp_fits.txt
步驟 8:豐富講者資料
針對每人:蒐集 LinkedIn 網址、近期動態(播客/部落格/演講/GitHub/X),並撰寫people/{slug}.md 檔案。硬性上限:每人最多 4 次工具呼叫,分為三條處理路徑:
瀏覽雲端搜尋 "{姓名} {公司} linkedin"(始終執行)瀏覽雲端搜尋 "{姓名} 播客 OR 演講 OR 部落格 2026"(深度+)瀏覽雲端搜尋「{姓名} github」(更深入)瀏覽雲端搜尋「{姓名} site:x.com OR site:twitter.com」(更深入,盡力而為)
快速模式:完全跳過第 8 步。深度模式:通道 1-2。更深度模式:通道 1-4。
步驟 8a — 詢問使用者:資料豐富化的範圍
在分派之前,計算兩個候選方案的計數,並請使用者進行選擇。預設僅進行ICP 擬合(速度較快、成本較低,且符合多數使用者需求);針對每位講者進行資料豐富化則需主動選擇,因為成本會隨豐富化的人數呈線性增長。
TOTAL=$(wc -l < {OUTPUT_DIR}/people.jsonl)
ICP_FITS=$(node -e '
const fs = require("fs");
const fits = new Set(fs.readFileSync("{OUTPUT_DIR}/icp_fits.txt", "utf-8").split("\n").filter(Boolean));
const slug2name = {};
for (const slug of fits) {
const md = fs.readFileSync(`{OUTPUT_DIR}/companies/${slug}.md`, "utf-8");
const m = md.match(/^company_name:\s*(.+)$/m);
if (m) slug2name[slug] = m[1].trim();
}
const want = new Set(Object.values(slug2name).map(s => s.toLowerCase()));
const ppl = fs.readFileSync("{OUTPUT_DIR}/people.jsonl","utf-8").split("\n").filter(Boolean).map(JSON.parse);
console.log(ppl.filter(p => p.company && want.has(p.company.toLowerCase())).length);
')
# 每人對應的語線數:2(淺層)或 4(更深層) — 對應 {DEPTH}
LANES=2 # 或 4 以增加深度
echo "ICP 匹配:${ICP_FITS} 位講者 × ${LANES} = $((ICP_FITS * LANES)) 次呼叫"
echo "總計: ${TOTAL} 講者 × ${LANES} = $((TOTAL * LANES)) 次通話"
接著透過AskUserQuestion進行詢問 — 提供兩個選項,並明確標示每個選項的量化成本:
AskUserQuestion(questions: [
{
question: "要針對哪些講者進行資料豐富化?",
header: "增強範圍",
multiSelect: false,
options: [
{ label: "僅 ICP FITS", description: "${ICP_FITS} 個揚聲器,約 $((ICP_FITS * LANES)) 次呼叫 (建議)" },
{ label: "ICP FITS 專用", description: "${ICP_FITS} 名講者,約 $((ICP_FITS * LANES)) 次呼叫 (建議)" },
]
}
])
將選定的範圍儲存為ENRICH_SCOPE=icp_fits或ENRICH_SCOPE=all。若使用者選擇「所有發射體」,且TOTAL × LANES > 600,請顯示警告並再次確認——這將是耗時 10 多分鐘且包含數百次工具呼叫的執行程序。
步驟 8b — 篩選與批次處理
# 根據 ENRICH_SCOPE 建立 _people_to_enrich.jsonl
if [ "$ENRICH_SCOPE" = "all" ]; then
cp {OUTPUT_DIR}/people.jsonl {OUTPUT_DIR}/_people_to_enrich.jsonl
else
node -e '
const fs = require("fs");
const fits = new Set(fs.readFileSync("{OUTPUT_DIR}/icp_fits.txt", "utf-8").split("\n").filter(Boolean));
const slug2name = {};
for (const slug of fits) {
const md = fs.readFileSync(`{OUTPUT_DIR}/companies/${slug}.md`, "utf-8");
const m = md.match(/^company_name:\s*(.+)$/m);
if (m) slug2name[slug] = m[1].trim();
}
const wantNames = new Set(Object.values(slug2name).map(s => s.toLowerCase()));
const lines = fs.readFileSync("{OUTPUT_DIR}/people.jsonl", "utf-8").split("\n").filter(Boolean);
const keep = lines.filter(l => {
const p = JSON.parse(l);
return p.company && wantNames.has(p.company.toLowerCase());
});
fs.writeFileSync("{OUTPUT_DIR}/_people_to_enrich.jsonl", keep.join("\n") + "\n");
console.error(`正在豐富 ${keep.length} 位講者(總共 ${lines.length} 位)`);
'
fi
# 拆分為約 5 人的批次
split -l 5 {OUTPUT_DIR}/_people_to_enrich.jsonl {OUTPUT_DIR}/_batch_people_
接著在單則訊息中,針對每個批次發送一次 Agent 呼叫(每則訊息最多 6 次),並使用references/workflow.md→ 「Person Enrichment」中的提示。每個子代理的提示應包含:
{SKILL_DIR},{OUTPUT_DIR},{DEPTH}(deep|deeper){USER_COMPANY}、{USER_PRODUCT}、{ICP_DESCRIPTION}{EVENT_NAME}(取自recon.json中的.title){LANES}→ 深度模式為2,更深度模式為4(代入# browse 呼叫中的 N/{LANES}){PEOPLE_BATCH}→_batch_people_aa的內容(每行皆為來自people.jsonl的 JSON 記錄)
代理派遣(骨架,每批次重複一次並封裝於單一訊息中):
Agent(
description: "人員資料豐富化批次 aa",
prompt:,
subagent_type: "通用型"
)
Agent(
description: "人員資料豐富化批次 ab",
prompt:,
subagent_type: "general-purpose"
)
... 每則訊息最多 6 個
待所有子代理返回後,請驗證人員檔案是否存在:
ls {OUTPUT_DIR}/people/*.md | wc -l
# 結果應等於 wc -l _people_to_enrich.jsonl
步驟 9:彙編報告
透過單一指令產生按公司分組的 HTML 索引、替代檢視頁面及 CSV 檔案:
node {SKILL_DIR}/scripts/compile_report.mjs {OUTPUT_DIR} --open
這將產生:
{OUTPUT_DIR}/index.html— 按公司分組的人員清單,依公司 ICP 分數排序(在瀏覽器中開啟){OUTPUT_DIR}/people.html— 可篩選的講者清單(替代檢視){OUTPUT_DIR}/companies.html— 依 ICP 分數排序且包含與會者的公司列表{OUTPUT_DIR}/results.csv— 適用於冷外展的試算表
接著在聊天室中呈現摘要:
## 活動潛在客戶開發完成 — {活動名稱}
- **共萃取講者**:{數量}
- **獨特公司數**:{數量}
- **符合 ICP 標準者(分數 ≥ {閾值})**:{數量}
- **已補充資料的講者**:{count}
- **評分分佈**(企業):
- 高度契合(8-10):{count}
- 部分契合(5-7):{count}
- 低匹配度 (1-4):{count}
- **報告已在瀏覽器中開啟**:{OUTPUT_DIR}/index.html
將前 5 張人物卡片以 Markdown 表格形式顯示,並按公司 ICP 分數排序,接著提供以下選項:
- 調整
--icp-threshold並重新執行步驟 6-9 - 將 CSV 匯出至 CRM
---
name: event-prospecting
description: Takes a conference or event speakers URL, extracts the people, filters their companies against the user's ICP, and deep-researches only the speakers at ICP-fit companies. Outputs a person-first HTML report with a 'why reach out' rationale per person.
license: MIT
---
# Event Prospecting
Take a conference URL → get a ranked list of people the AE should talk to, with a "why reach out" rationale per person.
**Required**: `BROWSERBASE_API_KEY` env var and the `browse` CLI installed (`npm install -g browse`). Use `browse cloud ...` for API calls and `browse open` / `browse get markdown` for JS-heavy speaker pages.
**Path rules**: Always use the full literal path in all Bash commands — NOT `~` or `$HOME` (both trigger "shell expansion syntax" approval prompts). Resolve the home directory once and use it everywhere. When constructing subagent prompts, replace `{SKILL_DIR}` with the full literal path (typically `/Users/jay/skills/skills/event-prospecting`).
**Output directory**: All event prospecting output goes to `~/Desktop/{event_slug}_prospects_{YYYY-MM-DD-HHMM}/`. Final deliverable is `index.html` (people grouped by company, ranked by company ICP), with `companies.html` and `people.html` (filterable) as alternate views, plus `results.csv` for cold-outbound import.
**CRITICAL — Tool restrictions (applies to main agent AND all subagents)**:
- All web searches: use `browse cloud search`. NEVER use WebSearch.
- All page content extraction: use `node {SKILL_DIR}/scripts/extract_page.mjs "<url>"`. This script fetches via `browse cloud fetch --output`, parses title + meta tags + visible body text, and automatically falls back to `browse get markdown` when fetch fails or returns thin JS-rendered content. NEVER hand-roll a `browse cloud fetch | sed` pipeline. NEVER use WebFetch.
- All research output: subagents write **one markdown file per company OR per person** to `{OUTPUT_DIR}/companies/{slug}.md` or `{OUTPUT_DIR}/people/{slug}.md` using bash heredoc. NEVER use the Write tool or `python3 -c`. See `references/example-research.md` for both file formats.
- Report compilation: use `node {SKILL_DIR}/scripts/compile_report.mjs {OUTPUT_DIR} --open`.
- **Subagents must use ONLY the Bash tool. No other tools allowed.**
- **HARD TOOL-CALL CAPS**: ICP triage = 1 call/company; deep research = 5 calls/company; person enrichment = 4 calls/person. See `references/workflow.md` for enforcement detail.
**CRITICAL — Anti-hallucination rules (applies to main agent AND all subagents)**:
- NEVER infer `product_description`, `industry`, or a person's `role_reason` from a site's fonts, framework, design system, or typography. These are cosmetic and say nothing about what the company sells or what the person does.
- NEVER let the user's own ICP leak into a target's description. If you don't know what the target does, write `Unknown` — do not pattern-match them onto the ICP.
- `product_description` MUST quote or paraphrase a specific phrase from `extract_page.mjs` output. If none of TITLE/META/OG/HEADINGS/BODY yield a recognizable product statement, write `Unknown — homepage content not accessible` and cap `icp_fit_score` at 3.
- A person's `hook` MUST quote or paraphrase a specific finding from a `browse cloud search` result (podcast title, blog headline, GitHub repo, talk abstract). If no public signal exists in the last 6 months, fall back to event-context (their talk title at this event).
**CRITICAL — Minimize permission prompts**:
- Subagents MUST batch ALL file writes into a SINGLE Bash call using chained heredocs. One Bash call = one permission prompt.
- Batch ALL searches and ALL fetches into single Bash calls using `&&` chaining.
## Pipeline Overview
Follow these 10 steps in order. Do not skip steps or reorder.
0. **Setup** — output dir + clean slate
1. **Load profile** — read `profiles/{user_slug}.json`
2. **Recon** — detect event platform
3. **Extract people** — `people.jsonl`
4. **Group by company** — `seed_companies.txt`
5. **ICP triage** — fast company-level scoring (1 call/company)
6. **Filter** — companies with `icp_fit_score >= --icp-threshold`
7. **Deep research** — full Plan→Research→Synthesize on ICP fits
8. **Enrich speakers** — ask user: ICP-fit only (default) or all speakers
9. **Compile report** — HTML + CSV, open in browser
The user invokes the skill with a URL like `/event-prospecting <URL>`. Parse `EVENT_URL` from that invocation message. Defaults: `DEPTH=deep`, `ICP_THRESHOLD=6`. The `USER_SLUG` (ICP profile) is auto-resolved in Step 1 from whatever profile files exist locally — there is no built-in default profile. Do NOT ask the user to confirm the URL — they already gave you it.
---
## Step 0: Setup Output Directory
Derive the output directory from the URL the user gave you. Do NOT hardcode any event name.
```bash
# EVENT_URL came from the invocation message (whatever the user typed after `/event-prospecting`)
EVENT_SLUG=$(node -e 'const h = new URL(process.argv[1]).hostname.replace(/^www\./,""); console.log(h.split(".")[0])' "$EVENT_URL")
TIMESTAMP=$(date +%Y-%m-%d-%H%M)
OUTPUT_DIR=/Users/jay/Desktop/${EVENT_SLUG}_prospects_${TIMESTAMP}
mkdir -p "$OUTPUT_DIR/companies" "$OUTPUT_DIR/people"
```
Use the full literal home path — never `~` or `$HOME`. Pass `{OUTPUT_DIR}` as the full literal path to all subagent prompts.
## Step 1: Load User Profile
The profile defines the ICP that ICP triage and deep research score against. Load from `{SKILL_DIR}/profiles/{user_slug}.json` (interchangeable across all GTM skills — same shape as company-research). `example.json` is a template, not a real profile — never use it.
**DO NOT look outside `{SKILL_DIR}/profiles/`** for profiles — never reach into other skills' directories. If a profile is needed elsewhere, the user copies it explicitly.
**Resolution order**:
1. If the user invoked with `--user-company <slug>`, use that slug.
2. Else, list `profiles/*.json` excluding `example.json`. If exactly one profile exists, use it (and tell the user which one). If multiple exist, ask the user (plain chat) which one.
3. If zero profiles exist, **fail loudly** and instruct the user to create one (copy `profiles/example.json` to `profiles/<your_slug>.json` and fill it in, or run the company-research skill which builds one automatically).
```bash
PROFILES=$(ls {SKILL_DIR}/profiles/*.json 2>/dev/null | xargs -n1 basename | sed 's/\.json$//' | grep -v '^example$')
COUNT=$(echo "$PROFILES" | grep -c .)
if [ -z "$USER_SLUG" ]; then
if [ "$COUNT" -eq 0 ]; then
echo "No profiles found in {SKILL_DIR}/profiles/. Copy profiles/example.json to profiles/<your_slug>.json and fill it in, or run the company-research skill to build one."
exit 1
elif [ "$COUNT" -eq 1 ]; then
USER_SLUG=$PROFILES
echo "Using the only profile available: ${USER_SLUG}"
else
echo "Multiple profiles found:"
echo "$PROFILES" | sed 's/^/ - /'
echo "Re-invoke with --user-company <slug> to pick one."
exit 1
fi
fi
test -f {SKILL_DIR}/profiles/${USER_SLUG}.json || {
echo "Profile not found: profiles/${USER_SLUG}.json"
exit 1
}
cat {SKILL_DIR}/profiles/${USER_SLUG}.json
```
The profile yields: `company`, `product`, `icp_description`, `existing_customers`. These get embedded verbatim in every subagent prompt downstream.
## Step 2: Recon
Detect the event platform and extraction strategy. One command:
```bash
node {SKILL_DIR}/scripts/recon.mjs {EVENT_URL} {OUTPUT_DIR}
```
Writes `{OUTPUT_DIR}/recon.json` with `platform`, `strategy`, and (for Next.js) `nextDataPaths`. See `references/event-platforms.md` for the platform catalog and detection priority.
Expected outcomes:
- Stripe Sessions class (Next.js): `platform: "next-data"`, 1-3 paths
- Sessionize: `platform: "sessionize"`
- Lu.ma / Eventbrite: `platform: "luma" | "eventbrite"`
- Anything else: `platform: "custom"`, `strategy: "markdown"` (best-effort fallback)
## Step 3: Extract People
```bash
node {SKILL_DIR}/scripts/extract_event.mjs {OUTPUT_DIR} --user-company {USER_SLUG}
```
Reads `recon.json`, dispatches to the platform-specific extractor, writes `people.jsonl` (one speaker per line) and `seed_companies.txt` (deduped companies).
The `--user-company` flag also drops the host-org's own employees (a Stripe-hosted event drops Stripe employees) and the user's own employees from the speaker list — those aren't prospects.
Sanity-check the output:
```bash
wc -l {OUTPUT_DIR}/people.jsonl {OUTPUT_DIR}/seed_companies.txt
head -3 {OUTPUT_DIR}/people.jsonl
```
If `people.jsonl` is empty or under ~10 lines, recon picked the wrong platform — see `references/event-platforms.md` and re-run with adjusted strategy.
## Step 4: Group by Company
`extract_event.mjs` emits `seed_companies.txt` already (one company per line, deduped, sorted). This step is informational — verify the count looks reasonable before fanning out:
```bash
wc -l {OUTPUT_DIR}/seed_companies.txt
```
Expected: roughly 0.4-0.6× the speaker count (most events have ~2 speakers per company on average, some companies send 5+, many send 1).
## Step 5: ICP Triage
**Fast pass — one tool call per company, no deep research.** Score every company in `seed_companies.txt` against the user's ICP and write a thin triage stub to `companies/{slug}.md`. Companies with `icp_fit_score >= --icp-threshold` (default 6) advance to Step 7's deep research; the rest stay as triage stubs.
**Dispatch pattern**: split `seed_companies.txt` into batches of ~10 and fan out N subagents in a SINGLE Agent batch (multiple Agent tool calls in one message). Each subagent runs the prompt from `references/workflow.md` → "ICP Triage" section. Hard cap: **1 tool call per company** (just `extract_page.mjs` on the homepage), enforced via the `# browse call N/1` comment pattern.
```bash
# Build batch files: each batch line is "name|guessed_homepage|slug".
# extract_event.mjs only emits company NAMES (no URLs), so we slugify and guess
# https://{slug-without-spaces}.com as the canonical homepage. The triage subagent
# is allowed to write product_description: "Unknown — homepage content not accessible"
# and cap score at 3 if the guessed URL 404s — that's the documented fallback in
# workflow.md (rule 3 of the ICP Triage prompt). Burning a real browse cloud search to
# discover the URL would bust the 1-call-per-company HARD CAP.
node -e '
const fs = require("fs");
const slugify = (s) => (s || "").toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
const seed = fs.readFileSync("{OUTPUT_DIR}/seed_companies.txt", "utf-8").split("\n").filter(Boolean);
const lines = seed.map(c => {
const slug = slugify(c);
const guessedHost = c.toLowerCase().replace(/[^a-z0-9]/g, "");
return `${c}|https://${guessedHost}.com|${slug}`;
});
fs.writeFileSync("{OUTPUT_DIR}/_seed_with_urls.txt", lines.join("\n") + "\n");
'
# Split into ~10-company batches
split -l 10 {OUTPUT_DIR}/_seed_with_urls.txt {OUTPUT_DIR}/_batch_triage_
# Count batches → number of subagents to dispatch (cap at 6 per message; second wave for the rest)
ls {OUTPUT_DIR}/_batch_triage_* | wc -l
```
Then in a single message, dispatch one Agent call per batch (up to 6 in parallel; subsequent waves after the first returns). Each Agent gets the prompt from `references/workflow.md` → "ICP Triage" with these substitutions before sending:
- `{SKILL_DIR}` → full literal skill path (e.g. `/Users/jay/skills/skills/event-prospecting`)
- `{OUTPUT_DIR}` → full literal output path
- `{USER_COMPANY}`, `{USER_PRODUCT}`, `{ICP_DESCRIPTION}` → from the loaded profile
- `{EVENT_NAME}` → `recon.json` `.title`
- `{COMPANY_LIST}` → contents of the batch file (e.g. `cat {OUTPUT_DIR}/_batch_triage_aa`)
- `{TOTAL}` → number of lines in this batch (substitute into `# browse call N/{TOTAL}`)
**Agent dispatch (skeleton, repeat per batch in one message)**:
```
Agent(
description: "ICP triage batch aa",
prompt: <ICP Triage prompt from workflow.md with all placeholders substituted>,
subagent_type: "general-purpose"
)
Agent(
description: "ICP triage batch ab",
prompt: <same prompt template, COMPANY_LIST swapped to batch ab>,
subagent_type: "general-purpose"
)
... up to 6 per message
```
After all subagents return, verify every company in `seed_companies.txt` has a corresponding `companies/{slug}.md`:
```bash
ls {OUTPUT_DIR}/companies/*.md | wc -l
# Should equal `wc -l {OUTPUT_DIR}/seed_companies.txt`
```
Clean up the batch files: `rm {OUTPUT_DIR}/_batch_triage_*`.
## Step 6: Filter by ICP Threshold
Read each `companies/*.md` frontmatter, keep those with `icp_fit_score >= 6` (or whatever `--icp-threshold` is). Write the surviving company slugs to `{OUTPUT_DIR}/icp_fits.txt`:
```bash
THRESHOLD=6 # from --icp-threshold flag
for f in {OUTPUT_DIR}/companies/*.md; do
score=$(awk '/^icp_fit_score:/{print $2; exit}' "$f")
if [ -n "$score" ] && [ "$score" -ge "$THRESHOLD" ]; then
basename "$f" .md
fi
done > {OUTPUT_DIR}/icp_fits.txt
wc -l {OUTPUT_DIR}/icp_fits.txt
```
Expected: 20-40% of `seed_companies.txt`. If the survival rate is < 10%, the threshold may be too high or the ICP description too narrow — surface a warning to the user.
## Step 7: Deep Research
Full Plan→Research→Synthesize on ICP-fit companies only. Hard cap: **5 tool calls per company** (homepage extract + 2-3 sub-question searches + 1-2 supplementary fetches). Subagents OVERWRITE the existing `companies/{slug}.md` triage stub with the richer deep-research version (frontmatter `triage_only: false`).
**Dispatch pattern**: split `icp_fits.txt` into batches of ~5 (deep mode default) and fan out one Agent per batch in a SINGLE message (up to 6 Agents per message). Each Agent gets the prompt from `references/workflow.md` → "Deep Research" with these substitutions:
- `{SKILL_DIR}`, `{OUTPUT_DIR}`, `{USER_COMPANY}`, `{USER_PRODUCT}`, `{ICP_DESCRIPTION}`
- `{EVENT_NAME}` (from `recon.json` `.title`), `{EVENT_CONTEXT}` (track / topic, manually inferred from the event homepage)
- `{COMPANY_LIST}` → contents of the batch file (each line `slug|website`)
```bash
# Build {company-slug|website} pairs by reading frontmatter from each triage stub
while read slug; do
website=$(awk '/^website:/{print $2; exit}' {OUTPUT_DIR}/companies/${slug}.md)
echo "${slug}|${website}"
done < {OUTPUT_DIR}/icp_fits.txt > {OUTPUT_DIR}/_deep_targets.txt
# Split into ~5-company batches (deep mode)
split -l 5 {OUTPUT_DIR}/_deep_targets.txt {OUTPUT_DIR}/_batch_deep_
ls {OUTPUT_DIR}/_batch_deep_* | wc -l
```
**Agent dispatch (skeleton, repeat per batch in one message)**:
```
Agent(
description: "Deep research batch aa",
prompt: <Deep Research prompt from workflow.md with all placeholders substituted; COMPANY_LIST = cat _batch_deep_aa>,
subagent_type: "general-purpose"
)
Agent(
description: "Deep research batch ab",
prompt: <same template, COMPANY_LIST = cat _batch_deep_ab>,
subagent_type: "general-purpose"
)
... up to 6 per message; second wave after the first returns
```
After all subagents return, verify the deep-research files exist and have `triage_only: false`:
```bash
grep -l "triage_only: false" {OUTPUT_DIR}/companies/*.md | wc -l
# Should equal wc -l icp_fits.txt
```
## Step 8: Enrich Speakers
Per person: harvest LinkedIn URL, recent activity (podcast / blog / talk / GitHub / X), and write `people/{slug}.md`. Hard cap: **4 tool calls per person**, three lanes:
1. `browse cloud search "{name} {company} linkedin"` (always)
2. `browse cloud search "{name} podcast OR talk OR blog 2026"` (deep+)
3. `browse cloud search "{name} github"` (deeper)
4. `browse cloud search "{name} site:x.com OR site:twitter.com"` (deeper, best-effort)
Quick mode: skip Step 8 entirely. Deep mode: lanes 1-2. Deeper mode: lanes 1-4.
### Step 8a — Ask the user: scope of enrichment
Before dispatching, compute the two candidate counts and ask the user to choose. The default is **ICP-fit only** (faster, cheaper, what most users want); enriching every speaker is opt-in because cost scales linearly with people enriched.
```bash
TOTAL=$(wc -l < {OUTPUT_DIR}/people.jsonl)
ICP_FITS=$(node -e '
const fs = require("fs");
const fits = new Set(fs.readFileSync("{OUTPUT_DIR}/icp_fits.txt", "utf-8").split("\n").filter(Boolean));
const slug2name = {};
for (const slug of fits) {
const md = fs.readFileSync(`{OUTPUT_DIR}/companies/${slug}.md`, "utf-8");
const m = md.match(/^company_name:\s*(.+)$/m);
if (m) slug2name[slug] = m[1].trim();
}
const want = new Set(Object.values(slug2name).map(s => s.toLowerCase()));
const ppl = fs.readFileSync("{OUTPUT_DIR}/people.jsonl","utf-8").split("\n").filter(Boolean).map(JSON.parse);
console.log(ppl.filter(p => p.company && want.has(p.company.toLowerCase())).length);
')
# Lanes per person: 2 (deep) or 4 (deeper) — match {DEPTH}
LANES=2 # or 4 for deeper
echo "ICP fits: ${ICP_FITS} speakers × ${LANES} = $((ICP_FITS * LANES)) calls"
echo "All: ${TOTAL} speakers × ${LANES} = $((TOTAL * LANES)) calls"
```
Then ask via `AskUserQuestion` — clean two-option choice with the quantified cost on each:
```
AskUserQuestion(questions: [
{
question: "Enrich which speakers?",
header: "Enrichment scope",
multiSelect: false,
options: [
{ label: "ICP fits only", description: "${ICP_FITS} speakers, ~$((ICP_FITS * LANES)) calls (recommended)" },
{ label: "All speakers", description: "${TOTAL} speakers, ~$((TOTAL * LANES)) calls" }
]
}
])
```
Save the chosen scope as `ENRICH_SCOPE=icp_fits` or `ENRICH_SCOPE=all`. If the user picks "All speakers" and `TOTAL × LANES > 600`, print a warning and ask once more — that's a 10+ minute run with hundreds of tool calls.
### Step 8b — Filter and batch
```bash
# Build _people_to_enrich.jsonl based on ENRICH_SCOPE
if [ "$ENRICH_SCOPE" = "all" ]; then
cp {OUTPUT_DIR}/people.jsonl {OUTPUT_DIR}/_people_to_enrich.jsonl
else
node -e '
const fs = require("fs");
const fits = new Set(fs.readFileSync("{OUTPUT_DIR}/icp_fits.txt", "utf-8").split("\n").filter(Boolean));
const slug2name = {};
for (const slug of fits) {
const md = fs.readFileSync(`{OUTPUT_DIR}/companies/${slug}.md`, "utf-8");
const m = md.match(/^company_name:\s*(.+)$/m);
if (m) slug2name[slug] = m[1].trim();
}
const wantNames = new Set(Object.values(slug2name).map(s => s.toLowerCase()));
const lines = fs.readFileSync("{OUTPUT_DIR}/people.jsonl", "utf-8").split("\n").filter(Boolean);
const keep = lines.filter(l => {
const p = JSON.parse(l);
return p.company && wantNames.has(p.company.toLowerCase());
});
fs.writeFileSync("{OUTPUT_DIR}/_people_to_enrich.jsonl", keep.join("\n") + "\n");
console.error(`Enriching ${keep.length} of ${lines.length} speakers`);
'
fi
# Split into ~5-person batches
split -l 5 {OUTPUT_DIR}/_people_to_enrich.jsonl {OUTPUT_DIR}/_batch_people_
```
Then in a single message, dispatch one Agent call per batch (up to 6 per message) with the prompt from `references/workflow.md` → "Person Enrichment". Each subagent's prompt should include:
- `{SKILL_DIR}`, `{OUTPUT_DIR}`, `{DEPTH}` (`deep` | `deeper`)
- `{USER_COMPANY}`, `{USER_PRODUCT}`, `{ICP_DESCRIPTION}`
- `{EVENT_NAME}` (from `recon.json` `.title`)
- `{LANES}` → `2` for deep mode, `4` for deeper mode (substituted into `# browse call N/{LANES}`)
- `{PEOPLE_BATCH}` → contents of `_batch_people_aa` (each line a JSON record from `people.jsonl`)
**Agent dispatch (skeleton, repeat per batch in one message)**:
```
Agent(
description: "Person enrichment batch aa",
prompt: <Person Enrichment prompt from workflow.md with all placeholders substituted; PEOPLE_BATCH = cat _batch_people_aa>,
subagent_type: "general-purpose"
)
Agent(
description: "Person enrichment batch ab",
prompt: <same template, PEOPLE_BATCH = cat _batch_people_ab>,
subagent_type: "general-purpose"
)
... up to 6 per message
```
After all subagents return, verify the people files exist:
```bash
ls {OUTPUT_DIR}/people/*.md | wc -l
# Should equal wc -l _people_to_enrich.jsonl
```
## Step 9: Compile Report
Generate the company-grouped HTML index, alternate views, and CSV in one command:
```bash
node {SKILL_DIR}/scripts/compile_report.mjs {OUTPUT_DIR} --open
```
This generates:
- `{OUTPUT_DIR}/index.html` — people grouped by company, ranked by company ICP score (opens in browser)
- `{OUTPUT_DIR}/people.html` — filterable speaker list (alternate view)
- `{OUTPUT_DIR}/companies.html` — ICP-ranked company table with attendees
- `{OUTPUT_DIR}/results.csv` — cold-outbound-ready spreadsheet
Then present a summary in chat:
```
## Event Prospecting Complete — {Event Name}
- **Total speakers extracted**: {count}
- **Unique companies**: {count}
- **ICP fits (score ≥ {threshold})**: {count}
- **Speakers enriched**: {count}
- **Score distribution** (companies):
- Strong fit (8-10): {count}
- Partial fit (5-7): {count}
- Weak fit (1-4): {count}
- **Report opened in browser**: {OUTPUT_DIR}/index.html
```
Show the **top 5 people cards** as a markdown table sorted by company ICP score, then offer to:
- Adjust `--icp-threshold` and re-run Steps 6-9
- Export the CSV to a CRM





首頁
