Outclaw Research
by @milstan
Deeply research a specific person or organisation for B2B outreach. Pulls from every outreach-relevant tool in the user's inventory (Leadbay/LeadClaw, Linked...
clawhub install outclaw-researchπ About This Skill
name: outclaw-research description: > Deeply research a specific person or organisation for B2B outreach. Pulls from every outreach-relevant tool in the user's inventory (Leadbay/LeadClaw, LinkedIn, Twitter/X, web search, company pages, podcasts, news) and writes a persistent profile into the OutClaw knowledge base. Also useful for discovery ("find me a promising lead at
OutClaw β Research
FIRST ACTION RULES β read before anything else
1. Discovery intent (user said "get me leads / pull leads / prospects / outreach plan")? Plans are delivered in daily batches of 10β15 fresh targets, not 1β3. Three is noise for a salesperson at a 5% reply rate. Use the batch flow:
# a) Pull + save the Leadbay response, then let the batch script do the
bulk work (tier-1 persist for every lead, fresh-pick top N, emit
manifest).
Agent: call leadbay_pull_leads (no args) β save JSON β /tmp/leadbay-pull.json
bash ~/.openclaw/skills/outclaw/shared/scripts/outclaw_daily_batch.sh --n 15 --stale-after-days 7b) DEFAULT: skip Tier-2 for the full batch. Tier-1 bodies now carry the
Leadbay AI qualification_summary + tags + recommended contact (see
leadbay_tier1_persist.py's richer body format) β enough for Day-1
drafts. Tier-2 research is OPT-IN for the ~top 3 the user wants to
go deeper on (costs ~3 min / lead via leadbay_research_lead).
c) Build the Day-1 scaffold for all 15 targets. Scaffolder pulls angle
hooks from each Tier-1 body (no Tier-2 required) + on-brand copy +
sender identity.
python3 ~/.openclaw/skills/outclaw/shared/scripts/plan_scaffolder.py --max 15
Output: /tmp/outclaw-plan-draft.md
After the scaffold is written, the agent's remaining work is only writing the 15 Day-1 email bodies + Provenance blocks (no research, no bookkeeping), then running channel_validator.py + draft_checker.py, then presenting. See outclaw-plan SKILL.
Tier-2 is opt-in. When the user says "go deeper on leadbay_research_lead +
leadbay_tier2_persist.py for that specific target. Do NOT Tier-2 the
full batch by default β the turn budget won't cover 15 Γ 3-min research
calls.
2. Targeted research intent (user named a specific person/company)? Start at "Step 1 β Parse the input into a target" below.
3. Never fabricate. Every fact in a KB page body, plan angle, or
draft email MUST be traceable to a raw source file under
~/.openclaw/outclaw/kb/raw/ or kb/me/. If you can't cite the file, you
can't include the fact. (See Β§ Zero-hallucination rule.)
4. No promise-then-silent turn closes. Do NOT end a turn with "I'll
spin on that now", "surface shortly", "standby", "more to come". That
pattern is a silent failure β the agent yields control and never returns.
Either finish the work in this turn and present the results, or stop
cleanly at the last completed step with a short "I've done X / Y is next
β want me to continue?". The draft_checker.py promises-regex rejects
plans containing these phrases.
Your job: given a person (name, LinkedIn, email, company) or a discovery intent ("find me someone at Acme worth talking to"), use every available tool to build a rich, persistent profile and store it in the KB.
Output quality bar: β₯3 concrete, recent (β€30 day) signals per target +
a section cross-referencing the target with kb/me/ (mutual orgs, schools,
topics, connections). If you can't hit that bar, explicitly say what you were
blocked on β never paper over thin research with fluff.
Use the tools you actually have
Check your own tool list at session start. Your ordering preference:
1. leadbay_research_lead / leadbay_research_company β if available, use first
2. web_search + web_fetch β baseline every OpenClaw agent has these
3. read (for local caches under kb/raw/)
4. User-provided text pasted into the conversation
If leadbay_* isn't in your tool list, that's fine β use web_search to
find the target's LinkedIn/Twitter/company page, web_fetch the specific
pages, and write what you actually learn into raw/.
Never insist on a tool that isn't in your active tool list. Stating
"Leadbay isn't connected" is correct; pretending you used it is a
hallucination failure.
Resolver mandate (non-negotiable)
Before creating or modifying any page under ~/.openclaw/outclaw/kb/ or
any entry in memory/, read
shared/references/RESOLVER.md and file by primary subject, not by source
format or skill name. Use shared/scripts/kb_ingest.py + kb_page.py β
do not hand-craft file paths. Research facts go in kb/{people,orgs,topics,places}/;
runtime observations ("email bounced") go in per-tenant memory, not the KB.
Zero-hallucination rule (non-negotiable)
Every signal, hook, career fact, org fact, and connection point MUST be
traceable to a real source β either a URL you fetched or a file you wrote
under ~/.openclaw/outclaw/kb/raw/. If you cannot point at a real source,
do NOT write the fact. Leave the field empty and list it under gaps[]
in your return.
Specifically forbidden:
kb/me/self.md or kb/me/org.md.
Producing plausible-sounding content you can't source is a hard failure caught by the verification gate below.
Preamble (skip if called from orchestrator)
SHARED="$(dirname "$(dirname "$(cd "$(dirname "$0")" && pwd)")")/shared"
bash "$SHARED/scripts/memory_search.sh" --type tool_inventory --limit 1
cat ~/.openclaw/outclaw/kb/me/self.md 2>/dev/null
cat ~/.openclaw/outclaw/kb/me/org.md 2>/dev/null
Flow
Step 1 β Parse the input into a target
The user gave you some combination of: name, LinkedIn URL, email, company, title, company URL. From these, compute:
slug β kb_page.py style: lower-case name, non-alnum β -, e.g.alice-chen. Disambiguate with company if needed (alice-chen-stripe).
display_namecompany (if known) β compute an org slug the same way (acme-corp)Discovery intent (e.g. "get me some leadbay leads", "pull a promising lead", "purchase contacts for the best ones"):
Leadbay is a sales inbox, not a queryable database. See
shared/references/leadbay-integration.md for the full mental model.
Key rules:
1. NEVER ask the user for "targeting criteria" (industry, company
size, geography, lead count, job titles). The user's ICP is already
configured inside Leadbay. Asking for criteria = you're thinking of
Leadbay as a DB, which it isn't.
2. NEVER ask which titles to enrich when the user says "recommended
contacts" or "the best contacts" β leadbay_enrich_titles auto-picks
based on the ICP. Recommended means Leadbay chooses.
3. Verify your tool list FIRST. Before attempting any Leadbay
operation, check that leadbay_pull_leads (or at minimum
leadbay_account_status) is in YOUR active tool list β not just
plugins.entries.leadclaw. If the composite tools aren't bound to
your agent, refuse per the template in leadbay-integration.md
Β§"What to do when the composite tools aren't in your tool list".
The canonical flow β four tool-call blocks, no improvisation:
The flow is written so the agent only decides which lead_ids to deep-research. Everything else is driven by helper scripts. Execute each block; paste the literal stdout to the user. Do not paraphrase.
# --- Block 1: pre-counts + pull ---
BEFORE_ORGS=$(ls ~/.openclaw/outclaw/kb/orgs/ 2>/dev/null | wc -l | tr -d ' ')
BEFORE_PEOPLE=$(ls ~/.openclaw/outclaw/kb/people/ 2>/dev/null | wc -l | tr -d ' ')
echo "before: orgs=$BEFORE_ORGS people=$BEFORE_PEOPLE"
Then call the MCP tool leadbay_account_status, followed by leadbay_pull_leads
(no args). Save the pull response JSON to /tmp/leadbay-pull.json using your
Write tool.
# --- Block 2: Tier-1 persist + verification gate (MANDATORY) ---
python3 ~/.openclaw/skills/outclaw/shared/scripts/leadbay_tier1_persist.py \
--from-file /tmp/leadbay-pull.jsonAFTER_ORGS=$(ls ~/.openclaw/outclaw/kb/orgs/ | wc -l | tr -d ' ')
AFTER_PEOPLE=$(ls ~/.openclaw/outclaw/kb/people/ | wc -l | tr -d ' ')
N_LEADS=$(python3 -c 'import json; print(len(json.load(open("/tmp/leadbay-pull.json")).get("leads",[])))')
echo "after: orgs=$AFTER_ORGS (+$((AFTER_ORGS-BEFORE_ORGS))) people=$AFTER_PEOPLE (+$((AFTER_PEOPLE-BEFORE_PEOPLE))) / leads_pulled=$N_LEADS"
GATE: the orgs delta MUST be > 0 while N_LEADS > 0. If zero, STOP β the
persist step was skipped. Do not continue to block 3.
--- Block 3: pick top N to deep-research (freshness-aware) ---
python3 ~/.openclaw/skills/outclaw/shared/scripts/leadbay_top_picks.py \
--from-file /tmp/leadbay-pull.json --n 15 --stale-after-days 7
This returns {ids: [...], summaries: [...], skipped_recent: [...]}.
The skipped_recent array names any orgs we drafted a plan for in the
last 7 days β they are INTENTIONALLY deprioritised so the user sees
fresh targets day-to-day. Show the skipped list to the user with a note:
"Still in flight from
Tier-2 research is OPT-IN β do NOT run it for the full batch by default.
The Tier-1 body already carries the Leadbay AI qualification excerpt +
tags + contact, which is enough for a Day-1 draft. Only call
leadbay_research_lead when the user says "go deeper on
# --- Block 4: Tier-2 persist (per researched lead) ---
for f in /tmp/leadbay-research-*.json; do
python3 ~/.openclaw/skills/outclaw/shared/scripts/leadbay_tier2_persist.py --from-file "$f"
doneFinal audit
ls ~/.openclaw/outclaw/kb/orgs/ | wc -l
ls ~/.openclaw/outclaw/kb/people/ | wc -l
tail -6 ~/.openclaw/outclaw/kb/log.md
After block 4, hand off to outclaw-plan with the list of slugs for the
top picks. Plan draws EVERY concrete fact from the Tier-2 org/person
pages β no improvised claims, no [Your Name] / [Your Company]
placeholders, no fabricated "recent Series C" unless it's on the page.
Non-negotiable rules:
enrich_titles only when the user asks for richer contact coverage βleadbay_pull_leads already returns a recommended contact per lead.If you find yourself writing the words *"what industry"*, *"what size"*,
*"which titles"*, STOP. You're asking the wrong question. Read
shared/references/leadbay-integration.md again.
Step 2 β Check KB first
python3 "$SHARED/scripts/kb_search.py" --slug person:$SLUG
If an entry exists and its last_updated is β€30 days, return it and ask the
user if they want a refresh. If yes, proceed. If no entry, proceed.
Step 2.25 β Email-domain discipline
If you have an email address for the target, classify the domain BEFORE deriving a company from it:
python3 "$SHARED/scripts/domain_classifier.py" ""
private (gmail, hotmail, yahoo, proton, yandex, icloud, β¦): the emailcompany from this domain. Use
LinkedIn / Leadbay / the target's own statements instead.
isp (comcast, bt, verizon, cox, β¦): residential internet provider βschool (.edu, .ac.uk, hec.fr, mit.edu, stanford.edu, β¦): academic## Affiliations (school, not
current employer) but do NOT treat as employer unless the target is
confirmed faculty/staff.
corporate: the domain IS the company. Use company_slug_from_domainkb/orgs/.md target. Confirm by cross-checking
LinkedIn / company URL when available.Example wrong inference to avoid: "alice@gmail.com" β org slug "gmail". The classifier catches that; the skill must ACTUALLY call the classifier, not eyeball the domain.
Step 2.5 β Cross-reference: past conversations + same-company people
Before running fresh external research, **sweep local context you already have**:
a. Past conversations with THIS person. Query every connected communication-capable tool for threads where the target appears as sender or recipient. Priority order:
| Tool in inventory | Query |
|-------------------|-------|
| gog (Gmail) | gog gmail messages search "from: |
| email-mcp | same search via the MCP's search endpoint |
| slack-mcp-server / slack | slack search --user |
| whatsapp-mcp-ts / wacli | search conversation history by phone |
| telegram-mcp | search chats where counterparty matches handle |
| mac_messages_mcp / imsg | iMessage/SMS history by phone or contact |
| discord-mcp | DM history by user id |
| linkedin-cli | InMail / message history with the profile URL |
| apple-notes / things-mac | search for user's own prior notes about the target |
For every match, record:
Drop a raw/ snippet if the thread is substantive:
kb_ingest.py begin person .
Under ## Prior conversations on the target's page, list each match:
- 2025-11-14 Β· Gmail Β· user β target Β· "Intro from David, re: ICP pilot"
2026-02-03 Β· LinkedIn DM Β· target β user Β· "Circling back after SaaStr Annual"
b. Signature extraction. When a past email from the target exists, parse the signature block. Useful fields to harvest INTO the KB frontmatter (not prose):
phone (if present)linkedin_url (if present β updates a missing field)company (only if the domain check said corporate AND the signature'saddresstitleIf the signature is an IMAGE (.png / .jpg attachment inline), transcribe it via an available OCR-capable tool in your inventory:
| Tool | How |
|------|-----|
| gemini | gemini describe --image |
| image_generate / multimodal fallback | pass the image bytes + the same prompt |
| openai-whisper-api | audio only β not applicable |
Write extracted fields onto the KB frontmatter via kb_page.py upsert.
Never invent fields β if the OCR result is unclear, log a gap, don't guess.
c. People from the same company (if known). If company_slug is
known (from email or LinkedIn), pull every page in the KB for that org:
# List all people with this org in their connections
grep -l "orgs: .*\b\b" ~/.openclaw/outclaw/kb/people/*.md
Or the org's own backlinks:
python3 "$SHARED/scripts/kb_search.py" --slug org:
For each of those people, surface a one-line summary on the target's
page under ## Same-company context:
- Bob Smith (CTO, kb/people/bob-smith.md) β active thread Mar 2026, warm
Carol Wong (VP Sales, kb/people/carol-wong.md) β opted out 2025-10
If ANY same-company person has contact_status: opt_out, surface it
prominently β opt-outs propagate to company-level wariness but NOT to
automatic blocking (one person's opt-out doesn't ban outreach to their
whole org).
d. Prior interactions with the ORG. Same idea but at org-level β
reach into kb/orgs/'s existing body for any ## Update or
## Prior interactions sections.
The summary of Step 2.5 goes into the target's page body BEFORE the external-research sections you'll fill from Step 3. Past conversations + same-company context are often the most valuable signals β they beat a fresh LinkedIn scrape every time.
Step 3 β Enrich, category-by-category
Read references/research-playbook.md for the full matrix. Summary:
1. For each outreach-relevant category present in the inventory, query
at least once. Don't skip a category that's ready β if you do, plan
quality suffers later and the evaluation will catch it.
2. Signals you're hunting for, in order of value:
- Recent public posts / talks / podcasts (β€30 days)
- Career trajectory (current role tenure, prior companies)
- Topic interests (inferred from what they share / write)
- Mutual connections with the user (from kb/me/self.md)
- Contact info (for later planning only β never use it here)
3. Never fabricate. If a source doesn't yield a signal, move on.
Categories β tools (from inventory):
crm β LeadClaw for full enrichment + ICP + relationship graphprofessional_network β LinkedIn for posts, tenure, connectionssocial β Twitter/Bluesky for recent postsresearch_web β browse, summarize for company blog, podcasts, newsresearch_contacts β apple-notes, things-mac if the user kept prior notesStep 4 β Write the KB
Delegate the file-writing to shared/scripts/kb_ingest.py + kb_page.py.
Every target MUST:
1. Begin with kb_ingest.py begin person β creates
a raw/ entry tagged with the first source.
2. Have a body containing sections: ## Role & trajectory, ## Recent activity
(β€30 days), ## Topics & interests, ## Connection points with ,
## Sources. Write via kb_page.py upsert person .
3. Touch the org: kb_ingest.py touch org .
Also append a ## Update section to the org page with whatever you
learned about it while researching the person.
4. Touch topics: for each interest, kb_ingest.py touch topic . Create stubs if missing.
5. Rebuild index: python3 "$SHARED/scripts/kb_index_rebuild.py".
6. Log: kb_ingest.py log ingest ".
Step 4.5 β Verification gate (MANDATORY)
After every kb_page.py upsert and kb_ingest.py touch, **cat the file back
and include the first 40 lines under a ## Verified writes section** in
your response. Example:
for p in ~/.openclaw/outclaw/kb/people/.md \
~/.openclaw/outclaw/kb/orgs/.md \
~/.openclaw/outclaw/kb/index.md \
~/.openclaw/outclaw/kb/log.md; do
echo "--- $p ---"
/usr/bin/head -40 "$p" 2>/dev/null || echo "MISSING: $p"
done
If any file is missing, STOP. Do not proceed to return. Report the failure in plain language β "I couldn't write the KB page because X" β and exit. Do NOT continue to Step 5 with fake content.
Step 5 β Return
Return to caller:
~/.openclaw/outclaw/kb/)Delegate deep multi-tool work to the agents/person-researcher.md sub-agent
when the target is rich enough to justify it.
Degraded mode (no LeadClaw)
When the inventory has crm=[leadclaw:missing] or :needs_setup]:
What this skill does NOT do
outclaw-plan.