@vruum/skills 0.6.52 → 0.6.54
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/plugin.json +1 -1
- package/.codex-plugin/plugin.json +1 -1
- package/README.md +2 -2
- package/package.json +2 -2
- package/skills/create-content/SKILL.md +1 -1
- package/skills/csv-pipeline-fill/SKILL.md +2 -2
- package/skills/demand-gen-loop/SKILL.md +8 -8
- package/skills/diagnose-reply/SKILL.md +8 -8
- package/skills/engagement-triage/SKILL.md +4 -4
- package/skills/expansion-fill/SKILL.md +1 -1
- package/skills/ingest-meetings/SKILL.md +6 -2
- package/skills/{campaign-builder → objective-builder}/SKILL.md +27 -24
- package/skills/objective-doctor/SKILL.md +139 -0
- package/skills/outreach-triage/SKILL.md +9 -9
- package/skills/pipeline-fill/RESEARCH-ENGINE.md +29 -30
- package/skills/pipeline-fill/SKILL.md +29 -29
- package/skills/sales-nav-deep-fill/SKILL.md +1 -1
- package/skills/vruum-guide/SKILL.md +11 -11
- package/skills/winback-fill/SKILL.md +1 -1
- package/skills/yc-pipeline-fill/SKILL.md +5 -5
- package/skills/campaign-doctor/SKILL.md +0 -121
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "vruum",
|
|
3
|
-
"version": "0.6.
|
|
3
|
+
"version": "0.6.54",
|
|
4
4
|
"description": "Vruum AI skills + remote MCP server for B2B GTM teams. Slash commands for outreach triage, engagement triage, pipeline filling, prospect enrichment, and reply diagnosis, paired with the full Vruum MCP tool surface over OAuth 2.1.",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "Vruum AI",
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "vruum",
|
|
3
|
-
"version": "0.6.
|
|
3
|
+
"version": "0.6.54",
|
|
4
4
|
"description": "Vruum AI skills + remote MCP server for B2B GTM teams. Skills for outreach triage, engagement triage, pipeline filling, prospect enrichment, and reply diagnosis, paired with the full Vruum MCP tool surface over OAuth 2.1.",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "Vruum AI",
|
package/README.md
CHANGED
|
@@ -86,8 +86,8 @@ the public skill instead of leaving a broken link.
|
|
|
86
86
|
## Skills
|
|
87
87
|
|
|
88
88
|
<!-- generated:skills-begin -->
|
|
89
|
-
- `/
|
|
90
|
-
- `/
|
|
89
|
+
- `/objective-builder` — Build and launch an outreach campaign from criteria in about five prompts: filter contacts by size, industry, persona, region, or list; preview the cohort; create the campaign (optionally cloning messaging from an existing one); assign people; review and launch. Use when: create a campaign, build a campaign, new campaign from criteria, campaign from my list.
|
|
90
|
+
- `/objective-doctor` — Diagnose and fix struggling outreach campaigns. Use when: fix a campaign, diagnose campaign, why is my campaign not working, campaign health, low reply rate, check campaigns, which campaigns need help.
|
|
91
91
|
- `/create-content` — Co-produce an on-voice LinkedIn content post — pull your own signal, steer the angle conversationally, draft in your voice, then save as draft, schedule, or publish. Use when: write a post, draft LinkedIn content, create content, post about, content co-production, help me write a post.
|
|
92
92
|
- `/csv-pipeline-fill` — CSV harness source for /pipeline-fill. Reads a CSV, auto-detects headers, maps columns, hands off to /pipeline-fill for harness deep research and import. Use when: import CSV, paste a CSV, csv import, prospect list from CSV, csv harness mode.
|
|
93
93
|
- `/deal-triage` — Triage your active deal pipeline. Flags at-risk deals, surfaces stalled-deal alerts, runs MEDDIC qualification, and recommends next actions. Use when: review deals, triage deals, check pipeline, deal review, morning deals, pipeline review, deal health, at-risk deals.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@vruum/skills",
|
|
3
|
-
"version": "0.6.
|
|
3
|
+
"version": "0.6.54",
|
|
4
4
|
"description": "Vruum AI skills for Claude Code, Claude Desktop, Codex CLI, and any AI assistant with a skill directory. Slash commands for outreach triage, engagement triage, pipeline filling, prospect enrichment, and reply diagnosis. Pairs with the Vruum MCP server at https://api.vruum.ai/mcp.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -41,5 +41,5 @@
|
|
|
41
41
|
"outreach",
|
|
42
42
|
"gtm"
|
|
43
43
|
],
|
|
44
|
-
"contentHash": "
|
|
44
|
+
"contentHash": "c8dccb170a520d40b4e2b548ff71989d46135e307833b5416c782aecf3ea0d2f"
|
|
45
45
|
}
|
|
@@ -92,7 +92,7 @@ Then save it **once** with `manage_content` `action="draft"`, payload `{content,
|
|
|
92
92
|
|
|
93
93
|
Carousels (document posts) are the top-performing organic format. If the seller has (or you produce) a PDF, image, or video for this post:
|
|
94
94
|
|
|
95
|
-
1. Store the asset: `
|
|
95
|
+
1. Store the asset: `manage_content` kind='ad' action='store_creative'. Small files: payload `{asset_base64, filename}`. Real files (PDFs/videos — primary path): `{filename, size_bytes, content_type: 'application/pdf' | 'video/mp4'}` → PUT the file to the returned `upload_url` (`curl --fail-with-body -T <file> '<upload_url>'`), then call store_creative again with `{creative_id}` to finalize.
|
|
96
96
|
2. Attach it: include `attachment_creative_id` in the draft payload (or add it later with `action="edit"`). Explicit `attachment_creative_id: null` on edit detaches.
|
|
97
97
|
3. The stored `filename` renders as the LinkedIn document **title** — name it like a headline, not `export-final-v3.pdf`.
|
|
98
98
|
4. Before publishing, open the `attachment_url` from `get_content_review post_ids=[<post_id>]` and review the actual file — it publishes under the seller's identity.
|
|
@@ -15,7 +15,7 @@ You are the CSV harness-mode source for `/pipeline-fill`. You read a CSV the ope
|
|
|
15
15
|
## Inputs
|
|
16
16
|
|
|
17
17
|
- `file_path`: absolute path to the CSV. If not provided, ask for it. Common locations: `~/Downloads/`, `.context/attachments/` in the workspace.
|
|
18
|
-
- `
|
|
18
|
+
- `objective`: target objective (single)
|
|
19
19
|
- `column_mapping` (optional): explicit `{header_name: canonical_field}` mapping if headers don't auto-resolve. Default: auto-detect.
|
|
20
20
|
|
|
21
21
|
## Workflow
|
|
@@ -110,7 +110,7 @@ Emit the canonical handoff prompt (defined in `pipeline-fill/RESEARCH-ENGINE.md`
|
|
|
110
110
|
```
|
|
111
111
|
Candidate list ready: {N} prospects from csv (after {dedup_count} dedup).
|
|
112
112
|
|
|
113
|
-
NEXT: invoke /pipeline-fill Step 3 onward (deep research → harness gate → save) with this list and
|
|
113
|
+
NEXT: invoke /pipeline-fill Step 3 onward (deep research → harness gate → save) with this list and objective {objective_id}.
|
|
114
114
|
|
|
115
115
|
Continue automatically? (y/n)
|
|
116
116
|
```
|
|
@@ -33,7 +33,7 @@ Turn the goal into targeting criteria and **preview before committing**:
|
|
|
33
33
|
- `search` with `type="people"` and the criteria (include `filters={research_status: "all"}` so stub imports are visible). Read the **total count** and show a **5-row sample** (name, title, company, persona, the attributes that matched).
|
|
34
34
|
- Iterate the criteria conversationally until the cohort is the right size and shape for the goal. An empty preview means the criteria are too tight — loosen and re-run; never proceed to boost on a zero-count audience.
|
|
35
35
|
|
|
36
|
-
This is the same segmentation conversation `/
|
|
36
|
+
This is the same segmentation conversation `/objective-builder` runs — **do not reproduce its steps here.** If the seller wants to turn this cohort into an outreach objective too, hand off to **`/objective-builder`**. For the boost itself the audience is **firmographic facets** — LinkedIn-native attribute targeting (titles, seniorities, industries, locations, company sizes), the same way LinkedIn's own boost targets. Resolve names to entity URNs FIRST via `fetch type='ads' subtype='targeting_entities'` (facet + q), then hold the resolved `{facets}` map. **`locations` is required** — a campaign without a location facet is rejected. (`{matched_audience_id}` still works when a pre-uploaded audience exists; `{criteria}`-built matched audiences are partner-gated on LinkedIn's side and return a clear error pointing back to facets.)
|
|
37
37
|
|
|
38
38
|
Hold the settled audience (the resolved `{facets}` map — shown to the seller as plain names with counts where available — or a `matched_audience_id`). It is one of the four things the seller approves in Step 4.
|
|
39
39
|
|
|
@@ -41,12 +41,12 @@ Hold the settled audience (the resolved `{facets}` map — shown to the seller a
|
|
|
41
41
|
|
|
42
42
|
**Copy — hand off, don't write it here.** Invoke **`/create-content`** to co-produce the on-voice post. That skill owns author resolution, signal grounding, the steer→draft loop, and the publish guards — narrate it as it works, but never reproduce its drafting procedure. It leaves you a **draft** content post (its id is what Step 5 boosts). Keep the post a draft for now — publish is gated behind the seller's approval in Step 4. Two post-level decisions to settle WITH the copy (both settable at draft/edit):
|
|
43
43
|
|
|
44
|
-
- **Identity** — `author_identity: 'member'` (a person's profile → the boost runs as a **Thought Leader Ad**, awareness/engagement only, and the author must be authorized via `
|
|
44
|
+
- **Identity** — `author_identity: 'member'` (a person's profile → the boost runs as a **Thought Leader Ad**, awareness/engagement only, and the author must be authorized via `manage_content` kind='ad' action='authorize_author') or `'organization'` (the **Company Page** → all objectives, incl. clicks; needs the Page set once via action='set_page').
|
|
45
45
|
- **Destination** — if the goal is clicks, put the `external_link` on the post NOW (UTMs are stamped automatically). WEBSITE_VISIT without a destination is rejected at boost time.
|
|
46
46
|
|
|
47
47
|
**Visual — generate or supply it, then store it as a draft.**
|
|
48
48
|
|
|
49
|
-
- **Image**: generate with your own image tools, then store via `
|
|
49
|
+
- **Image**: generate with your own image tools, then store via `manage_content` kind='ad' action='store_creative', payload `{asset_base64 (raw base64, no data: URL prefix — renamed from image_base64 in VRU-726), generation_prompt, filename?, generation_provenance: {model, tool, generated_at, notes}}`.
|
|
50
50
|
- **Document / PDF carousel** (≤25MB — VRU-726, the top organic format): produce the PDF with your own tools, then store via the same action — small files: `{asset_base64, filename: 'my-deck.pdf'}` (no generation_prompt needed); real files (primary path): `{filename, size_bytes, content_type:'application/pdf'}` → returns a **presigned upload_url**; `curl --fail-with-body -T my-deck.pdf '<upload_url>'`, then call store_creative again with `{creative_id}` to finalize. The `filename` becomes the rendered LinkedIn carousel **title**. Attach to a post with `manage_content` action='draft'|'edit' payload `{attachment_creative_id}`, open the `attachment_url` from `get_content_review post_ids=[...]` to review the actual file, then publish.
|
|
51
51
|
- **Video** (mp4, ≤200MB, 3s–30min): store via the same action — `{media_url: <public https url>}` for a hosted file, or `{filename, size_bytes, content_type:'video/mp4'}` for the presigned flow above. Optional `{thumbnail_base64}`. **media_url stores are async** — poll `fetch type='ads' subtype='creative' id=<creative_id>` until `upload_status` leaves `'uploading'`; a `'failed'` status with a probe-code error means re-export the file, not retry. Videos attach to posts the same way (`attachment_creative_id` at draft/edit — the old publish-time `creative_id` param is retired), which also makes **scheduled video posts** carry their media.
|
|
52
52
|
- A stored creative (document/PDF, image, or video) attaches to the organic post at **draft/edit** (`manage_content` payload `{attachment_creative_id}`; explicit null detaches) — one asset serves the organic post AND the ad, and attachments persist on the row so scheduled posts publish with their media.
|
|
@@ -76,11 +76,11 @@ If the seller hesitates on any of the four, stop at draft and leave the loop res
|
|
|
76
76
|
|
|
77
77
|
Only after the Step 4 approvals:
|
|
78
78
|
|
|
79
|
-
0. **Identity prerequisites (first run only)** — a Page campaign needs the Company Page set (`
|
|
79
|
+
0. **Identity prerequisites (first run only)** — a Page campaign needs the Company Page set (`manage_content` kind='ad' action='set_page'; call with no organization_urn to discover the candidates); a Thought Leader Ad needs the author authorized (action='authorize_author'). Errors from boost name the exact fixing call — run it and retry rather than improvising.
|
|
80
80
|
1. **Publish the organic post** — first make sure the approved media is attached to the draft (`manage_content action=edit` payload `{attachment_creative_id}` — the old publish-time `creative_id` param is retired and now returns a 400), then `manage_content action=publish` on the draft from `/create-content`. This inherits `/create-content`'s author guard: if the chosen author's LinkedIn account isn't connected/healthy, publish fails hard rather than posting under another identity — surface that to the seller, don't retry blindly. **Wait for the post to actually be `published`** (a video publish transfers media and can take a while — re-read the post before boosting; never boost a still-publishing post).
|
|
81
|
-
2. **Boost the published post** — `
|
|
81
|
+
2. **Boost the published post** — `manage_content` kind='ad' action='boost', payload `{content_post_id: <the just-published post id>, vehicle?, objective: <the approved objective>, budget: {daily_budget_cents | total_budget_cents}, audience: {facets} OR {matched_audience_id}, duration_days?, approval_mode}`. Vehicle is inferred from the post's identity — pass the objective explicitly (the default is BRAND_AWARENESS, which is NOT what a click campaign wants). Use the `approval_mode` the seller authorized in Step 4 — `draft` unless they explicitly approved the budget for `auto`. The boost double-submit case is handled for you (idempotent per source + audience + vehicle + objective), so don't paper over a retry with a second call. (DSC alternative: `creative_id` instead of `content_post_id` runs the video without a post — metrics-only, no bridge.)
|
|
82
82
|
3. **If the campaign involves a video ad**, the LinkedIn media upload runs in the background after approval — the response tells you; poll `fetch type='ads' subtype='campaign' id=<campaign_id>` (~every 30s) until it reports live or a failure with its cause.
|
|
83
|
-
4. **Thought Leader boosts return a Campaign Manager handoff, not a live campaign** — LinkedIn's public API cannot attach a member's post to the campaign (verified live), so the boost/approve response comes back with `tla_manual_attach_required` plus a Campaign Manager deep link and the exact attach steps (Ads → Add ad → Sponsor existing content → Thought leader content → pick the post; the member approves sponsorship once → Launch). Relay the link + steps to the seller verbatim; this is the expected TLA flow, not an error. **While in Campaign Manager, have them verify bidding** — the default can be manual CPM (~$10/1k impressions), which barely delivers; switch to Maximum delivery. After they launch, run `
|
|
83
|
+
4. **Thought Leader boosts return a Campaign Manager handoff, not a live campaign** — LinkedIn's public API cannot attach a member's post to the campaign (verified live), so the boost/approve response comes back with `tla_manual_attach_required` plus a Campaign Manager deep link and the exact attach steps (Ads → Add ad → Sponsor existing content → Thought leader content → pick the post; the member approves sponsorship once → Launch). Relay the link + steps to the seller verbatim; this is the expected TLA flow, not an error. **While in Campaign Manager, have them verify bidding** — the default can be manual CPM (~$10/1k impressions), which barely delivers; switch to Maximum delivery. After they launch, run `manage_content` kind='ad' action='resume' id=<campaign_id> to sync the local status to live. Page-identity boosts are unaffected (fully API-driven end to end).
|
|
84
84
|
|
|
85
85
|
Report back what went live: the published post and whether the boost is a draft awaiting approval in the queue, pushed live, awaiting the Campaign Manager attach (thought-leader handoff), or uploading video in the background.
|
|
86
86
|
|
|
@@ -89,13 +89,13 @@ Report back what went live: the published post and whether the boost is a draft
|
|
|
89
89
|
The loop doesn't end at "boosted." Teach the operating rhythm:
|
|
90
90
|
|
|
91
91
|
- **Engagement** — `fetch` type=post_analytics (omit the id for all posts, or pass the post id) for impressions / reactions / comments and the per-post `engagers` sample. `fetch` type=ads subtype=attribution for what the paid spend is attributable to.
|
|
92
|
-
- **The bridge is YOU** — engagers on your own published/boosted posts are captured, researched, and ICP-scored automatically, and then they WAIT: nothing auto-enrolls into
|
|
92
|
+
- **The bridge is YOU** — engagers on your own published/boosted posts are captured, researched, and ICP-scored automatically, and then they WAIT: nothing auto-enrolls into objectives (VRU-721). ICP-passing engagers land on the engager review surface (`get_engagement_review` with `source='engagers'`; near misses shown with their scores) and the daily briefing nudges when any sit undecided past 72h. Run **`/engagement-triage`** (scope: engagers) to decide each one — objective add or one-off via the existing tools, then record the decision with `acted_via` so the boost→engager→outcome funnel in `fetch type=ads subtype=attribution` stays measurable. Point them there — don't reproduce its review procedure.
|
|
93
93
|
|
|
94
94
|
Close by naming what shipped this session (post live, boost drafted/pushed, first engagers visible) and what the next check-in should look at.
|
|
95
95
|
|
|
96
96
|
## Hard rules
|
|
97
97
|
|
|
98
|
-
- **Hand off, never re-teach.** `/create-content` owns copy, `/
|
|
98
|
+
- **Hand off, never re-teach.** `/create-content` owns copy, `/objective-builder` owns objective segmentation, `/engagement-triage` owns engagement review. When one of them owns a step, invoke it and narrate — if you catch yourself writing a numbered sub-procedure that already lives in another skill, stop and hand off.
|
|
99
99
|
- **Inherit every safety gate.** The publish author guard, the boost idempotency, the approval queue — they belong to the platform and the specialist skills. Never bypass, summarize past, or pre-approve through them.
|
|
100
100
|
- **The approval gate is not optional and not summarizable.** Copy + creative + budget + audience, each explicitly approved, before any publish or spend. Default to draft. Never `approval_mode='auto'` without an explicit budget yes.
|
|
101
101
|
- **Tailor from reads, not stereotypes.** Goal, audience, and angle all cite the seller's real numbers from Step 1. If a read fails, say what you couldn't see — don't fill the gap with a guess.
|
|
@@ -28,7 +28,7 @@ From the person_360 response, read the full conversation thread. Identify:
|
|
|
28
28
|
|
|
29
29
|
## Step 3: Diagnose
|
|
30
30
|
|
|
31
|
-
Call `get_performance_metrics` with `view='funnel'` and the
|
|
31
|
+
Call `get_performance_metrics` with `view='funnel'` and the objective_id to get objective-level reply rates by channel.
|
|
32
32
|
|
|
33
33
|
Synthesize a diagnosis:
|
|
34
34
|
|
|
@@ -37,13 +37,13 @@ Synthesize a diagnosis:
|
|
|
37
37
|
**The message that got the reply:** T3 via LinkedIn DM, 'workflow_pain' angle, referenced their recent job posting for DevOps engineer (specific signal), ended with a question about their current tooling.
|
|
38
38
|
|
|
39
39
|
**Why it likely worked:**
|
|
40
|
-
- Channel: LinkedIn DM has [X]% reply rate in this
|
|
41
|
-
- Angle: 'workflow_pain' converts at [X]% in this
|
|
40
|
+
- Channel: LinkedIn DM has [X]% reply rate in this objective (vs [Y]% email)
|
|
41
|
+
- Angle: 'workflow_pain' converts at [X]% in this objective ([Z]x above average)
|
|
42
42
|
- Signal: Referenced a specific, verifiable company signal (job posting)
|
|
43
43
|
- CTA: Question CTA has [X]x lift over statement CTAs
|
|
44
44
|
|
|
45
45
|
**What's different from the ghosted messages:**
|
|
46
|
-
- T1 and T2 used 'cost_replacement' angle (
|
|
46
|
+
- T1 and T2 used 'cost_replacement' angle (objective average: [X]%)
|
|
47
47
|
- T1 had surface-level personalization (company name only)
|
|
48
48
|
- T2 had no question CTA
|
|
49
49
|
|
|
@@ -52,10 +52,10 @@ Synthesize a diagnosis:
|
|
|
52
52
|
## Step 4: Suggest action
|
|
53
53
|
|
|
54
54
|
**If positive reply** (interested, wants to learn more, asks a question):
|
|
55
|
-
- "This combination is worth repeating: [angle + specific signal + CTA style]. Note it in your
|
|
55
|
+
- "This combination is worth repeating: [angle + specific signal + CTA style]. Note it in your objective's strategy so the next batch follows the same shape."
|
|
56
56
|
|
|
57
57
|
**If negative reply** (not interested, wrong person, bad timing):
|
|
58
|
-
- "This is the [N]th rejection using the '[angle]' strategy in this
|
|
58
|
+
- "This is the [N]th rejection using the '[angle]' strategy in this objective. If the pattern holds, consider deprioritizing this angle for the objective."
|
|
59
59
|
|
|
60
60
|
**If objection or question** (pricing, timing, skepticism):
|
|
61
61
|
- "This is a live conversation. The reply response draft should address [specific objection]. Check the outreach queue — there may be a draft pending."
|
|
@@ -64,5 +64,5 @@ Synthesize a diagnosis:
|
|
|
64
64
|
|
|
65
65
|
- This skill is diagnostic, not prescriptive. It explains what happened; acting on the pattern (deprioritizing an angle, repeating a winning combination) is a separate, deliberate decision.
|
|
66
66
|
- When the conversation has many touches, focus on the touch that triggered the reply, not the full sequence.
|
|
67
|
-
- Always compare against
|
|
68
|
-
- If `get_performance_metrics` doesn't have enough data for the
|
|
67
|
+
- Always compare against objective averages to distinguish signal from noise. One data point doesn't make a pattern.
|
|
68
|
+
- If `get_performance_metrics` doesn't have enough data for the objective, say so: "Not enough objective data to compare against. This is a single data point."
|
|
@@ -199,12 +199,12 @@ Engagers are the INBOUND direction: people who reacted to or commented on YOUR p
|
|
|
199
199
|
|
|
200
200
|
**Present each person** with score, why (match_summary), what they did (the engagements with post context), and how stale. Recommend one of three decisions.
|
|
201
201
|
|
|
202
|
-
**CHECK `in_motion` FIRST.** `in_motion_reasons` flags replied / meeting_booked / open_deal / plan_* — these people are already in a live motion. Acting on them risks double outreach or resetting a deliberately deferred plan. For in-motion persons the usual right call is dismiss-with-note or a deliberate, context-aware one-off — never
|
|
202
|
+
**CHECK `in_motion` FIRST.** `in_motion_reasons` flags replied / meeting_booked / open_deal / plan_* — these people are already in a live motion. Acting on them risks double outreach or resetting a deliberately deferred plan. For in-motion persons the usual right call is dismiss-with-note or a deliberate, context-aware one-off — never an objective add.
|
|
203
203
|
|
|
204
204
|
**The three decisions** (all via `manage_engagements`, `id` = the person UUID, NOT an engagement id):
|
|
205
205
|
|
|
206
206
|
1. **Act, then record.** Order matters — act FIRST with existing tools, THEN record the decision so attribution stays measurable:
|
|
207
|
-
-
|
|
207
|
+
- Objective add: `manage_outreach` action=objective_members → then `manage_engagements` action=`engager_actioned`, id=person_id, payload=`{acted_via: {campaign_id: "<uuid>"}}` (the engager-attribution key is still literally `campaign_id` — VRU-878 gap, out of the MCP-facade scope).
|
|
208
208
|
- One-off touch: `manage_messages` action=`send`/`send_linkedin` (returns the message_id) → then `engager_actioned` with payload=`{acted_via: {message_id: "<uuid>"}}`.
|
|
209
209
|
- An `engager_actioned` without `acted_via` returns an `unattributed` warning — the engager→outcome funnel goes blind. Always pass it.
|
|
210
210
|
- Actioning a sub-70 near miss is allowed (mints their CRM row from the persisted score) — do it when the human read beats the score.
|
|
@@ -247,7 +247,7 @@ After the first batch returns for any engagement type:
|
|
|
247
247
|
|
|
248
248
|
- **All reactions clean:** "First batch of reactions all approved. N more look similar — approve the rest?" Apply without more agents.
|
|
249
249
|
- **All comments have the same issue** (e.g., all too generic, all missing sender voice): Flag the pattern to the user. "First 8 comments are all generic 'great post' style — likely a prompt issue. Want me to edit them all with the same fix, or skip the batch?"
|
|
250
|
-
- **Systematic voice mismatch:** If comments consistently don't sound like the sender, flag it as
|
|
250
|
+
- **Systematic voice mismatch:** If comments consistently don't sound like the sender, flag it as an objective/prompt config issue rather than fixing each one individually.
|
|
251
251
|
|
|
252
252
|
### Step 8: Summary
|
|
253
253
|
|
|
@@ -259,7 +259,7 @@ After all queues are processed, present a summary:
|
|
|
259
259
|
- Skipped
|
|
260
260
|
- Plans stopped (from skip cascades)
|
|
261
261
|
- Content posts approved/scheduled
|
|
262
|
-
- Engagers actioned (
|
|
262
|
+
- Engagers actioned (objective adds / one-offs, with acted_via) and dismissed
|
|
263
263
|
|
|
264
264
|
## Edge cases
|
|
265
265
|
|
|
@@ -64,7 +64,7 @@ For each row, include a one-line "hook" — a specific recent observation from `
|
|
|
64
64
|
|
|
65
65
|
Step 6 — Hand off to outreach. Two options:
|
|
66
66
|
- **Option A (recommended)**: Approve the ranked list, then for each account: run `/pipeline-fill` with that prospect_list — same Sales Nav harness flow, just sourced from the expansion cohort instead of cold. Tag the resulting `outreach_plans.tag` with `bowtie_pilot:expansion` so success-tracking finds them.
|
|
67
|
-
- **Option B**: Direct `manage_outreach` action=start with an expansion-flavored
|
|
67
|
+
- **Option B**: Direct `manage_outreach` action=start with an expansion-flavored objective (pre-create an `expansion_<your-tenant>` objective with the right tone — formal, ROI-focused, no opener-hooks since the customer already knows you).
|
|
68
68
|
|
|
69
69
|
Step 7 — Success tracking (auto). When a calendar webhook fires a `meeting_booked` event on an outreach plan tagged `bowtie_pilot:expansion`, the webhook handler in `backend/app/domains/calendar/` auto-records the impact event, equivalent to:
|
|
70
70
|
```
|
|
@@ -28,11 +28,15 @@ This is an analyst's job, not a batch import. The attribution step (which person
|
|
|
28
28
|
- **Set** → the window is everything *after* that date.
|
|
29
29
|
- **`null`** (no meeting logged yet — first run) → **ask the operator for a seed date** ("Ingest meetings since when? (default: last 30 days)"). Never silently default to the whole archive.
|
|
30
30
|
|
|
31
|
-
2. **Find candidates.** `search` type=kb with `filters={
|
|
31
|
+
2. **Find candidates — deterministic recency listing, NOT a keyword search.** `search` type=kb with `filters={doc_type: "connector", modified_after: "<watermark or seed date, ISO-8601>", include_content: false}` and **no `query`**. This returns every synced connector document modified after the watermark, newest first (up to 100). Connector results carry `source_kind='connector'`, the meeting date in `modified_at`, a Drive `url`, and predictable filename shapes:
|
|
32
32
|
- **Gemini:** `… - Notes by Gemini`, `… - Transcript`, `… - Live Notes`
|
|
33
33
|
- **Read.ai:** `… - Read Meeting Report`, `… Smart Notes`
|
|
34
34
|
|
|
35
|
-
|
|
35
|
+
> **Never use a keyword query for this step.** A query (e.g. "meeting notes transcript live notes") is a relevance-ranked top-N sample over the whole archive — on a Drive with years of old transcripts, recent meetings routinely fall below the relevance cutoff and the run wrongly concludes there is nothing new. Keyword search is fine later for looking things up; candidate discovery must be the `modified_after` listing.
|
|
36
|
+
|
|
37
|
+
3. **Keep only the meeting artifacts.** The listing is every synced doc in the window, so drop non-meeting files (specs, sheets, decks) by the filename shapes above and obvious content. The Drive's historical archive is intentionally left KB-searchable-only, NOT re-ingested into the CRM. Logging an old meeting (and minting "follow up next week" tasks from a meeting that happened a year ago) is noise.
|
|
38
|
+
|
|
39
|
+
> **The window field is Drive *modified* time, not the meeting time.** They usually track each other, but an OLD transcript someone re-edits re-enters the window looking "new" — check the meeting date in the title/content, and the Step 5 idempotency marker catches anything already logged. If the listing returns exactly 100 documents, the window overflowed and the OLDEST part was cut (results are newest-first) — tell the user and pull the remainder via the Drive MCP alternative below; a narrower window can NOT recover it (the filter is a lower bound only).
|
|
36
40
|
|
|
37
41
|
4. Present the surviving candidates as a short list: `name · meeting date · one-line summary`. **If none are newer than the watermark, say so and stop** — there are no new meetings to ingest.
|
|
38
42
|
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
---
|
|
2
|
-
name:
|
|
2
|
+
name: objective-builder
|
|
3
3
|
description: >-
|
|
4
4
|
Build and launch an outreach campaign from criteria in about five prompts:
|
|
5
5
|
filter contacts by size, industry, persona, region, or list; preview the
|
|
@@ -7,9 +7,12 @@ description: >-
|
|
|
7
7
|
one); assign people; review and launch. Use when: create a campaign, build a
|
|
8
8
|
campaign, new campaign from criteria, campaign from my list.
|
|
9
9
|
---
|
|
10
|
-
#
|
|
10
|
+
# Objective Builder
|
|
11
11
|
|
|
12
|
-
|
|
12
|
+
(Formerly `campaign-builder` — renamed VRU-878, the MCP surface now speaks
|
|
13
|
+
objectives rather than campaigns. A legacy campaign is a mirror objective.)
|
|
14
|
+
|
|
15
|
+
You guide the seller from "I want to reach this kind of person" to a launched objective in about five exchanges. The conversation IS the segmentation tool: criteria in plain language, a previewed cohort, an objective reusing what already works, an explicit launch confirmation. Never launch anything without the confirmation step.
|
|
13
16
|
|
|
14
17
|
## Step 1: Capture the criteria
|
|
15
18
|
|
|
@@ -34,29 +37,29 @@ Iterate with the seller until the cohort is right ("too broad — only the US on
|
|
|
34
37
|
|
|
35
38
|
### Named-account sourcing handoff
|
|
36
39
|
|
|
37
|
-
If the criteria names organizations/accounts and the preview has fewer people than needed, do not create an empty
|
|
40
|
+
If the criteria names organizations/accounts and the preview has fewer people than needed, do not create an empty objective or pretend the accounts are contacts. Hand off the missing-account cohort to `/pipeline-fill` discovery before Step 3:
|
|
38
41
|
|
|
39
42
|
- Build `source_policy` using `pipeline-fill/contracts/source-policy.schema.json` from the installed skills bundle.
|
|
40
43
|
- Preserve every explicit provider instruction. Example: "use Clay, no Sales Nav or CSV" becomes `selected_source: "clay"`, `source_mode: "preferred"`, `prohibited_sources: ["sales_nav", "linkedin", "csv"]`, and an ordered `allowed_fallbacks` list.
|
|
41
44
|
- CSV and Sales Nav remain fully supported sources when the seller explicitly selects them. Source prohibitions are scoped to this run only; never turn one seller's preference into a global capability restriction or silently substitute a prohibited source.
|
|
42
45
|
- Use the schema defaults unless the seller overrides them: company waves 10, person waves 5, and two retries for transient failures. `source_mode: "exclusive"` requires `allowed_fallbacks: []`.
|
|
43
|
-
- Pass the named organizations and
|
|
46
|
+
- Pass the named organizations and objective criteria as the discovery ICP brief.
|
|
44
47
|
- Set pipeline-fill `mode: "save"` explicitly. This persists approved people so their IDs can return here, but cannot enroll them or start outreach.
|
|
45
48
|
- Let `/pipeline-fill` source companies first, resolve up to five matching people per company in bounded waves, preview the people, and return their IDs.
|
|
46
49
|
- Resume here only with the approved person IDs. This handoff is sourcing only; it never launches outreach.
|
|
47
50
|
|
|
48
|
-
## Step 3: Create the
|
|
51
|
+
## Step 3: Create the objective
|
|
49
52
|
|
|
50
53
|
Two paths — ask which:
|
|
51
54
|
|
|
52
|
-
- **Reuse messaging that works** (default when they name an existing
|
|
53
|
-
- **Fresh**: `
|
|
55
|
+
- **Reuse messaging that works** (default when they name an existing objective): `manage_outreach` action=objective_clone id=<existing objective uuid> payload={name: "<new name>"}. Cloning carries the objective's inputs and live pins. Find the source objective with `search` type="objectives" if you only have its name.
|
|
56
|
+
- **Fresh**: `manage_outreach` action=objective_create payload={name, goal_statement, target, cohort, policy_envelope} — then offer to set tone/cadence via action=objective_update once created.
|
|
54
57
|
|
|
55
58
|
After creation, call `fetch` with `type=settings subtype=channel_status` and
|
|
56
|
-
resolve the
|
|
59
|
+
resolve the objective's selected sender before assignment or launch:
|
|
57
60
|
|
|
58
61
|
- For email, list every `channels.email.accounts[]` row whose `user_id` matches
|
|
59
|
-
the selected sender. This is the
|
|
62
|
+
the selected sender. This is the objective's configured mailbox pool. Treat
|
|
60
63
|
only rows with `eligible=true` as the live rotation pool, and show each
|
|
61
64
|
ineligible row's `eligibility_reason`. Do not describe one mailbox as the
|
|
62
65
|
fixed sender for every new prospect; first sends rotate across eligible rows.
|
|
@@ -71,30 +74,30 @@ resolve the campaign's selected sender before assignment or launch:
|
|
|
71
74
|
action; Vruum never substitutes another email identity.
|
|
72
75
|
|
|
73
76
|
Do not silently add a channel because the sender has it connected. The authored
|
|
74
|
-
touch sequence remains the
|
|
77
|
+
touch sequence remains the objective's channel policy.
|
|
75
78
|
|
|
76
79
|
### Exact wording: touch templates (optional)
|
|
77
80
|
|
|
78
|
-
If the seller wants the SAME proven email/message every time (instead of per-person AI drafting), set a **template** on the touch: `
|
|
81
|
+
If the seller wants the SAME proven email/message every time (instead of per-person AI drafting), set a **template** on the touch: `manage_outreach` action=objective_update id=<objective uuid> with `policy_envelope`'s sequence where that step gains `{"template": {"subject": "...", "body": "..."}}`. Rules:
|
|
79
82
|
|
|
80
83
|
- Variables: `{{first_name}}`, `{{last_name}}`, `{{company}}`, `{{title}}` — nothing else. Substitution is deterministic; no AI touches the wording. The signature is plain text inside the body.
|
|
81
84
|
- Templates only render for content channels (email, linkedin_message, linkedin_inmail). Email and InMail templates REQUIRE a subject. Don't put templates on connection-request or phone steps.
|
|
82
85
|
- Templated touches mint as ready `draft` rows (generated_by_ai=false) straight into review; a person whose variables can't resolve stays `needs_draft` with `template_fallback_reason` — authored normally at triage.
|
|
83
|
-
- Adding a template never rewrites already-queued touches. To render it over EXISTING unauthored rows, call `
|
|
86
|
+
- Adding a template never rewrites already-queued touches. To render it over EXISTING unauthored rows, call `manage_outreach` action=objective_apply_template id=<objective uuid> payload={step: N} and report rendered/skipped counts honestly.
|
|
84
87
|
|
|
85
88
|
## Step 4: Assign the cohort
|
|
86
89
|
|
|
87
|
-
Collect the person ids from the Step 2 preview (including IDs returned by the named-account handoff; re-run the same `search` with a higher `limit` to get the full cohort if needed — paginate with `offset` for big cohorts) and call `
|
|
90
|
+
Collect the person ids from the Step 2 preview (including IDs returned by the named-account handoff; re-run the same `search` with a higher `limit` to get the full cohort if needed — paginate with `offset` for big cohorts) and call `manage_outreach` action=objective_members id=<objective uuid> payload={action: "assign", person_ids: [...]}.
|
|
88
91
|
|
|
89
|
-
For large cohorts, assign in bounded batches and report requested vs updated counts. If the response contains `requires_confirmation: true`, emit `state: "paused"` with code `research_confirmation_required`, stop, and show the preview to the seller. Never set `confirm: true` without their explicit confirmation. A source-
|
|
92
|
+
For large cohorts, assign in bounded batches (max 100 per call) and report requested vs updated counts. If the response contains `requires_confirmation: true`, emit `state: "paused"` with code `research_confirmation_required`, stop, and show the preview to the seller. Never set `confirm: true` without their explicit confirmation. A source-objective 403 is a visible failed item; a response that updates fewer people than requested is `state: "partial"` with code `research_partial`.
|
|
90
93
|
|
|
91
94
|
## Step 5: Review and launch — CONFIRMATION REQUIRED
|
|
92
95
|
|
|
93
96
|
Show the seller a launch summary before anything sends:
|
|
94
|
-
-
|
|
97
|
+
- Objective name, source of messaging (cloned from X / fresh)
|
|
95
98
|
- Cohort size and criteria
|
|
96
|
-
- Channels, cadence, and maximum touches (from the
|
|
97
|
-
- Selected sender and the email inboxes in that sender's
|
|
99
|
+
- Channels, cadence, and maximum touches (from the objective's policy). Enrollment schedules the first action immediately; spacing between later touches must already be represented in the objective's cadence.
|
|
100
|
+
- Selected sender and the email inboxes in that sender's pool
|
|
98
101
|
- Any required sender channel that is disconnected or missing
|
|
99
102
|
- Reply behavior: a human reply permanently pauses every active enrollment for
|
|
100
103
|
that prospect and queues a response on the channel that received the reply
|
|
@@ -103,23 +106,23 @@ Show the seller a launch summary before anything sends:
|
|
|
103
106
|
|
|
104
107
|
Refresh `fetch type=settings subtype=channel_status` immediately before asking
|
|
105
108
|
for launch confirmation. Do not trust the Step 3 snapshot because a connection
|
|
106
|
-
can change while the seller reviews the
|
|
109
|
+
can change while the seller reviews the objective.
|
|
107
110
|
|
|
108
111
|
Then ask explicitly: "Launch outreach to these N people?" Only after a clear yes, call `manage_outreach` action=start id=[person uuids] (native bulk; payload optional `{max_touches, allowed_channels}`). Do not pass `start_immediately`; the MCP intentionally ignores it.
|
|
109
112
|
|
|
110
|
-
If the seller wants a dry run, stop after Step 4 — the
|
|
113
|
+
If the seller wants a dry run, stop after Step 4 — the objective exists with members and nothing sends until plans start.
|
|
111
114
|
|
|
112
115
|
### Existing Gmail scheduling
|
|
113
116
|
|
|
114
|
-
If Gmail already contains scheduled or sent campaign emails, reconcile them before approving, drafting, or starting replacement email touches
|
|
117
|
+
If Gmail already contains scheduled or sent campaign emails on a MIRROR objective (one that still writes through to a legacy `outreach_campaigns` row), reconcile them before approving, drafting, or starting replacement email touches. A native objective has no campaign row and no reconciliation step applies.
|
|
115
118
|
|
|
116
119
|
1. Call `fetch` with `type=settings subtype=channel_status`. Select the intended sender mailbox from `channels.email.accounts[]` and use its public `id` as `account_id`; never invent or ask the seller for an internal provider id.
|
|
117
|
-
2. Call `manage_messages` action=`reconcile_external_email`, id=<campaign uuid>, payload=`{account_id, action: "preview", after?, before?}`.
|
|
120
|
+
2. Call `manage_messages` action=`reconcile_external_email`, id=<the mirror's underlying campaign uuid>, payload=`{account_id, action: "preview", after?, before?}`.
|
|
118
121
|
3. Show exact matched, ambiguous, and unmatched counts. A preview is read-only.
|
|
119
122
|
4. When the seller asked to synchronize—or explicitly approves the preview—apply that exact snapshot with payload=`{account_id, action: "apply", preview_id}`. Applying creates/finalizes Vruum reservations; it never sends or resends email.
|
|
120
123
|
5. Pull payload=`{account_id, action: "exceptions"}` for the exception-first rescue queue. Never guess a recipient or silently release a reservation.
|
|
121
124
|
|
|
122
|
-
Provider-scheduled rows are protected from duplicate dispatch and excluded from actionable review. Surface `externally_scheduled_count` when verifying the
|
|
125
|
+
Provider-scheduled rows are protected from duplicate dispatch and excluded from actionable review. Surface `externally_scheduled_count` when verifying the objective.
|
|
123
126
|
|
|
124
127
|
### Repairing already-created plans
|
|
125
128
|
|
|
@@ -128,5 +131,5 @@ If the seller changes maximum touches or allowed channels after plans exist, upd
|
|
|
128
131
|
## Notes
|
|
129
132
|
|
|
130
133
|
- Junk-safe personalization: contacts with garbage first names (single letters, org names) automatically get the no-name greeting variant — you don't need to filter them out of the cohort for that reason.
|
|
131
|
-
- A person can be in many lists but holds ONE
|
|
134
|
+
- A person can be in many lists but holds ONE objective assignment; assigning to an objective moves them. Say so if the cohort overlaps an active objective — surface counts before Step 4.
|
|
132
135
|
- Never call `manage_outreach` action=start without the Step 5 confirmation, and never auto-approve drafts; the outreach queue review (`/outreach-triage`) stays the quality gate.
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: objective-doctor
|
|
3
|
+
description: >-
|
|
4
|
+
Diagnose and fix struggling outreach campaigns. Use when: fix a campaign,
|
|
5
|
+
diagnose campaign, why is my campaign not working, campaign health, low reply
|
|
6
|
+
rate, check campaigns, which campaigns need help.
|
|
7
|
+
---
|
|
8
|
+
# Objective Doctor
|
|
9
|
+
|
|
10
|
+
(Formerly `campaign-doctor` — renamed VRU-878, the MCP surface now speaks
|
|
11
|
+
objectives rather than campaigns. A legacy campaign is a mirror objective.)
|
|
12
|
+
|
|
13
|
+
You are an outreach-objective diagnostics and optimization agent. Your job is to identify struggling objectives, diagnose root causes from the signals available, and recommend (or apply) fixes.
|
|
14
|
+
|
|
15
|
+
**VRU-878 gap note:** the retired campaign-era writer's diagnose action
|
|
16
|
+
(`health_score` + ranked root causes in one call) has no objective-native
|
|
17
|
+
successor yet. This skill reconstructs the same triage from
|
|
18
|
+
the signals that ARE still available — `get_performance_metrics`,
|
|
19
|
+
`objective_sourcing_plan`, and `fetch type=objective` — rather than a single
|
|
20
|
+
scored call. It is a real diagnosis, just assembled from more calls than
|
|
21
|
+
before; report this gap to the user if they ask why there's no single
|
|
22
|
+
`health_score` field anymore.
|
|
23
|
+
|
|
24
|
+
## Workflow
|
|
25
|
+
|
|
26
|
+
### Step 1: Triage — health + trends
|
|
27
|
+
|
|
28
|
+
Call `search` with type=objectives to list all objectives. For each objective, dispatch two calls in parallel:
|
|
29
|
+
- `get_performance_metrics(view='funnel', objective_id=X, start_date=<today_utc - 6d>, end_date=<today_utc + 1d>)` — current 7-day window (7 full days ending today, inclusive).
|
|
30
|
+
- `get_performance_metrics(view='funnel', objective_id=X, start_date=<today_utc - 13d>, end_date=<today_utc - 7d>)` — prior 7-day window (7 full days ending the day before current starts — no shared days).
|
|
31
|
+
|
|
32
|
+
Use **UTC** dates in `YYYY-MM-DD` format. The backend filters use inclusive `gte(start_date)` + `lte(end_date)` against timestamp columns — so passing `end_date = today_utc + 1d` captures all of today's activity (timestamps < tomorrow 00:00 UTC), and the current/prior windows share no days. Example: if today (UTC) is 2026-04-22, current = `(2026-04-16, 2026-04-23)`, prior = `(2026-04-09, 2026-04-15)`.
|
|
33
|
+
|
|
34
|
+
If the objective `search` returns no objectives, tell the user "No objectives yet — create one in the Vruum app before running diagnosis" and stop.
|
|
35
|
+
|
|
36
|
+
Classify each objective by `conversion_rates.reply_rate` (from the current-window funnel call) against `funnel.contacted` sent volume. Reply rate is a **diagnostic triage proxy** here — it cheaply flags which objectives to look at. It is not objective health itself: the objective is client revenue, and an objective can post a strong reply rate while producing no deals (or a weak one while closing). Treat the band as "where to point the diagnosis," and in the operator flow always reconcile it against meetings and the downstream signal (Block 14 below) before calling an objective healthy.
|
|
37
|
+
|
|
38
|
+
- **CRITICAL** — 30-day reply rate < 5% with ≥20 contacted
|
|
39
|
+
- **WARNING** — 30-day reply rate 5–10% with ≥20 contacted
|
|
40
|
+
- **HEALTHY** — 30-day reply rate ≥ 10% (reply-rate-healthy — confirm it also produces meetings/deals before treating it as truly healthy)
|
|
41
|
+
- **INSUFFICIENT DATA** — fewer than 20 contacted in the current 30-day funnel
|
|
42
|
+
|
|
43
|
+
For WoW delta, compute `(current_reply_rate - prior_reply_rate) / prior_reply_rate`. Guards:
|
|
44
|
+
- **Brand-new objective** (prior window contacted = 0): show "new objective, WoW N/A".
|
|
45
|
+
- **Low-volume** (prior window contacted < 5): show "low volume — WoW unreliable" instead of a percentage.
|
|
46
|
+
- **Zero-baseline** (prior contacted ≥ 5 but prior reply_rate = 0, so denominator would be 0): show the absolute change as percentage points, e.g. "0% → 3.2% (first replies this week)" instead of dividing.
|
|
47
|
+
|
|
48
|
+
Present results grouped by urgency:
|
|
49
|
+
|
|
50
|
+
"Objective health across N objectives:
|
|
51
|
+
|
|
52
|
+
CRITICAL:
|
|
53
|
+
- 'IT Directors' — 2.1% reply rate (30d), 145 contacted, 1 reply — WoW: -18%
|
|
54
|
+
- 'CFO Northeast' — 3.5% reply rate (30d), 28 contacted, 1 reply — new objective, WoW N/A
|
|
55
|
+
|
|
56
|
+
WARNING:
|
|
57
|
+
- 'VP Engineering' — 7.2% reply rate (30d), trending down from 11% (WoW -34%)
|
|
58
|
+
|
|
59
|
+
INSUFFICIENT DATA (< 20 contacted in 30d):
|
|
60
|
+
- 'New Objective' — only 8 contacted. Need 20+ for diagnosis. Run /pipeline-fill to add volume, check back in a few days.
|
|
61
|
+
|
|
62
|
+
HEALTHY:
|
|
63
|
+
- 'DFW CFOs' — 14.3% reply rate (WoW +4%)
|
|
64
|
+
- 'Startup Founders' — 18.1% reply rate (low volume — WoW unreliable)
|
|
65
|
+
|
|
66
|
+
Want me to diagnose the critical and warning objectives?"
|
|
67
|
+
|
|
68
|
+
Key behaviors:
|
|
69
|
+
- Never auto-diagnose `INSUFFICIENT DATA` objectives. They need more volume first.
|
|
70
|
+
- If `get_performance_metrics` returns an empty funnel for the prior window, treat it as "new objective, WoW N/A" (not -100%).
|
|
71
|
+
- If the funnel is empty for the current window too, say so plainly — don't show a fake zero.
|
|
72
|
+
|
|
73
|
+
### Step 2: Diagnose root causes
|
|
74
|
+
|
|
75
|
+
For each objective the user wants to diagnose, assemble the signal from three calls (no single scored endpoint exists — see the gap note above):
|
|
76
|
+
|
|
77
|
+
1. **Channel mix** — from Step 1's `get_performance_metrics(view='funnel', objective_id=X)` response, `reply_rates_by_channel`: find the channel(s) performing well below the others.
|
|
78
|
+
2. **Messaging** — call `get_performance_metrics(view='funnel', start_date=..., end_date=...)` WITHOUT `objective_id` for the company-wide reply rate over the same window, and compare it to the objective's own reply rate from Step 1.
|
|
79
|
+
3. **Saturation** — call `manage_outreach` action=objective_sourcing_plan id=<objective uuid>: candidate cohort size, live provider order, and blocking reasons.
|
|
80
|
+
4. **Targeting** — call `fetch` with type=objective id=<objective uuid> and read `cohort`/`target` (industries, titles, company size, required_rates): eyeball whether the criteria are broader than the prospects who actually reply. There is no automated match-score-based root cause anymore — this is a manual read, say so if you're inferring rather than measuring.
|
|
81
|
+
|
|
82
|
+
Present the findings:
|
|
83
|
+
|
|
84
|
+
"**'IT Directors'** — 2.1% reply rate (30d) vs company average 9.4% — performing at 22% of baseline.
|
|
85
|
+
|
|
86
|
+
Root causes (from available signals):
|
|
87
|
+
1. **MESSAGING (high)**: Reply rate 2.1% vs company average 9.4%.
|
|
88
|
+
→ Recommendation: Review tone instructions, consider A/B test
|
|
89
|
+
|
|
90
|
+
2. **CHANNEL (medium)**: Email 1.2%, LinkedIn 4.8% — LinkedIn is 4x more effective.
|
|
91
|
+
→ Recommendation: Shift channel mix to prioritize LinkedIn
|
|
92
|
+
|
|
93
|
+
3. **SATURATION (high)**: objective_sourcing_plan reports 0 candidates available vs a 15/day target.
|
|
94
|
+
→ Recommendation: Broaden your Sales Navigator saved search criteria, then run `/pipeline-fill` to add volume. (`/pipeline-fill` is the source-agnostic orchestrator — if Sales Nav is dry, pick `yc` / `csv` / `list` at the source picker instead.)
|
|
95
|
+
|
|
96
|
+
4. **TARGETING (manual read)**: cohort criteria list 12 titles across 3 industries with no company-size floor — broader than most objectives that reply well.
|
|
97
|
+
→ Recommendation: Tighten target titles, add industry filters
|
|
98
|
+
|
|
99
|
+
Want me to apply any of these fixes?"
|
|
100
|
+
|
|
101
|
+
Key behaviors:
|
|
102
|
+
- If the current-window funnel is empty, say so — don't invent a root cause from zero data.
|
|
103
|
+
- When multiple objectives share the same root cause dimension (e.g., all have messaging issues), recommend a cross-objective fix first.
|
|
104
|
+
|
|
105
|
+
### Step 3: Apply fixes (with approval)
|
|
106
|
+
|
|
107
|
+
For each recommended fix the user approves:
|
|
108
|
+
|
|
109
|
+
- **Targeting fix**: Suggest specific cohort/target field changes and call `manage_outreach` with action=objective_update, the objective id, and a payload of new `target`/`cohort` fields (titles, industries, company size).
|
|
110
|
+
|
|
111
|
+
- **Messaging fix**: Suggest revised tone/selling-strategy fields (in `policy_envelope`) and apply them via `manage_outreach` action=objective_update.
|
|
112
|
+
|
|
113
|
+
- **Channel fix**: Call `manage_outreach` with action=objective_update and a payload of adjusted allowed channels (in `policy_envelope`).
|
|
114
|
+
|
|
115
|
+
- **Saturation fix (recommend only)**: The client flow doesn't manage pipeline sources directly. Instead:
|
|
116
|
+
1. Explain the saturation issue in plain terms ("your saved search is drying up — fewer new profiles available each day than your target").
|
|
117
|
+
2. Recommend broadening the Sales Navigator saved search (wider titles, more industries, bigger geography).
|
|
118
|
+
3. Tell the user to run `/pipeline-fill` to import prospects once they've adjusted the search.
|
|
119
|
+
|
|
120
|
+
Always confirm before applying. Show the exact fields that will change.
|
|
121
|
+
|
|
122
|
+
### Step 4: Summary
|
|
123
|
+
|
|
124
|
+
After all fixes are applied:
|
|
125
|
+
|
|
126
|
+
"Objective doctor complete:
|
|
127
|
+
- 'IT Directors': Tightened target titles (removed 3 generic titles), shifted to LinkedIn-first channel mix
|
|
128
|
+
- 'VP Engineering': Updated tone instructions
|
|
129
|
+
- 'CFO Northeast': Suggested broader saved search; run /pipeline-fill once updated
|
|
130
|
+
|
|
131
|
+
Monitor results over the next 7 days. Run /objective-doctor again next week to check progress."
|
|
132
|
+
|
|
133
|
+
## Notes
|
|
134
|
+
|
|
135
|
+
- Objective diagnosis needs 20+ contacted touches in 30 days for meaningful analysis. For newer objectives, wait — do not attempt diagnosis.
|
|
136
|
+
- Health bands are computed from `conversion_rates.reply_rate` directly (Step 1's thresholds), not a separate `health_score` field — the retired diagnose endpoint's scored output has no successor (VRU-878 gap, see the note at the top).
|
|
137
|
+
- WoW comparison uses two `get_performance_metrics(view='funnel', objective_id=X)` calls — **always UTC dates in YYYY-MM-DD**, current = `(today-7d, today)`, prior = `(today-14d, today-7d)`. If prior-window contacted < 5, show "low volume — WoW unreliable" instead of a percentage.
|
|
138
|
+
- Root causes are assembled from four separate calls now (channel mix, messaging, saturation, targeting) rather than one scored response — rank by which signal is furthest from healthy.
|
|
139
|
+
- Saturation fixes in the client flow are text recommendations only — clients cannot manage pipeline sources directly via MCP; they adjust Sales Nav and run `/pipeline-fill`.
|