---
name: campaign-messages
description: Review leads in a campaign and craft/update messages with research.
visibility: internal
---

# Campaign Messages

Use this command when the user provides a campaignId and wants to research + craft messages for leads already imported into a campaign workflow table.

## Bootstrap (Required)

1. Load tools in a SINGLE message with ToolSearch calls:
   - `mcp__sellable__get_auth_status`
   - `mcp__sellable__get_campaign`
   - `mcp__sellable__get_campaign_messages_preview`
   - `mcp__sellable__get_table_rows`
   - `mcp__sellable__get_rows`
   - `mcp__sellable__get_message_prompt`
   - `mcp__sellable__update_cell`
   - `mcp__sellable__get_subskill_prompt`
   - `mcp__sellable__search_subskill_prompts`
2. Call `get_auth_status({})` and store as `auth`.
3. If `auth.ok !== true`, stop and show:
   - `auth.error.message`
   - `auth.error.guidance`
4. Call `get_subskill_prompt({ subskillName: "craft-message" })` and store as `craftPrompt`.

## Flow (Source of Truth)

Follow the flow in `mcp/sellable/skills/campaign-messages/flow.v1.json`.

```json
{
  "version": "v1",
  "workflow": "campaign-messages",
  "events": [
    "leads_selected",
    "drafts_ready",
    "messages_saved",
    "approval_complete"
  ],
  "steps": [
    "load-preview",
    "draft-messages",
    "save-messages",
    "approve-messages",
    "done"
  ]
}
```

## Tools

- `mcp__sellable__get_campaign`
- `mcp__sellable__get_campaign_messages_preview`
- `mcp__sellable__get_rows`
- `mcp__sellable__get_message_prompt`
- `mcp__sellable__update_cell`
- `WebSearch`
- `mcp__sellable__fetch_linkedin_profile` (optional, costs credits)
- `mcp__sellable__fetch_linkedin_posts` (optional, costs credits)
- `mcp__sellable__fetch_company` (optional, costs credits)
- `Task` (parallel subagents for batch)

## Flow

### 1) Load Campaign + Lead Preview

1. Extract `campaignId` (pattern: `cmp_...`). If missing, ask for it.
2. Call `get_campaign({ campaignId })`.
3. Immediately fetch the first page preview (no filters yet):

`get_campaign_messages_preview({ campaignId, tableId: workflowTableId, leadLimit: 25, page: 1 })`.

Then render the ASCII table and stats. After that, ask if they want to filter or page.

5. Render an ASCII table with the preview rows (show message previews, not checkmarks):

```
# | Name | Title | Company | Message | Approved?
1 | ...  | ...   | ...     | Hi ...  | ✓/✗
```

Use `messagePreview` for the Message column (show "—" if empty). Keep Approved as a checkmark based on `isApproved`.

If `stats` are present in the preview response, summarize them explicitly using ONLY these fields:

- totalRows
- messagesCount
- approvedCount
- needsApprovalCount
- enrichedCount
- needsEnrichCount

Never mention "pending" or "sent" here (those are campaign-level stats, not workflow-table stats). Never infer whole-table counts from the current page. Only claim "all approved" if `approvedCount === totalRows` and `messagesCount === totalRows`. If stats are missing, say "Showing page X of Y (page-only preview)".

Then ask: “Which leads should I craft messages for? (e.g., 1,3 or all)”

If they want filters, translate into filters using real column names from the table meta. Example:

`filters: [{ "column": "Message", "op": "is_empty" }, { "column": "Approved", "op": "eq", "value": false }]`

Then call:

`get_campaign_messages_preview({ campaignId, tableId: workflowTableId, leadLimit: 25, page: 1, filters })`.

If the user asks for more rows:

- Always page with `get_campaign_messages_preview({ campaignId, tableId, leadLimit, page, filters })` so stats and filters stay consistent.
- Use `get_table_rows` only when the user explicitly asks for the raw table rows tool.

Then re-render the ASCII table for that page and ask which rows to craft.

### Optional Research Toggle

Ask once per session (or when the user requests deeper research):

“Do you want deep research for these leads (parallel sonnet subagents)? Yes/No”

If **Yes**:

- Call `get_subskill_prompt({ subskillName: "research-prospect" })`
- For each lead, run the **Shared Flow** with `positioningMode=false`
- Use LinkedIn URL + company name when available
- If a Company LinkedIn URL exists in row data, pass it as `companyLinkedinUrl`
- Synthesize findings into a short “Research Notes” block per lead

If **No**: proceed directly to drafting.

### 2) Single Lead (in-thread)

1. Call `get_rows({ tableId, rowIds: selectedRowId })`.
2. Use `craftPrompt` as the authoritative craft rules (includes research subtask).
3. Call `get_message_prompt()`.
4. Craft message using the REPLY framework.
5. Show message + gates summary.
6. If the message cell is empty, **auto-save the draft** with `update_cell(messageCellId, message)` and say “Draft saved (not approved).”
7. If the message cell already has content, ask before overwriting. Only save on explicit user confirmation.
8. Ask if they want to approve for sending; if yes: `update_cell(approveCellId, true)`.

### 3) Batch (parallel)

If user selects multiple leads:

1. Call `get_message_prompt()` once.
2. Spawn parallel Task subagents for each row **with explicit `model: "opus"` and `subagent_type: "general-purpose"`** (never omit model):
   - Provide campaign brief + positioning + sender info
   - Include the full REPLY framework from `get_message_prompt()`
   - Provide row data + messageCellId + tableId
   - Use WebSearch + LinkedIn tools only if needed
   - **Do NOT** save in subagent; return draft + research notes only
3. Wait for all tasks.
4. For rows where the message cell is empty, auto-save drafts with `update_cell` and announce “Draft saved (not approved).”
5. Summarize drafts and ask which to overwrite or edit.
6. Save selected drafts with `update_cell`.
7. Ask whether to approve saved messages.

### Selection Logic

- If the user selects one row: run the single-lead flow.
- If the user selects multiple rows: run the batch flow.

### Optional: Research Subtask (if user requests deeper research)

Use `research-prospect` (wrapper over `mcp/sellable/skills/research/SKILL.md`) and run the Shared Flow with `positioningMode=false`. Only run this when explicitly asked to deepen research.

## Approval Rules

- Never auto-approve a message.
- Auto-save drafts is allowed **only** when the message cell is empty.
- Do not overwrite a non-empty message without explicit user approval.
- After saving, always ask whether to approve the message for sending.
