Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ description: "Import a user-supplied CSV/file into Leadbay through five phases w
---


Import the user's Leadbay file<if the user named one, render as " (<filename>)" with a leading space; otherwise empty. Source: Path or user-visible name of the CSV/file to import. If omitted, use the file the user attached or referenced.> and satisfy this instruction: <the user-supplied value if any; otherwise a sensible default. Source: Additional user goal, e.g. "then qualify the leads", "preserve owner phone as a custom field", or "only import restaurants in Manhattan".>.
Import the user's Leadbay file<if the user supplied this argument, render the short parenthetical or inline clause derived from it; otherwise empty. Source: Path or user-visible name of the CSV/file to import. If omitted, use the file the user attached or referenced.> and satisfy this instruction: <the user-supplied value if any; otherwise a sensible default. Source: Additional user goal, e.g. "then qualify the leads", "preserve owner phone as a custom field", or "only import restaurants in Manhattan".>.

# GOAL — what we're actually trying to do

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
---
name: leadbay_plan_tour_in_city
description: "Plan a field sales tour: in one flow, surface follow-ups + fresh Discover leads in the target city via `leadbay_tour_plan`, render to a map, draft in-area outreach via `leadbay_prepare_outreach`, and optionally persist the selected accounts as a named campaign via `leadbay_create_campaign`. Closes #3630 US1 end-to-end."
---


Plan a field sales tour for me in **<City or region the user is visiting (e.g. 'Limoges', 'Bay Area'). Used as the geo filter for both Monitor and Discover lookups. If not provided in the user's most recent message, ask once before proceeding.>**<if the user supplied this argument, render the short parenthetical or inline clause derived from it; otherwise empty. Source: When the visit is (e.g. 'May 24', 'next Thursday'). Surfaced in the outreach drafts as 'I'll be in <city> on <date>'.>.

GATE — DEFER TO TOOL RENDERING. When you call a Leadbay composite that ships its own RENDERING block (every composite in 0.9.0+ does), render the response using that block's recipe verbatim — score bars, glyph palette, column order, hide-list, link priorities, all of it. Do NOT substitute prose, a numbered list, or a different column structure even when an orchestrating prompt's body suggests alternate framing. Prompt-specific commentary (motivational nudges, summaries, next-action recommendations) belongs ABOVE or BELOW the canonical table, never in place of it.

If the prompt's body and the tool's RENDERING appear to conflict, the tool's RENDERING wins for the structural layout; the prompt's voice wins for the commentary that surrounds it.


# PHASE 1 — BUILD THE ITINERARY

Call `leadbay_tour_plan({city: "<the city (as extracted above)>"})` with the default counts (6 follow-ups + 6 discover). If the response is `status: "ambiguous_locations"`, surface the candidates and ask me to pick one, then re-call with `city_id`.

Split the returned `monitor_leads` into two buckets client-side using `last_monitor_action`:

- **Customers** — leads with any `last_monitor_action` history (CONTACTED, MEETING_BOOKED, etc.). Treat as known accounts with prior engagement.
- **Qualified prospects** — leads with high `ai_agent_lead_score` (or `score`) but no recent action.

`discover_leads` are the **New** bucket.

Aim for a 3+3+3 split if possible. If the customers bucket has fewer than 3, fill from qualified. If discover_filter_note indicates a low match ratio for the city, mention it: "Only N/30 fresh leads matched your city" — better honest than padded.

# PHASE 2 — RENDER THE MAP

Route the union of `monitor_leads + discover_leads` into `places_map_display_v0` (when the host exposes it). Per-lead `notes` string:

- `★ Customer — <one-sentence sector + why-now>. Reach <name>, <role>: <bare phone>, <bare email>.`
- `★ Qualified — <one-sentence>. Reach <name>...`
- `✦ New — <one-sentence>. Reach <name>...`

Skip leads with `location.pos === null` (no coordinates → no pin) — list them as "+ N leads without coordinates" below the widget.

Below the widget, emit a chat-prose summary grouped by mode (Customers / Qualified / New), with LinkedIn-linked contact name + bare phone/email pills per lead. Use the canonical `linking/contact-linkedin` rules.

# PHASE 3 — DRAFT IN-AREA OUTREACH (optional, ask first)

After the map, ask me ONCE: "Want me to draft 'I'll be in <the city (as extracted above)><the date_paren (as extracted above)>' outreach for the top accounts?" If I say yes, for each of the top 3 leads (1 Customer / 1 Qualified / 1 New), call `leadbay_prepare_outreach(leadId)` and route the draft through `message_compose_v1` with a single variant labeled "In-area visit" — body opens with the visit context, references the AI-summary angle, ends with a clear ask (15-min coffee / on-site stopover).

Serialize the prepare_outreach calls (max 3 in parallel — see the long-running-tools rule).

# PHASE 4 — PERSIST AS A CAMPAIGN (optional, ask first)

After drafts, ask me ONCE: "Save these 9 accounts as a campaign called '**<the city (as extracted above)> Tour<if the user supplied this argument, render the dash-prefixed phrase derived from it; otherwise empty. Source: When the visit is (e.g. 'May 24', 'next Thursday'). Surfaced in the outreach drafts as 'I'll be in <city> on <date>'.>**'?" If I say yes, call `leadbay_create_campaign({lead_ids: [...all_nine_lead_ids], name: "<the city (as extracted above)> Tour<the date_dash (as extracted above)>"})`. Surface the returned `id` + `name` as a confirmation line, and offer the NEXT STEPS chip "View progression" (which routes to `leadbay_campaign_progression`).

If I declined the campaign step, end the turn — the map + drafts are enough for an ad-hoc trip.

# PHASE 5 — STOP

Done. The map is the surface; the drafts are the action; the campaign is the persistence layer for managerial follow-up after the trip.
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
---
name: leadbay_setup_team_prospecting
description: "Manager-led prospecting setup: conversationally turn a natural-language audience ask into a Leadbay lens, validate the candidate leads, and persist them as one or more named campaigns the rep(s) can work through. Closes #3630 US3 end-to-end (within the current creator-scoped campaign visibility model)."
---


Set up manager-led prospecting for me: turn the audience into a lens, validate candidates, then persist as named campaigns.

Audience: **<Natural-language audience description (e.g. 'plumbing companies with 10-50 employees in Seine-Maritime'). The lens-creation step (`leadbay_refine_prompt` → `leadbay_create_lens`) interprets it. If not provided in the user's most recent message, ask once before proceeding.>**
<if the user supplied this argument, render the short block derived from it; otherwise empty. Source: Optional: how to split the validated leads into per-rep campaigns. Free text — e.g. 'split by city' or 'one campaign per rep: John gets Tulsa, Sarah gets OKC'.>

GATE — DEFER TO TOOL RENDERING. When you call a Leadbay composite that ships its own RENDERING block (every composite in 0.9.0+ does), render the response using that block's recipe verbatim — score bars, glyph palette, column order, hide-list, link priorities, all of it. Do NOT substitute prose, a numbered list, or a different column structure even when an orchestrating prompt's body suggests alternate framing. Prompt-specific commentary (motivational nudges, summaries, next-action recommendations) belongs ABOVE or BELOW the canonical table, never in place of it.

If the prompt's body and the tool's RENDERING appear to conflict, the tool's RENDERING wins for the structural layout; the prompt's voice wins for the commentary that surrounds it.


# PHASE 1 — INTERPRET INTENT INTO A LENS

Call `leadbay_refine_prompt({user_prompt: "<the audience (as extracted above)>"})`. This handles the clarification protocol natively — if the system needs more info (e.g. industry disambiguation, geography precision), it returns `status: "clarification_needed"` with options. Surface those to me; on my answer, re-call `leadbay_refine_prompt` until the prompt converges.

When the prompt has converged, call `leadbay_create_lens({user_prompt: <refined>, name: "<short descriptive name>"})` to create a draft lens, then `leadbay_promote_lens({lensId})` to make it the active lens.

# PHASE 2 — PULL + VALIDATE CANDIDATES

Call `leadbay_pull_leads({count: 20, lensId: <the new lens id>})` to surface the top 20 candidates from the freshly-created lens. Render with the canonical `pull_leads` table layout.

Ask me ONCE: "Want me to deep-research the top N for validation?" If yes, call `leadbay_research_lead_by_id` serialized over the top 3-5 (one at a time, max 3 in parallel per the long-running-tools rule). Surface a research summary per lead.

Then ask me ONCE: "Which of these should we drop?" If I name leads to drop, exclude them from the working set. The remaining is the validated set.

# PHASE 3 — DECIDE THE CAMPAIGN SHAPE

If I provided a `rep_split` ("one campaign per rep: John gets Tulsa, Sarah gets OKC"), partition the validated leads accordingly. If I didn't, ask ONCE: "Create one campaign for the whole batch, or split per rep / region / sector?" — surface 2-4 options via `ask_user_input_v0` when available, else as a bulleted list.

For each campaign-shape decision, derive a name. Templates:
- Whole batch: `"<lens-name> – <YYYY-MM-DD>"`
- Per rep: `"<lens-name> – <RepName>"`
- Per region: `"<lens-name> – <RegionName>"`

# PHASE 4 — PERSIST

For each campaign-shape partition, call `leadbay_create_campaign({lead_ids: [...partition], name: "<derived>"})`. Surface the returned `id` + `name` per campaign as a confirmation line.

# PHASE 5 — BE HONEST ABOUT SCOPE

Once the campaigns are created, surface this caveat in plain prose:

> Campaign visibility is currently scoped to the user who CREATED the campaign — the reps won't see these in their own MCP `leadbay_list_campaigns` calls. They CAN see them in the web UI at app.leadbay.ai → Campaigns. Cross-user MCP visibility would need backend work; flag this as a #3630 US3 product gap if your reps work primarily through MCP.

End with a NEXT STEPS chip via `ask_user_input_v0`: "View progression on one of these now?" → routes to `leadbay_campaign_progression`.

# PHASE 6 — STOP

Done. The lens is live, the validated cohort is persisted as named campaigns, and the manager knows where the cross-user-visibility gap is.
108 changes: 108 additions & 0 deletions .claude-plugin/plugins/leadbay/skills/leadbay_work_campaign/SKILL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
---
name: leadbay_work_campaign
description: "Work a campaign as a real outreach session: pick the campaign, assess what the user has (phones / emails / coords), then PROPOSE the right session mode (call sheet, email sheet, enrich titles first, map). After they pick, render — and as they dictate outcomes per lead, record both note + epilogue via `leadbay_report_outreach` in one round trip."
---


Work my **<the user-supplied value if any; otherwise a sensible default. Source: Campaign name (fuzzy match against your own campaigns) or campaign UUID. Omit to list and pick interactively.>** campaign as an outreach session<if the user supplied this argument, render the short parenthetical or inline clause derived from it; otherwise empty. Source: Optional: skip the readiness-assessment proposal and jump directly into 'call_sheet' / 'email_sheet' / 'map' / 'enrich_first'. Omit (recommended) and let the prompt propose based on the data.>.

GATE — DEFER TO TOOL RENDERING. When you call a Leadbay composite that ships its own RENDERING block (every composite in 0.9.0+ does), render the response using that block's recipe verbatim — score bars, glyph palette, column order, hide-list, link priorities, all of it. Do NOT substitute prose, a numbered list, or a different column structure even when an orchestrating prompt's body suggests alternate framing. Prompt-specific commentary (motivational nudges, summaries, next-action recommendations) belongs ABOVE or BELOW the canonical table, never in place of it.

If the prompt's body and the tool's RENDERING appear to conflict, the tool's RENDERING wins for the structural layout; the prompt's voice wins for the commentary that surrounds it.


# PHASE 0 — PICK THE CAMPAIGN

If I gave you a name or id, resolve it. Otherwise call `leadbay_list_campaigns()` and surface the active campaigns as a `single_select` via `ask_user_input_v0` (cap at 4 — sort by `updated_at` desc, archived hidden):

> Which campaign do you want to work?
> - <Name 1> · <N leads> · last touched <date>
> - <Name 2> · <N leads> · last touched <date>
> - …

When the user picks, capture the `campaign_id`. If `<Campaign name (fuzzy match against your own campaigns) or campaign UUID. Omit to list and pick interactively. Optional.>` is a name, fuzzy-match against `campaigns[].campaign.name`. On ambiguous matches, surface a `single_select` instead of guessing.

# PHASE 1 — FETCH + ASSESS READINESS (the load-bearing phase)

Call `leadbay_campaign_call_sheet({campaign_id})`. The response carries `summary` + `readiness` — use them to figure out what the user CAN actually do today, then PROPOSE the right session mode rather than auto-rendering.

**Read the summary numbers**:
- `total_leads`, `total_contacts`
- `leads_with_phone` — can call from this many leads
- `leads_with_email` — can email this many
- `leads_with_coords` — can map this many
- `leads_without_contacts` — these need enrichment before any outreach is possible
- `leads_already_contacted` — these have prior touches; the rep may want to skip them for cold work

**Read the `readiness` booleans** (pre-computed thresholds):
- `ready_for_calling` (phone coverage ≥60%) — call session viable
- `ready_for_emailing` (email coverage ≥60%) — email session viable
- `needs_enrichment` (≥30% no-contacts OR both phone+email coverage <40%) — enrichment recommended first
- `travel_friendly` (≥5 geocoded leads AND coord coverage ≥60%) — map mode worth proposing

**One-line situation report** (always emit BEFORE the proposal):

```
📋 <total_leads> leads · 📞 <leads_with_phone> with a phone · ✉ <leads_with_email> with an email · 🗺 <leads_with_coords> with coords · 🔴 <leads_without_contacts> need enrichment · ✅ <leads_already_contacted> already touched
```

**Then PROPOSE the right modes via `ask_user_input_v0`** (2-4 options, sorted by what makes the most sense for THIS campaign's data):

- "📞 Start calling now" — IF `ready_for_calling`. Top option when phones are there.
- "✉ Email session instead" — IF `ready_for_emailing` AND `email_ratio > phone_ratio`. Don't surface this when calling is more obvious.
- "🔧 Enrich titles first" — IF `needs_enrichment`. Top option when most leads have no contacts. Phrase as "<N> leads have no reachable contact yet — enrich titles before we start?" so the user understands the cost.
- "🗺 View on a map" — IF `travel_friendly` **AND** the user hasn't previously signaled disinterest in maps (check your conversation memory; if you've seen the user dismiss map renders before in this session or saved a "no maps" preference, drop this option).

If the MCP prompt argument `mode` was actually supplied, skip the proposal and jump to the matching mode below. If `mode` was omitted, do not treat `call_sheet` as implicit user consent — propose first.

# PHASE 2A — CALL-SHEET MODE (default after "📞 Start calling now")

Render per the `leadbay_campaign_call_sheet` RENDERING block — one CARD per lead with the 4-col contact table (Contact / Phone / Role / Recent). The phone in column 2 MUST be `[bare](tel:URL)` (use `contact.phone_tel_url` verbatim — the composite has already canonicalized it). The contact name in column 1 MUST be `[Name](linkedin_url)`. Email stacks under the name when present (`✉ [email](mailto_url)`). Recent stacks `📝 last note` + `📞 last_action_headline`.

End the turn with the standby line:

> Ready to start calling. Tell me what happened after each call — I'll record the note + outcome.

# PHASE 2B — EMAIL-SHEET MODE (after "✉ Email session instead")

Same data, slightly different render emphasis: drop the Phone column, put `✉ [email](mailto_url)` as column 2. Below each lead's table, generate a SUGGESTED short email draft per the next-step — but DON'T send. Drafts are for the user to copy-paste / send themselves.

# PHASE 2C — ENRICH-FIRST MODE (after "🔧 Enrich titles first")

Extract `leadIds` from `sheet.leads[].lead_id`, then call `leadbay_enrich_titles({leadIds, …})` (consult its description for titles / email / phone selection; do not pass `campaign_id`, because that is not part of the tool schema). Surface progress to the user. When complete, automatically loop back to Phase 1 (re-fetch the call sheet, re-assess readiness, re-propose).

# PHASE 2D — MAP MODE (after "🗺 View on a map")

Pass `response.map_locations` directly to `places_map_display_v0` — the composite has already built the per-pin notes string with the top contact's phone inline. After the widget, emit the standard 4-col card list anyway so the rich detail is still scannable.

# PHASE 3 — RECORD OUTCOMES, ONE AT A TIME (after the user starts dictating)

When the user says something like *"Called Bree, voicemail, trying again Tuesday"* or *"Talked to John, wants pricing sent next week"*, parse:

1. **Which lead** — by company name OR contact name (cross-reference with the cards you just rendered).
2. **The note** — the user's exact words about what happened (the SDR's voice — don't paraphrase).
3. **The outcome** — pick ONE of these four epilogue values based on what the user said:
- `STILL_CHASING` — pursuing, no decision yet ("trying again", "they'll get back to me")
- `COULD_NOT_REACH_STILL_TRYING` — voicemail, no answer, wrong number, gatekeeper blocked
- `INTEREST_VALIDATED_OR_MEETING_PLANED` — meeting booked, quote requested, "send me more info"
- `NOT_INTERESTED_LOST` — declined, "not now", "not a fit", "remove from list"

Call `leadbay_report_outreach({lead_id, note: <user's words>, epilogue_status: <picked>, verification: {source: "user_confirmed", ref: <user's exact words verbatim>}})`. Confirm in ONE line: *"✅ Logged: <Company> → <epilogue>. Next?"*

Then wait for the next dictation. Don't ask "anything else?" — just acknowledge and wait.

# PHASE 4 — STOP

When the user says "done" / "that's it" / "wrapping up" / similar, surface a session summary chip:

> Session complete — N calls logged: X meetings booked · Y still chasing · Z couldn't reach · W declined.

Optional: offer to review the `leadbay_campaign_progression` for the same campaign to see the updated counts.

# Iron laws

- The `verification` field on `leadbay_report_outreach` is REQUIRED. For calls (no message id), always use `{source: "user_confirmed", ref: <user's verbatim words>}`. Skipping it is forbidden; fabricating a gmail_message_id for a call is forbidden.
- ONE call → ONE `leadbay_report_outreach` invocation. Don't batch; each call has its own note + outcome.
- Map mode is OPT-IN, never automatic. The user invokes it via the proposal options or by passing `mode=map`.
- If you've seen the user dismiss / dislike map renders earlier in the session, don't propose map mode again.
- If the user dictates an outcome that doesn't cleanly map to one of the four epilogue values, ASK ONCE before guessing.
Loading
Loading