---
name: research-sender
description: "Parallel-first sender research protocol. One round of five batched tool calls (fetch_linkedin_profile + fetch_company + 3× WebSearch), or the profile first and then the rest when only a profile URL is known; no enrich_sender required, ~30-40s wall time."
visibility: internal
allowed-tools: mcp__sellable__fetch_linkedin_profile mcp__sellable__fetch_company mcp__sellable__complete_sender_research WebSearch WebFetch ToolSearch
---

# Research Sender (Parallel-First)

Use this subskill when creating campaigns and you need sender/company research.
This protocol is standalone and does not depend on the generic `research` subskill.

## Writing to the customer

Everything you write to the customer (Slack messages, questions, progress
notes, summaries, the campaign brief) follows ASD-STE100 Simplified Technical
English at about 80%:

- One idea per sentence. One instruction per sentence.
- An instruction has at most 20 words; any other sentence at most 25.
- Active voice: say who does the action ("I added 60 people").
- Simple tenses only: present, simple past, and "will".
- Common words with one meaning. Use the same word for the same thing every time.
- No idioms, slang or phrasal verbs where one verb works: "start", not "kick off".
- An instruction starts with its verb: "Reply to Priya.", "Connect your LinkedIn."
- Keep small words such as "the", "a" and "to". Use a list for more than two items.

Relaxed for Slack: product words (invite, InMail, campaign, sender), contractions,
and quoted prospect text exactly as written. This rule is for text to the
customer only. Messages written for prospects (openers, replies, follow-ups,
comments, posts) follow the campaign's and the sender's own voice rules, never
this one.

## Core Idea — Don't Wait, Fan Out

Old protocol: call `enrich_sender` → wait for partial → maybe spawn 2-3 subagents → wait.
That's 3-4 sequential rounds and ~88s+ wall time.

New protocol: **fire one parallel batch.** No `enrich_sender` precondition.
`fetch_linkedin_profile` (sender background), `fetch_company` (company description /
industry / employees), and 3× `WebSearch` (proof, growth, positioning) all run in the
same window. They finish in ~10-15s if truly parallel, ~30-40s when the model
serializes (interactive mode is best-effort, headless `claude -p` always serializes).

Then synthesize once into the campaign brief — the brief is what downstream
message generation reads, not a `clientProspect` row. `clientProspectId` is
optional in `create_campaign`; pass `senderLinkedinUrl` instead.

## Inputs

Provide as many as available. Use `"Unknown"` for missing values.

- `linkedinUrl` (REQUIRED — drives `fetch_linkedin_profile`)
- `name`
- `title`
- `companyName`
- `companyDomain` (REQUIRED — drives `fetch_company` and all three WebSearch queries; derive from email or LinkedIn if absent)
- `headline` (optional)

## Execution Backend Routing

This skill runs differently depending on the host:

- **Claude Code host:** the parallel batch uses MCP tools directly — `mcp__sellable__fetch_linkedin_profile`, `mcp__sellable__fetch_company`, and `WebSearch`. Issue all five calls as `tool_use` blocks in a single assistant message.
- **Codex host:** if MCP `fetch_*` tools are not available, fall back to web-fetch equivalents — use the LinkedIn URL with WebFetch for the sender profile, fetch the company's `/about` page for company facts, and use the same three WebSearch queries below for proof / growth / positioning. Synthesis treats both backends identically.

## Setup: Pre-Load Deferred Tools (One-Time, Before The Parallel Batch)

Some hosts (Claude Code with deferred-tool fetch) require `WebSearch` and
some MCP tools to be loaded via `ToolSearch` before they can be invoked. If
they are not already directly callable, issue this once as the first tool
call, on its own:

`ToolSearch({ query: "select:WebSearch,mcp__sellable__fetch_linkedin_profile,mcp__sellable__fetch_company,mcp__sellable__complete_sender_research", max_results: 5 })`

Skip this turn entirely if the tools are already directly callable.

## The One Round (Parallel Batch)

**HARD RULE:** the five tool calls below MUST be emitted as **five `tool_use`
content blocks inside a single assistant message** (one turn, parallel
execution). Do NOT split them across multiple assistant messages.

Concretely, your next assistant message must contain exactly these five
`tool_use` blocks, in any order, with no leading or trailing prose:

1. `fetch_linkedin_profile({ linkedinUrl })` — sender's LinkedIn profile (firstName/lastName, currentCompany, experience, follower count, education) for the brief's "sender background" section.
2. `fetch_company({ companyUrl })` — sender's company LinkedIn page (description, industry, employee range, recent posts). `companyUrl` is the LinkedIn company URL. REQUIRED — replaces what `enrich_sender` used to provide for `companySnapshot`.
3. `WebSearch({ query: 'site:{companyDomain} ("case study" OR "customer story" OR testimonial OR "success story") "{companyName}"' })` — proof.
4. `WebSearch({ query: '"{companyName}" {companyDomain} {currentYear} (funding OR raised OR seed OR series OR hiring OR launch OR "press release")' })` — growth/credibility.
5. `WebSearch({ query: '"{companyName}" about product site:{companyDomain}' })` — positioning.

**When only the profile URL is known** (a new customer gave nothing else), the
company name, domain and LinkedIn company URL come from the profile, so the
five calls cannot all go in one message. Call `fetch_linkedin_profile` alone
first, then emit the other four together in the next message. If the profile
does not show the company's website, call `fetch_company` next on its own and
take the website from it, then emit the three searches together. Never guess
the company or its domain from the handle. This order replaces "exactly these
five … in a single assistant message" above for this case.

**Self-check before you reply:** if your reply contains fewer than five
`tool_use` blocks (after the optional `ToolSearch` setup turn), or fewer than
four when the profile was fetched first, you are violating the protocol.
Stop, rewrite the reply with all of them.

**Known limitation (`claude -p` headless mode):** Claude often serializes
these calls one-per-turn even with explicit instructions. That's fine — the
skill still produces the same output, just with sequential tool execution
(~30–40s total wall time vs. ~10–15s if truly parallelized). If you can
batch, do; if not, proceed sequentially without retrying or re-prompting.

No subagent fan-out. No second round of enrichment polling. **No `enrich_sender` call.**

## Synthesis (Single Pass After Batch Returns)

Merge the five results into the campaign brief. Treat `fetch_linkedin_profile` and
`fetch_company` as authoritative for sender/company facts; treat `WebSearch` as
proof and signals.

```markdown
## {Company} - Sender Research

**Sender Background:** [from fetch_linkedin_profile — name, current title, follower count, key experience]
**Company Context:** [from fetch_company.description OR positioning WebSearch top result]
**Industry / Size:** [from fetch_company.industry + employee range]
**Positioning Notes:** [positioning WebSearch top results + any positioning one-liner from fetch_company]
**Proof Options:** [case-study WebSearch hits — pick 1-3 with named customers / metrics]
**Credibility Signals:** [growth WebSearch hits (funding, hiring, press) + any high-signal posts from fetch_company]
**Gaps:** [what's still missing — usually fine to ship with this]
```

The synthesis result is what feeds the campaign brief, which is what message
generation reads via `campaignBriefContent` (see `getOfferPositioning()` in
`prompt.ts:236`). The brief is the load-bearing path now — `clientProspect`
contributes only fallback context and the sender display name (which is
resolved from the `Sender` row's profileData when `clientProspect` is null).

### Partial Failure Handling

The five calls run in parallel, so any one of them can fail independently
(404, rate-limit, timeout). Synthesis MUST still produce a usable brief.
Apply these rules:

- **If `fetch_company` errors or returns empty:** mark `Company Context` as
  `[unavailable — fetch_company failed]`. Use the positioning WebSearch top
  result as a partial substitute. Set `Industry / Size` to `[unavailable]`
  (do NOT guess). The brief still ships; downstream message generation reads
  the brief verbatim and is null-safe on missing fields.
- **If `fetch_linkedin_profile` errors:** fall back to WebSearch-derived
  sender background. Set `Sender Background` to a one-liner derived from the
  positioning WebSearch ("found via {companyDomain}/about and other web
  signals — full LinkedIn profile unavailable").
- **If a WebSearch errors:** mark the corresponding section
  (`Proof Options`, `Credibility Signals`, or `Positioning Notes`) as `[no
results]` and continue. Do NOT retry — one round only.
- **NEVER skip `complete_sender_research`** even on partial failure. Pass
  `proofItemsFound: 0`, `caseStudyItemsFound: 0`, `credibilitySignalsFound: 0`
  for the failed sections so the preflight knows the research ran.

**The contract:** synthesis always produces SOME brief. `complete_sender_research`
always fires. The brief ships even with gaps; downstream is null-safe.

## Optional Deepen (Only If Synthesis Reveals A Hard Gap)

If after synthesis you genuinely have zero proof and zero credibility
signals, AND the campaign fixture/operator told you proof is required, you
MAY issue ONE additional WebFetch on the most promising case-study URL from
the proof WebSearch. Cap at one WebFetch. No subagents, no second WebSearch round.

If the proof and positioning searches returned nothing from the company's own
site (common when the company name is an ordinary word), you MAY issue that
ONE WebFetch on the company's homepage instead, to read what it says it does
and any customers it names. Same cap.

If that still yields nothing, set `proofItemsFound: 0` in
`complete_sender_research` and proceed; the brief can ship without proof.

## Completion Marker (Required)

After synthesis, call:

`complete_sender_research({ depth, proofItemsFound, caseStudyItemsFound, credibilitySignalsFound, notes? })`

Count rules:

- `depth`: always `"parallel-batch"` for this protocol.
- `proofItemsFound`: number of concrete proof items you would confidently use.
- `caseStudyItemsFound`: number of usable case-study examples.
- `credibilitySignalsFound`: number of trust signals (reviews/ratings/funding/hiring/press).

If no reliable evidence is found, set counts to 0 and include that in `notes`.

## Progress UX

In a chat host where every line you write is posted to the customer (Slack),
write nothing here: the customer was already told research is under way, and
the next thing they should read is the result. Tool names, "in parallel",
time estimates and counts of proof items are not for customers.

In a terminal host, before issuing the batch:

```
Researching the sender and company…
```

After synthesis:

```
Sender research ready.
```

## What Changed From The Previous Protocol

- **Removed `enrich_sender` from the parallel batch entirely.** The MCP `create_campaign` tool now accepts `senderLinkedinUrl` as an alternative to `clientProspectId`. `enrich_sender` itself has been removed from the MCP tool registry — calling it returns "tool not found".
- Replaced with `fetch_company` (REQUIRED) — provides company description, industry, employee data that `enrich_sender` used to give via `companySnapshot`.
- Five parallel tool calls instead of four (added `fetch_linkedin_profile` + `fetch_company`, dropped `enrich_sender`).
- Added explicit Partial Failure Handling — synthesis always produces a brief even when one or two of the five calls error.
- No "minimal-verification vs deep-proof" branch. Always run the same parallel batch.
- No subagent fan-out — WebSearch from the orchestrator turn is faster and cheaper.
- Synthesis writes to the campaign brief, not to an `EnrichedProspect` row.
- One round, one synthesis, one `complete_sender_research` call.
