---
name: research-sender
description: "Parallel-first sender research protocol. One round of five batched tool calls (fetch_linkedin_profile + fetch_company + 3× WebSearch), 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 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.

## 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({ companyDomain })` — sender's company LinkedIn page (description, industry, employee range, recent posts). 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.

**Self-check before you reply:** if your reply contains fewer than five
`tool_use` blocks (after the optional `ToolSearch` setup turn), you are
violating the protocol. Stop, rewrite the reply with all five.

**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 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

Before issuing the parallel batch:

```
Pulling sender profile + company snapshot + case studies + growth signals + positioning in parallel (~10s)...
```

After synthesis:

```
Sender research ready — {proofItemsFound} proof items, {credibilitySignalsFound} credibility signals.
```

## 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.
