@sellable/mcp 0.1.951-wip.sendercontext.20261001.1 → 0.1.953
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/dist/tools/bootstrap.d.ts +1 -1
- package/dist/tools/bootstrap.js +2 -2
- package/dist/tools/campaigns.d.ts +22 -0
- package/dist/tools/campaigns.js +18 -2
- package/dist/tools/navigation.d.ts +3 -0
- package/dist/tools/navigation.js +4 -0
- package/dist/tools/registry.d.ts +20 -0
- package/package.json +1 -1
- package/skills/create-campaign/references/ai-native-tokens.md +7 -0
- package/skills/create-campaign-v2/SKILL.md +7 -0
- package/skills/create-campaign-v2/core/policy.md +4 -1
- package/skills/create-campaign-v2/references/filter-leads.md +3 -1
- package/skills/create-campaign-v2/references/message-approach.md +70 -0
- package/skills/create-campaign-v2/references/sample-validation-loop.md +2 -1
- package/skills/create-campaign-v2/references/watch-guide-narration.md +2 -1
- package/skills/create-campaign-v2-tail/SKILL.md +11 -5
- package/skills/first-campaign-onboarding/SKILL.md +242 -13
- package/skills/research-sender/SKILL.md +28 -8
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { type AuthStatus } from "./auth.js";
|
|
2
2
|
import { getCampaignContext } from "./context.js";
|
|
3
3
|
import { getCampaignFramework } from "./framework.js";
|
|
4
|
-
import { type ListSubskillPromptsResponse, type SubskillPromptResponse } from "./prompts.js";
|
|
5
4
|
import { type CampaignModelQualityResult } from "./model-quality.js";
|
|
5
|
+
import { type ListSubskillPromptsResponse, type SubskillPromptResponse } from "./prompts.js";
|
|
6
6
|
type BootstrapCreateCampaignInput = {
|
|
7
7
|
campaignId?: string;
|
|
8
8
|
flowVersion?: "v1" | "v2";
|
package/dist/tools/bootstrap.js
CHANGED
|
@@ -2,8 +2,8 @@ import { resetApi } from "../api.js";
|
|
|
2
2
|
import { getAuthStatus } from "./auth.js";
|
|
3
3
|
import { getCampaignContext } from "./context.js";
|
|
4
4
|
import { getCampaignFramework } from "./framework.js";
|
|
5
|
-
import { getSubskillPrompt, listSubskillPrompts, } from "./prompts.js";
|
|
6
5
|
import { evaluateCampaignModelQuality, getCampaignModelMinimumSummary, } from "./model-quality.js";
|
|
6
|
+
import { getSubskillPrompt, listSubskillPrompts, } from "./prompts.js";
|
|
7
7
|
function toErrorMessage(error) {
|
|
8
8
|
return error instanceof Error ? error.message : String(error);
|
|
9
9
|
}
|
|
@@ -307,7 +307,7 @@ export async function bootstrapCreateCampaign(input = {}) {
|
|
|
307
307
|
? resumeDetected
|
|
308
308
|
? `Bootstrap complete.${workspaceNotice}${modelNotice} Resume from campaign state and navigation diagnostics first; treat local draft artifacts as debug-only evidence. Then load ${createCampaignSubskill?.name ?? "create-campaign"} instructions with get_subskill_prompt({ subskillName: "${createCampaignSubskill?.name ?? "create-campaign"}" }); if the response has hasMore=true, continue with nextOffset until hasMore=false.`
|
|
309
309
|
: flowVersion === "v2"
|
|
310
|
-
? `Bootstrap complete.${workspaceNotice}${modelNotice} Load the compact create-campaign-v2 entry prompt once with get_subskill_prompt({ subskillName: "create-campaign-v2" }); Hermes users should start this flow with /sellable-create-campaign. Load flow/reference assets lazily only when that stage needs them. Preserve the pre-intake sequence: confirm auth/workspace status, ask only for the LinkedIn profile URL or handle, normalize handles to a full profile URL, require that profile identity before continuing, run lightweight profile/company lookup, then ask the target, offer, credibility, and prospect-source setup questions. Do not call list_senders or sender discovery during setup; sender availability belongs only to Settings after message approval. Then write the campaign brief, call create_campaign once to mint the watchable shell, surface the returned watch link once before brief approval, and hand off to lead finding without repeating the link.`
|
|
310
|
+
? `Bootstrap complete.${workspaceNotice}${modelNotice} Load the compact create-campaign-v2 entry prompt once with get_subskill_prompt({ subskillName: "create-campaign-v2" }); Hermes users should start this flow with /sellable-create-campaign. Load flow/reference assets lazily only when that stage needs them. Preserve the pre-intake sequence: confirm auth/workspace status, ask only for the LinkedIn profile URL or handle, normalize handles to a full profile URL, require that profile identity before continuing, run lightweight profile/company lookup, then ask the target, offer, credibility, and prospect-source setup questions. Do not call list_senders or sender discovery during setup; sender availability belongs only to Settings after message approval. Then write the campaign brief, call create_campaign once to mint the watchable shell, surface the returned watch link once before brief approval, and hand off to lead finding without repeating the link. Exception: when you entered from the first-campaign-onboarding skill, its first-campaign rules replace the workspace notice, the slash-command note, the setup questions, and the watch-link handoff described here.`
|
|
311
311
|
: `Bootstrap complete.${workspaceNotice}${modelNotice} Load ${createCampaignSubskill?.name ?? "create-campaign"} instructions with get_subskill_prompt({ subskillName: "${createCampaignSubskill?.name ?? "create-campaign"}" }); if the response has hasMore=true, continue with nextOffset until hasMore=false. Follow that flow before calling create_campaign.`
|
|
312
312
|
: "Bootstrap incomplete. Resolve blockingErrors and rerun bootstrap_create_campaign before provider/search/import tools.";
|
|
313
313
|
// Strip prompt body from createCampaignSubskill — it's loaded via the host
|
|
@@ -141,6 +141,7 @@ export interface CreateCampaignInput {
|
|
|
141
141
|
offerPositioning?: unknown;
|
|
142
142
|
campaignBrief?: string;
|
|
143
143
|
messageGenerationMode?: "template" | "ai-generated";
|
|
144
|
+
messageApproach?: "conversation" | "message_test";
|
|
144
145
|
currentStep?: string | null;
|
|
145
146
|
watchNarration?: unknown;
|
|
146
147
|
leadSourceType?: string | null;
|
|
@@ -167,6 +168,7 @@ export interface UpdateCampaignInput {
|
|
|
167
168
|
interactionMode?: InteractionMode;
|
|
168
169
|
enableICPFilters?: boolean | null;
|
|
169
170
|
useMessagingTemplate?: boolean | null;
|
|
171
|
+
messageApproach?: "conversation" | "message_test";
|
|
170
172
|
autoApproveFollowups?: boolean;
|
|
171
173
|
enableFollowUps?: boolean;
|
|
172
174
|
senderIds?: string[];
|
|
@@ -251,6 +253,7 @@ export declare const campaignToolDefinitions: ({
|
|
|
251
253
|
offerPositioning?: undefined;
|
|
252
254
|
campaignBrief?: undefined;
|
|
253
255
|
messageGenerationMode?: undefined;
|
|
256
|
+
messageApproach?: undefined;
|
|
254
257
|
currentStep?: undefined;
|
|
255
258
|
watchNarration?: undefined;
|
|
256
259
|
leadSourceType?: undefined;
|
|
@@ -300,6 +303,7 @@ export declare const campaignToolDefinitions: ({
|
|
|
300
303
|
offerPositioning?: undefined;
|
|
301
304
|
campaignBrief?: undefined;
|
|
302
305
|
messageGenerationMode?: undefined;
|
|
306
|
+
messageApproach?: undefined;
|
|
303
307
|
currentStep?: undefined;
|
|
304
308
|
watchNarration?: undefined;
|
|
305
309
|
leadSourceType?: undefined;
|
|
@@ -351,6 +355,7 @@ export declare const campaignToolDefinitions: ({
|
|
|
351
355
|
offerPositioning?: undefined;
|
|
352
356
|
campaignBrief?: undefined;
|
|
353
357
|
messageGenerationMode?: undefined;
|
|
358
|
+
messageApproach?: undefined;
|
|
354
359
|
currentStep?: undefined;
|
|
355
360
|
watchNarration?: undefined;
|
|
356
361
|
leadSourceType?: undefined;
|
|
@@ -398,6 +403,7 @@ export declare const campaignToolDefinitions: ({
|
|
|
398
403
|
offerPositioning?: undefined;
|
|
399
404
|
campaignBrief?: undefined;
|
|
400
405
|
messageGenerationMode?: undefined;
|
|
406
|
+
messageApproach?: undefined;
|
|
401
407
|
currentStep?: undefined;
|
|
402
408
|
watchNarration?: undefined;
|
|
403
409
|
leadSourceType?: undefined;
|
|
@@ -445,6 +451,7 @@ export declare const campaignToolDefinitions: ({
|
|
|
445
451
|
offerPositioning?: undefined;
|
|
446
452
|
campaignBrief?: undefined;
|
|
447
453
|
messageGenerationMode?: undefined;
|
|
454
|
+
messageApproach?: undefined;
|
|
448
455
|
currentStep?: undefined;
|
|
449
456
|
watchNarration?: undefined;
|
|
450
457
|
leadSourceType?: undefined;
|
|
@@ -535,6 +542,7 @@ export declare const campaignToolDefinitions: ({
|
|
|
535
542
|
offerPositioning?: undefined;
|
|
536
543
|
campaignBrief?: undefined;
|
|
537
544
|
messageGenerationMode?: undefined;
|
|
545
|
+
messageApproach?: undefined;
|
|
538
546
|
currentStep?: undefined;
|
|
539
547
|
watchNarration?: undefined;
|
|
540
548
|
leadSourceType?: undefined;
|
|
@@ -593,6 +601,11 @@ export declare const campaignToolDefinitions: ({
|
|
|
593
601
|
enum: string[];
|
|
594
602
|
description: string;
|
|
595
603
|
};
|
|
604
|
+
messageApproach: {
|
|
605
|
+
type: string;
|
|
606
|
+
enum: string[];
|
|
607
|
+
description: string;
|
|
608
|
+
};
|
|
596
609
|
currentStep: {
|
|
597
610
|
type: string[];
|
|
598
611
|
description: string;
|
|
@@ -619,6 +632,7 @@ export declare const campaignToolDefinitions: ({
|
|
|
619
632
|
};
|
|
620
633
|
safety: {
|
|
621
634
|
type: string[];
|
|
635
|
+
description: string;
|
|
622
636
|
};
|
|
623
637
|
progressLabel: {
|
|
624
638
|
type: string[];
|
|
@@ -859,6 +873,7 @@ export declare const campaignToolDefinitions: ({
|
|
|
859
873
|
};
|
|
860
874
|
safety: {
|
|
861
875
|
type: string[];
|
|
876
|
+
description: string;
|
|
862
877
|
};
|
|
863
878
|
progressLabel: {
|
|
864
879
|
type: string[];
|
|
@@ -969,6 +984,11 @@ export declare const campaignToolDefinitions: ({
|
|
|
969
984
|
type: string[];
|
|
970
985
|
description: string;
|
|
971
986
|
};
|
|
987
|
+
messageApproach: {
|
|
988
|
+
type: string;
|
|
989
|
+
enum: string[];
|
|
990
|
+
description: string;
|
|
991
|
+
};
|
|
972
992
|
autoApproveFollowups: {
|
|
973
993
|
type: string;
|
|
974
994
|
description: string;
|
|
@@ -1036,6 +1056,7 @@ export declare const campaignToolDefinitions: ({
|
|
|
1036
1056
|
offerPositioning?: undefined;
|
|
1037
1057
|
campaignBrief?: undefined;
|
|
1038
1058
|
messageGenerationMode?: undefined;
|
|
1059
|
+
messageApproach?: undefined;
|
|
1039
1060
|
currentStep?: undefined;
|
|
1040
1061
|
watchNarration?: undefined;
|
|
1041
1062
|
leadSourceType?: undefined;
|
|
@@ -1086,6 +1107,7 @@ export declare const campaignToolDefinitions: ({
|
|
|
1086
1107
|
senderLinkedinUrl?: undefined;
|
|
1087
1108
|
offerPositioning?: undefined;
|
|
1088
1109
|
messageGenerationMode?: undefined;
|
|
1110
|
+
messageApproach?: undefined;
|
|
1089
1111
|
currentStep?: undefined;
|
|
1090
1112
|
watchNarration?: undefined;
|
|
1091
1113
|
leadSourceType?: undefined;
|
package/dist/tools/campaigns.js
CHANGED
|
@@ -29,7 +29,10 @@ const WATCH_NARRATION_TOOL_SCHEMA = {
|
|
|
29
29
|
visibleState: { type: "string" },
|
|
30
30
|
agentIntent: { type: "string" },
|
|
31
31
|
nextAction: { type: ["string", "null"] },
|
|
32
|
-
safety: {
|
|
32
|
+
safety: {
|
|
33
|
+
type: ["string", "null"],
|
|
34
|
+
description: 'Required whenever currentStep changes: one short line on what will not happen yet (for example "Rules only. No one is contacted."). Optional otherwise.',
|
|
35
|
+
},
|
|
33
36
|
progressLabel: { type: ["string", "null"] },
|
|
34
37
|
blockedReason: { type: ["string", "null"] },
|
|
35
38
|
workerStatuses: {
|
|
@@ -444,6 +447,11 @@ export const campaignToolDefinitions = [
|
|
|
444
447
|
enum: ["template", "ai-generated"],
|
|
445
448
|
description: "Defaults to template for autonomous messaging steps. Use ai-generated only when the user explicitly approves freeform AI-generated messages instead of an approved {{token}} template.",
|
|
446
449
|
},
|
|
450
|
+
messageApproach: {
|
|
451
|
+
type: "string",
|
|
452
|
+
enum: ["conversation", "message_test"],
|
|
453
|
+
description: 'How first messages are written. "conversation": each first message is written for that person by the backend writer and opens a conversation, with no pitch; the campaign must have NO approved message template and no {{token}} copy in the brief. "message_test": one approved message template goes to everyone, for testing copy. Omit to leave the campaign as it is.',
|
|
454
|
+
},
|
|
447
455
|
currentStep: {
|
|
448
456
|
type: ["string", "null"],
|
|
449
457
|
description: "Workflow step ID (headless or UI step ID such as filter-rules)",
|
|
@@ -566,6 +574,11 @@ export const campaignToolDefinitions = [
|
|
|
566
574
|
type: ["boolean", "null"],
|
|
567
575
|
description: "Whether to use the messaging template step",
|
|
568
576
|
},
|
|
577
|
+
messageApproach: {
|
|
578
|
+
type: "string",
|
|
579
|
+
enum: ["conversation", "message_test"],
|
|
580
|
+
description: 'How first messages are written. "conversation": each first message is written for that person by the backend writer and opens a conversation, with no pitch; the campaign must have NO approved message template and no {{token}} copy in the brief. "message_test": one approved message template goes to everyone, for testing copy. Omit to leave the campaign as it is.',
|
|
581
|
+
},
|
|
569
582
|
autoApproveFollowups: {
|
|
570
583
|
type: "boolean",
|
|
571
584
|
description: "Whether drafted campaign follow-ups are automatically approved/sent. This does not draft, approve, or send an individual prospect message during the settings update.",
|
|
@@ -1043,7 +1056,10 @@ export async function createCampaign(input) {
|
|
|
1043
1056
|
const isAutonomousMessageMint = currentStep === "auto-execute-leads" ||
|
|
1044
1057
|
currentStep === "validate-sample" ||
|
|
1045
1058
|
currentStep === "auto-execute-messaging";
|
|
1046
|
-
|
|
1059
|
+
// A conversation campaign has no template by design: each first message is
|
|
1060
|
+
// written for the person by the backend writer.
|
|
1061
|
+
const explicitlyAllowsAiGenerated = input.messageGenerationMode === "ai-generated" ||
|
|
1062
|
+
input.messageApproach === "conversation";
|
|
1047
1063
|
const hasTemplateToken = /\{\{\s*\S[^}]*\}\}/.test(briefContent);
|
|
1048
1064
|
if (isAutonomousMessageMint &&
|
|
1049
1065
|
!explicitlyAllowsAiGenerated &&
|
|
@@ -12,6 +12,9 @@ export type CampaignOfferNavigation = {
|
|
|
12
12
|
useMessagingTemplate?: boolean | null;
|
|
13
13
|
messagingTemplateId?: string | null;
|
|
14
14
|
messagingTemplate?: unknown | null;
|
|
15
|
+
messageGenerator?: {
|
|
16
|
+
mode?: string | null;
|
|
17
|
+
} | null;
|
|
15
18
|
leadSourceType?: string | null;
|
|
16
19
|
leadSourceProvider?: string | null;
|
|
17
20
|
selectedLeadListId?: string | null;
|
package/dist/tools/navigation.js
CHANGED
|
@@ -140,6 +140,10 @@ function hasActiveRubrics(campaign) {
|
|
|
140
140
|
function hasApprovedMessageTemplate(campaign) {
|
|
141
141
|
if (campaign.messagingTemplateId || campaign.messagingTemplate)
|
|
142
142
|
return true;
|
|
143
|
+
// A conversation campaign never has a template; its messages are written
|
|
144
|
+
// per person, so the message approach is already settled.
|
|
145
|
+
if (campaign.messageGenerator?.mode === "CONVERSATION")
|
|
146
|
+
return true;
|
|
143
147
|
const briefContent = getBriefContent(campaign.campaignBrief);
|
|
144
148
|
if (!briefContent.trim())
|
|
145
149
|
return false;
|
package/dist/tools/registry.d.ts
CHANGED
|
@@ -2334,6 +2334,7 @@ export declare const allTools: ({
|
|
|
2334
2334
|
offerPositioning?: undefined;
|
|
2335
2335
|
campaignBrief?: undefined;
|
|
2336
2336
|
messageGenerationMode?: undefined;
|
|
2337
|
+
messageApproach?: undefined;
|
|
2337
2338
|
currentStep?: undefined;
|
|
2338
2339
|
watchNarration?: undefined;
|
|
2339
2340
|
leadSourceType?: undefined;
|
|
@@ -2383,6 +2384,7 @@ export declare const allTools: ({
|
|
|
2383
2384
|
offerPositioning?: undefined;
|
|
2384
2385
|
campaignBrief?: undefined;
|
|
2385
2386
|
messageGenerationMode?: undefined;
|
|
2387
|
+
messageApproach?: undefined;
|
|
2386
2388
|
currentStep?: undefined;
|
|
2387
2389
|
watchNarration?: undefined;
|
|
2388
2390
|
leadSourceType?: undefined;
|
|
@@ -2434,6 +2436,7 @@ export declare const allTools: ({
|
|
|
2434
2436
|
offerPositioning?: undefined;
|
|
2435
2437
|
campaignBrief?: undefined;
|
|
2436
2438
|
messageGenerationMode?: undefined;
|
|
2439
|
+
messageApproach?: undefined;
|
|
2437
2440
|
currentStep?: undefined;
|
|
2438
2441
|
watchNarration?: undefined;
|
|
2439
2442
|
leadSourceType?: undefined;
|
|
@@ -2481,6 +2484,7 @@ export declare const allTools: ({
|
|
|
2481
2484
|
offerPositioning?: undefined;
|
|
2482
2485
|
campaignBrief?: undefined;
|
|
2483
2486
|
messageGenerationMode?: undefined;
|
|
2487
|
+
messageApproach?: undefined;
|
|
2484
2488
|
currentStep?: undefined;
|
|
2485
2489
|
watchNarration?: undefined;
|
|
2486
2490
|
leadSourceType?: undefined;
|
|
@@ -2528,6 +2532,7 @@ export declare const allTools: ({
|
|
|
2528
2532
|
offerPositioning?: undefined;
|
|
2529
2533
|
campaignBrief?: undefined;
|
|
2530
2534
|
messageGenerationMode?: undefined;
|
|
2535
|
+
messageApproach?: undefined;
|
|
2531
2536
|
currentStep?: undefined;
|
|
2532
2537
|
watchNarration?: undefined;
|
|
2533
2538
|
leadSourceType?: undefined;
|
|
@@ -2618,6 +2623,7 @@ export declare const allTools: ({
|
|
|
2618
2623
|
offerPositioning?: undefined;
|
|
2619
2624
|
campaignBrief?: undefined;
|
|
2620
2625
|
messageGenerationMode?: undefined;
|
|
2626
|
+
messageApproach?: undefined;
|
|
2621
2627
|
currentStep?: undefined;
|
|
2622
2628
|
watchNarration?: undefined;
|
|
2623
2629
|
leadSourceType?: undefined;
|
|
@@ -2676,6 +2682,11 @@ export declare const allTools: ({
|
|
|
2676
2682
|
enum: string[];
|
|
2677
2683
|
description: string;
|
|
2678
2684
|
};
|
|
2685
|
+
messageApproach: {
|
|
2686
|
+
type: string;
|
|
2687
|
+
enum: string[];
|
|
2688
|
+
description: string;
|
|
2689
|
+
};
|
|
2679
2690
|
currentStep: {
|
|
2680
2691
|
type: string[];
|
|
2681
2692
|
description: string;
|
|
@@ -2702,6 +2713,7 @@ export declare const allTools: ({
|
|
|
2702
2713
|
};
|
|
2703
2714
|
safety: {
|
|
2704
2715
|
type: string[];
|
|
2716
|
+
description: string;
|
|
2705
2717
|
};
|
|
2706
2718
|
progressLabel: {
|
|
2707
2719
|
type: string[];
|
|
@@ -2942,6 +2954,7 @@ export declare const allTools: ({
|
|
|
2942
2954
|
};
|
|
2943
2955
|
safety: {
|
|
2944
2956
|
type: string[];
|
|
2957
|
+
description: string;
|
|
2945
2958
|
};
|
|
2946
2959
|
progressLabel: {
|
|
2947
2960
|
type: string[];
|
|
@@ -3052,6 +3065,11 @@ export declare const allTools: ({
|
|
|
3052
3065
|
type: string[];
|
|
3053
3066
|
description: string;
|
|
3054
3067
|
};
|
|
3068
|
+
messageApproach: {
|
|
3069
|
+
type: string;
|
|
3070
|
+
enum: string[];
|
|
3071
|
+
description: string;
|
|
3072
|
+
};
|
|
3055
3073
|
autoApproveFollowups: {
|
|
3056
3074
|
type: string;
|
|
3057
3075
|
description: string;
|
|
@@ -3119,6 +3137,7 @@ export declare const allTools: ({
|
|
|
3119
3137
|
offerPositioning?: undefined;
|
|
3120
3138
|
campaignBrief?: undefined;
|
|
3121
3139
|
messageGenerationMode?: undefined;
|
|
3140
|
+
messageApproach?: undefined;
|
|
3122
3141
|
currentStep?: undefined;
|
|
3123
3142
|
watchNarration?: undefined;
|
|
3124
3143
|
leadSourceType?: undefined;
|
|
@@ -3169,6 +3188,7 @@ export declare const allTools: ({
|
|
|
3169
3188
|
senderLinkedinUrl?: undefined;
|
|
3170
3189
|
offerPositioning?: undefined;
|
|
3171
3190
|
messageGenerationMode?: undefined;
|
|
3191
|
+
messageApproach?: undefined;
|
|
3172
3192
|
currentStep?: undefined;
|
|
3173
3193
|
watchNarration?: undefined;
|
|
3174
3194
|
leadSourceType?: undefined;
|
package/package.json
CHANGED
|
@@ -53,6 +53,13 @@ The bracket is replaced at generation time by either (a) the rendered
|
|
|
53
53
|
sentence, or (b) nothing (omit). The brackets themselves never appear in
|
|
54
54
|
the final message.
|
|
55
55
|
|
|
56
|
+
**When you save the template in the campaign brief, write each bracket on one
|
|
57
|
+
line.** The examples below wrap a bracket across lines so it is readable here.
|
|
58
|
+
The brief validator reads a bracket that spans three or more lines as message
|
|
59
|
+
prose and rejects the save ("3+ consecutive non-empty prose lines with only
|
|
60
|
+
single newlines"). Keep the DO / DON'T / FALLBACK labels; drop the line breaks
|
|
61
|
+
inside the bracket.
|
|
62
|
+
|
|
56
63
|
## Required clauses inside an AI-native token
|
|
57
64
|
|
|
58
65
|
Every well-specified AI-native token must include four things:
|
|
@@ -80,6 +80,12 @@ approval and campaign setup validation.
|
|
|
80
80
|
Use `research-sender`, call `complete_sender_research` before shell creation,
|
|
81
81
|
and carry proof gaps into the brief.
|
|
82
82
|
|
|
83
|
+
## Message Approach
|
|
84
|
+
|
|
85
|
+
Before `create_campaign`, load `references/message-approach.md` and choose how
|
|
86
|
+
first messages are written: Conversations (each written for the person, no
|
|
87
|
+
template) or Message Lab (one approved template). Pass it as `messageApproach`.
|
|
88
|
+
|
|
83
89
|
## Brief Provenance
|
|
84
90
|
|
|
85
91
|
When rendering the first brief, do not add bracketed inline source tags. Start
|
|
@@ -308,6 +314,7 @@ Route user copy feedback before `approve-message` back to Message Drafting; pare
|
|
|
308
314
|
|
|
309
315
|
Load references only when needed:
|
|
310
316
|
|
|
317
|
+
- `references/message-approach.md` before `create_campaign`.
|
|
311
318
|
- `references/watch-link-handoff.md` for watch-link handoff.
|
|
312
319
|
- `references/watch-guide-narration.md` for watch narration.
|
|
313
320
|
- `references/lead-validation-preview.md` for legacy/debug preview shapes.
|
|
@@ -90,5 +90,8 @@ messages that pass my quality check and launch for you, then keep your sends fil
|
|
|
90
90
|
Reply review first if you would rather approve." Save the answer with `customer_program`
|
|
91
91
|
`save_context` as `launchConsent: { mode: "autopilot" | "review_first", decidedAt }`.
|
|
92
92
|
A yes, or no objection, is `autopilot`; a wish to approve first is `review_first`.
|
|
93
|
-
|
|
93
|
+
A reply that answers other questions and says nothing about this offer is not an answer to it:
|
|
94
|
+
save nothing and keep every approval. Never ask again; a later "review first" switches to `review_first`, and "pause" sets the saved work and sending pauses with `set_preferences`. If the
|
|
94
95
|
customer has not answered when you reach launch, keep the launch confirmation.
|
|
96
|
+
During a new workspace's first campaign (`first-campaign-onboarding`) do not make this offer at all:
|
|
97
|
+
the first launch always needs the customer's explicit yes.
|
|
@@ -540,4 +540,6 @@ marketplace actors, wrong function, or no budget path, classify it as
|
|
|
540
540
|
|
|
541
541
|
After filters are saved, wait for explicit filter approval before Filter Leads.
|
|
542
542
|
Then wait for message template/token approval before enrichment, scoring, or
|
|
543
|
-
Generate Message cells are queued.
|
|
543
|
+
Generate Message cells are queued. A Conversations campaign
|
|
544
|
+
(`references/message-approach.md`) has no template to approve: queue the
|
|
545
|
+
sample once the filters are settled.
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
# Message Approach
|
|
2
|
+
|
|
3
|
+
Every campaign writes its first message one of two ways. Decide which before
|
|
4
|
+
`create_campaign`, pass it as `messageApproach`, and say which you picked in
|
|
5
|
+
one plain line the customer can correct.
|
|
6
|
+
|
|
7
|
+
- **Conversations** (`messageApproach: "conversation"`). Each first message is
|
|
8
|
+
written for that one person by Sellable's writer and opens a conversation.
|
|
9
|
+
It carries no pitch and no ask; the offer comes up once they reply. Choose
|
|
10
|
+
it when replies have an owner: reply drafting is on for the campaign (the
|
|
11
|
+
default), so each reply arrives as a draft to approve, or the workspace's
|
|
12
|
+
Reply Agent answers.
|
|
13
|
+
- **Message Lab** (`messageApproach: "message_test"`). One approved message
|
|
14
|
+
goes to everyone, so the customer can test which copy wins. This is the
|
|
15
|
+
template flow described in the create-campaign-v2 prompt and flow file. Choose it when the
|
|
16
|
+
customer wants to approve exact wording, test copy against copy, or has
|
|
17
|
+
switched reply drafting off.
|
|
18
|
+
|
|
19
|
+
When the customer has not said, use Conversations if replies have an owner and
|
|
20
|
+
Message Lab otherwise. "Same message to everyone", "test my copy" or "I want
|
|
21
|
+
to approve the wording" means Message Lab. "Write each one", "personal",
|
|
22
|
+
"conversational" or "use the reply agent" means Conversations. Change an
|
|
23
|
+
existing campaign with `update_campaign({ campaignId, messageApproach })`.
|
|
24
|
+
|
|
25
|
+
**A Conversations campaign has no template.** The backend writes each message
|
|
26
|
+
only when the campaign carries no approved template, so:
|
|
27
|
+
|
|
28
|
+
- write no `## Approved Message Template` section, no `{{token}}` copy and no
|
|
29
|
+
first-message wording into the brief, and keep pitch wording out of `dos`;
|
|
30
|
+
- put the offer and the proof under headings that say "for replies only";
|
|
31
|
+
- when the first message goes out as a DM after the connection request is
|
|
32
|
+
accepted (the recommended sequence), add this line to the campaign `dos`
|
|
33
|
+
with the sender's first name: "This first message goes out as a LinkedIn DM
|
|
34
|
+
after they accept {sender}'s connection request: thank them for
|
|
35
|
+
connecting.";
|
|
36
|
+
- skip Message Drafting, the message template review and the template
|
|
37
|
+
approval. After the filters are saved, queue enrichment for the sample;
|
|
38
|
+
fit scoring and Generate Message follow on their own;
|
|
39
|
+
- review the generated messages themselves before showing or approving
|
|
40
|
+
them: the person and company match the profile, no pitch, and nothing
|
|
41
|
+
about the person that their profile does not show (a guessed city, a guessed
|
|
42
|
+
role). Leave a message that fails out of the sample, say so in one line,
|
|
43
|
+
and do not approve that row until it is rewritten
|
|
44
|
+
(`queue_campaign_cells` with `columnRole: "generateMessage"`, that row,
|
|
45
|
+
`forceRerun: true`). `get_campaign` reports the saved approach as
|
|
46
|
+
`campaignOffer.messageApproach`.
|
|
47
|
+
|
|
48
|
+
**The writer sounds like the sender only when it knows the sender.** With
|
|
49
|
+
exactly one sender attached to the campaign and that sender's context saved,
|
|
50
|
+
it writes the full message, including a line about the sender. With no sender,
|
|
51
|
+
several senders, or no saved context it writes the short form: a greeting,
|
|
52
|
+
one plain line about the person's work, and a light line about their town. So in a Conversations campaign:
|
|
53
|
+
|
|
54
|
+
1. Ask for the LinkedIn connection before the sample, not after it.
|
|
55
|
+
2. Once `list_senders` shows the account, attach it
|
|
56
|
+
(`update_campaign({ campaignId, senderIds: [id] })`), read it with
|
|
57
|
+
`get_sender`, and save what the profile research found with
|
|
58
|
+
`set_sender_context` (pass `get_sender`'s `generationContext.updatedAt` as
|
|
59
|
+
`expectedUpdatedAt`, null on the first save). Use facts from the sender's
|
|
60
|
+
own profile only: what they built and did (`workStories`), plain personal
|
|
61
|
+
facts (`personalNotes`), a few lines in their own words from their posts
|
|
62
|
+
or About (`voiceExamples`), and what they may share or will not say
|
|
63
|
+
(`sourcesAndLimits`).
|
|
64
|
+
3. Generate or regenerate the sample after that
|
|
65
|
+
(`queue_campaign_cells` with `columnRole: "generateMessage"`, the sample
|
|
66
|
+
rows, `forceRerun: true`).
|
|
67
|
+
|
|
68
|
+
If the customer has not connected an account when the sample is ready, show
|
|
69
|
+
the short-form sample and say the messages gain a line about them once the
|
|
70
|
+
account is connected.
|
|
@@ -54,7 +54,8 @@ rerun `import_leads`.
|
|
|
54
54
|
only after saved-filter approval; `validate-sample` is the logical loop name,
|
|
55
55
|
not a visible route)
|
|
56
56
|
- Approved message template/token rules exist in the campaign brief before any
|
|
57
|
-
enrichment or scoring cells are queued
|
|
57
|
+
enrichment or scoring cells are queued (Message Lab only; a Conversations
|
|
58
|
+
campaign has no template)
|
|
58
59
|
- Confirmed campaign rows from Step 13, with the internal execution slice stored
|
|
59
60
|
in `WorkflowTable.config.mcp.reviewBatch` (default 15)
|
|
60
61
|
- Config from `auto-execute.yaml`: `sample.sampleSize`,
|
|
@@ -40,7 +40,8 @@ states.
|
|
|
40
40
|
sample you are checking, and why that helps this campaign.
|
|
41
41
|
- At filter choice, do not say the batch is filtering before rubrics are saved.
|
|
42
42
|
- Avoid internal terms: MCP, tool, currentStep, workflow table, scout, debug.
|
|
43
|
-
-
|
|
43
|
+
- Always send a short `safety` line when the step changes; the API rejects a
|
|
44
|
+
step change without one. It is internal context, not a visible disclaimer.
|
|
44
45
|
- Do not repeat long negative lists like "no leads import, no enrichment, no
|
|
45
46
|
messages, no sequence, no sending" in routine watch copy. Prefer one concise
|
|
46
47
|
gate phrase only when it matters: "This only approves me to look for the best
|
|
@@ -8,6 +8,13 @@ visibility: internal
|
|
|
8
8
|
|
|
9
9
|
This is the tail detail extracted from the main create-campaign-v2 SKILL.md to keep the entry prompt slim. The agent loads this on-demand BEFORE entering the auto-execute or validate-sample steps. Follow this verbatim.
|
|
10
10
|
|
|
11
|
+
**Conversations campaigns** (`messageApproach: "conversation"`, see
|
|
12
|
+
`create-campaign-v2` `references/message-approach.md`) have no message
|
|
13
|
+
template. Every rule below that requires an approved template, `{{...}}`
|
|
14
|
+
tokens in the brief, Message Drafting, or template approval before queueing
|
|
15
|
+
applies to Message Lab campaigns only. In a Conversations campaign the same
|
|
16
|
+
cascade writes each message once a row passes the fit check.
|
|
17
|
+
|
|
11
18
|
CampaignOffer state and the watch link are canonical. Disk artifacts are
|
|
12
19
|
optional debug/UAT diagnostics; normal customer runs should not expose local
|
|
13
20
|
draft files. Resume, gating, and handoff read campaign state first.
|
|
@@ -198,9 +205,8 @@ import milestone.
|
|
|
198
205
|
`messaging.critique.enabled`, `handoff.autoStart`,
|
|
199
206
|
`handoff.orientation`, `retry.sameToolSameError`,
|
|
200
207
|
`logging.logEveryThresholdTrip`.
|
|
201
|
-
2. Resolve and materialize the approved source after Start Import approval.
|
|
202
|
-
`references/
|
|
203
|
-
discovery or when a legacy no-shell approval fixture must be replayed. If
|
|
208
|
+
2. Resolve and materialize the approved source after Start Import approval.
|
|
209
|
+
`references/step-13-import-leads.md` holds the import steps. If
|
|
204
210
|
CampaignOffer state already attached searches/selections to this campaign,
|
|
205
211
|
reuse them. If there is no `lead-source-intake.json`, replay the approved
|
|
206
212
|
provider recipe from `lead-review.md` with `campaignOfferId` before
|
|
@@ -381,7 +387,8 @@ the output — not to generate messages manually.
|
|
|
381
387
|
**What Step 15 does:**
|
|
382
388
|
|
|
383
389
|
1. Before queueing or waiting on Generate Message cells, confirm the minted
|
|
384
|
-
campaign brief still contains `{{...}}` in `## Approved Message Template
|
|
390
|
+
campaign brief still contains `{{...}}` in `## Approved Message Template`
|
|
391
|
+
(Message Lab only; a Conversations campaign must have no template).
|
|
385
392
|
If it does not, fail before the cascade runs. Do not repair after mint; the
|
|
386
393
|
template must be present during mint so Step 15 never starts in freeform
|
|
387
394
|
generation mode.
|
|
@@ -622,7 +629,6 @@ runs.
|
|
|
622
629
|
| ------------------------------------------------ | ------------------------------------------------------------------------- |
|
|
623
630
|
| `references/approval-gate-framing.md` | Approval gate, before showing the approval packet |
|
|
624
631
|
| `references/watch-link-handoff.md` | First brief handoff + explicit link recovery only |
|
|
625
|
-
| `references/post-mint-source-materialization.md` | Step 13, before importing normal-discovery or legacy campaignless sources |
|
|
626
632
|
| `references/sample-validation-loop.md` | Step 14, before enriching + scoring the sample |
|
|
627
633
|
| `references/escalation-ladder.md` | Any tail step that needs to decide retry / revise / escalate |
|
|
628
634
|
| `references/final-handoff-contract.md` | Step 16, and every Claude greenlight turn |
|
|
@@ -48,11 +48,16 @@ reply to either the same way.)
|
|
|
48
48
|
A reply in that thread is explicit permission to begin preparation, not
|
|
49
49
|
permission to send. It needs no mention of this Agent.
|
|
50
50
|
|
|
51
|
-
- **The reply contains a LinkedIn profile URL.**
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
51
|
+
- **The reply contains a LinkedIn profile URL or handle.** A single word such
|
|
52
|
+
as `csreyes92` or `@csreyes92` is a handle: read it as
|
|
53
|
+
`https://www.linkedin.com/in/{handle}/`. Send one short line first so
|
|
54
|
+
the customer is not left looking at silence: "Got it. Researching you and
|
|
55
|
+
your company now. I'll be back in a few minutes with a first campaign to
|
|
56
|
+
review." Send it before the Live State Check and before loading any other
|
|
57
|
+
skill or reference; those reads take over a minute. Then run the Live State
|
|
58
|
+
Check and enter **Create-Campaign Handoff** in the same turn, with that URL
|
|
59
|
+
as the sender identity. If the Live State Check finds a campaign already
|
|
60
|
+
launched, say so in the next line and offer to help with that campaign.
|
|
56
61
|
- **The reply is a yes, or anything else that agrees, with no profile URL.**
|
|
57
62
|
Ask for it once, in one sentence: "What's your LinkedIn profile URL? I'll
|
|
58
63
|
use it to research you and your company." Ask nothing else.
|
|
@@ -209,18 +214,242 @@ continue in the same conversation; do not ask the user to run another command.
|
|
|
209
214
|
|
|
210
215
|
1. Call `bootstrap_create_campaign({ flowVersion:"v2", host:<current host> })`.
|
|
211
216
|
2. Load `get_subskill_prompt({ subskillName:"create-campaign-v2" })` to
|
|
212
|
-
`hasMore:false`
|
|
217
|
+
`hasMore:false`, then `core/flow.v2.json`. Load every other reference only
|
|
218
|
+
at the stage that needs it. The customer is waiting in silence, so make
|
|
219
|
+
these reads in as few turns as you can: the Live State Check reads together
|
|
220
|
+
in one turn; then bootstrap, the create-campaign-v2 prompt, the flow file,
|
|
221
|
+
`research-sender` and `list_dnc_entries({ limit:1 })` together in the next.
|
|
213
222
|
3. Reuse known identity, company, audience, offer, and any explicit DNC decision.
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
223
|
+
A public LinkedIn profile URL for company research is not a connected
|
|
224
|
+
sending account: the latter belongs at Settings, not before the draft.
|
|
225
|
+
The flow's order and state gates still hold: the campaign exists before
|
|
226
|
+
anyone is imported, the source is chosen before the import, sample rows
|
|
227
|
+
exist before filters and messages run, and the template is saved before
|
|
228
|
+
anything is queued. Who decides each one is set by the next section. Offer
|
|
229
|
+
CRM or meeting integrations only as optional context when helpful.
|
|
219
230
|
4. Reuse an existing draft for the same goal. Never create a duplicate because
|
|
220
231
|
onboarding resumed in a new session.
|
|
221
232
|
5. Agreement authorizes campaign creation/preparation only, not launch. Never
|
|
222
|
-
call `start_campaign` until the
|
|
223
|
-
|
|
233
|
+
call `start_campaign` until the customer has seen who sends and how many a
|
|
234
|
+
day, and explicitly says to launch.
|
|
235
|
+
|
|
236
|
+
### The first campaign in three approvals
|
|
237
|
+
|
|
238
|
+
The welcome promised "a first campaign for you to review". For a new
|
|
239
|
+
workspace's first campaign the customer is asked for three things and nothing
|
|
240
|
+
else: the **direction**, the **sample**, and the **launch**. You decide
|
|
241
|
+
everything in between yourself, using the flow's own hands-off rules
|
|
242
|
+
(`yoloMode.autoSelectsPreLaunchChoices`, except its "brief approval" and
|
|
243
|
+
"generated message review" entries, which are approvals 1 and 2 here), and say
|
|
244
|
+
what you chose in one plain line when you report back. Where the general workflow, a reference file, or a
|
|
245
|
+
tool result says otherwise, this section wins for the first campaign.
|
|
246
|
+
|
|
247
|
+
This is not the standing `launchConsent` autopilot. Do not ask about or save
|
|
248
|
+
`launchConsent` during the first campaign, and never skip one of the three
|
|
249
|
+
approvals. Silence, or a reply that answers something else, is never an
|
|
250
|
+
approval. The daily review offers hands-off sending once the first campaign is
|
|
251
|
+
running.
|
|
252
|
+
|
|
253
|
+
**What you never ask.** Not the four setup questions ("Who should we target
|
|
254
|
+
first?", "What should we pitch?", the credibility question, "How should we
|
|
255
|
+
find prospects?"): giving the profile URL is the customer's instruction to use
|
|
256
|
+
Sellable's researched recommendation and to find prospects for them, which is
|
|
257
|
+
the flow's own skip rule for each question. Not "is this the right company?"
|
|
258
|
+
or "which of your offers should I lead with?": pick the one the profile and
|
|
259
|
+
site lead with and say so in the direction, where they can correct it. Not a
|
|
260
|
+
separate approval for where
|
|
261
|
+
to look, for starting the import, for using filters, for the filter rules, or
|
|
262
|
+
for the message template: those are yours to decide and report.
|
|
263
|
+
|
|
264
|
+
**How you write in Slack.**
|
|
265
|
+
|
|
266
|
+
- Ask in plain words the customer can answer with yes or with the change they
|
|
267
|
+
want. Slack has no question panel: never ask for option letters, and never
|
|
268
|
+
mention a question panel or a config file.
|
|
269
|
+
- Post nothing between "Got it" and the direction: no research progress lines,
|
|
270
|
+
tool names, counts of proof items, or which workspace you are in (this Agent
|
|
271
|
+
is locked to one workspace and the customer knows which).
|
|
272
|
+
- Never print the "WATCH … BUILD THE CAMPAIGN LIVE" block, "Keep this chat
|
|
273
|
+
open", "Brief approved", or any line that tells the customer to run a
|
|
274
|
+
command. Those are written for other hosts.
|
|
275
|
+
- The channel is shared, and the returned `watchUrl` carries a sign-in token.
|
|
276
|
+
Post that URL with only its `token` parameter removed, keeping the rest
|
|
277
|
+
exactly as returned. The customer signed in to Sellable when they set this
|
|
278
|
+
Agent up. This replaces the tool's instruction to print the handoff block
|
|
279
|
+
and its rule against changing the link.
|
|
280
|
+
- Do not paste the full brief into Slack. Save it on the campaign and post the
|
|
281
|
+
five-line summary below.
|
|
282
|
+
|
|
283
|
+
**How the first message is written.** The first campaign uses the
|
|
284
|
+
Conversations approach from the workflow's `references/message-approach.md`:
|
|
285
|
+
create it with `messageApproach: "conversation"`, write no message template,
|
|
286
|
+
and let Sellable's writer write each first message for the person. Use Message
|
|
287
|
+
Lab (`"message_test"`, one approved message to everyone) only when the
|
|
288
|
+
customer asks to send the same message to everyone, to test copy, or to
|
|
289
|
+
approve exact wording; then the template steps of the workflow apply and you
|
|
290
|
+
approve the template yourself as before.
|
|
291
|
+
|
|
292
|
+
**Approval 1: direction.** After research, write the brief, create the
|
|
293
|
+
campaign with it (`create_campaign`), and post one message in this shape. The
|
|
294
|
+
saved brief is the full brief; this message is how the customer approves it,
|
|
295
|
+
so skip the flow's setup receipt, its "Does this brief look right…" question
|
|
296
|
+
and the inline brief.
|
|
297
|
+
|
|
298
|
+
```text
|
|
299
|
+
Here's the first campaign I'd run for {company}.
|
|
300
|
+
|
|
301
|
+
*Who* {the buyer, one line}
|
|
302
|
+
*Why now* {why these people care right now, one line}
|
|
303
|
+
*Offer* {what you would get to once they reply, and the small ask, one line}
|
|
304
|
+
*Proof* {the proof you would lead with; if you found none, say so and what you would use instead}
|
|
305
|
+
*Where I'll look* {where you will find them first, in plain words}
|
|
306
|
+
*Messages* Written for each person to open a conversation; the offer comes once they reply. Say "same message to everyone" if you'd rather test one message.
|
|
307
|
+
|
|
308
|
+
Full brief: {campaign link}
|
|
309
|
+
|
|
310
|
+
Two things from you:
|
|
311
|
+
1. Does this look right? Reply yes, or tell me what to change.
|
|
312
|
+
2. Is there anyone I must never contact: customers, open deals, people who opted out? Paste the list here, or say "no list".
|
|
313
|
+
|
|
314
|
+
Nothing is sent on LinkedIn until you approve the launch.
|
|
315
|
+
```
|
|
316
|
+
|
|
317
|
+
The first message to a prospect opens a conversation; the message rules
|
|
318
|
+
decide its wording, and they keep the pitch and the ask out of it. So write
|
|
319
|
+
the *Offer* line as what comes once the prospect replies, and do not promise,
|
|
320
|
+
here or in the saved brief, that the first message will mention the post
|
|
321
|
+
someone engaged with or carry the ask.
|
|
322
|
+
|
|
323
|
+
If something you assumed could be stale (for example the profile pitches two
|
|
324
|
+
different things), say which one you picked in the line it affects. Leave out
|
|
325
|
+
the second question, and write "One thing from you:", when
|
|
326
|
+
`list_dnc_entries` already shows a list or the saved program context has
|
|
327
|
+
`dncState` `provided` or `explicit_none`. A yes, or "keep going", with no
|
|
328
|
+
answer to the second question is not a "no list" decision, and it is not a
|
|
329
|
+
reason to stop either: carry on with the list as it is, add to your
|
|
330
|
+
acknowledgement "You can send me a do-not-contact list any time before
|
|
331
|
+
launch.", and ask once more in the launch message. Never hold the work
|
|
332
|
+
waiting for a list. Follow the DNC Contract for a pasted list, and save
|
|
333
|
+
the decision with `customer_program` `save_context` (`dncState:
|
|
334
|
+
"explicit_none"` for "no list", `"provided"` once a list is imported) so a
|
|
335
|
+
later session does not ask again. The answer given here settles the flow's
|
|
336
|
+
do-not-contact gate; do not ask again before the import.
|
|
337
|
+
|
|
338
|
+
**Between 1 and 2.** Once the direction and the do-not-contact answer are in,
|
|
339
|
+
reply first, before any tool call: "On it. I'll find the people, check who
|
|
340
|
+
fits and write the messages. Your sample will be here in about 15 to 20
|
|
341
|
+
minutes. While I work: connect the LinkedIn account that will send,
|
|
342
|
+
https://app.sellable.dev/linkedin-accounts It takes about two minutes, it is
|
|
343
|
+
only used to send what you approve, and once it's connected I write the
|
|
344
|
+
messages in your voice." Then set `interactionMode: "autonomous"` on the campaign with
|
|
345
|
+
`update_campaign` and pass `confirmed: true` where a search or import tool
|
|
346
|
+
asks for it: the direction approval is that confirmation. Do the work without
|
|
347
|
+
asking: pick where to look, find and import the people within the flow's
|
|
348
|
+
caps, use filters and write the rules, and prepare the 15-person sample. In
|
|
349
|
+
Conversations there is no template to write: queue the sample once the rules
|
|
350
|
+
are saved and the messages are written for you.
|
|
351
|
+
|
|
352
|
+
- **Keep the customer posted.** Whenever about five minutes have passed since
|
|
353
|
+
your last message, post one short line in plain words on where you are
|
|
354
|
+
("Found 370 people who engaged with three posts about this. Checking who
|
|
355
|
+
fits and writing the message now."). No more often than that.
|
|
356
|
+
- **The live builder.** Where the flow's required watch copy tells the
|
|
357
|
+
customer to approve something this section removed, write instead what is
|
|
358
|
+
happening and that nothing is needed from them yet.
|
|
359
|
+
- **Import size.** If `import_leads` says the selected posts cover fewer
|
|
360
|
+
people than the target, select again with the number it says they cover and
|
|
361
|
+
retry once. Do not start a new search for a small gap.
|
|
362
|
+
- **Waits.** A wait tool that stops with `tool_timeout_guard` has only reached
|
|
363
|
+
the host's time limit. Call it again while work is still running.
|
|
364
|
+
- **A sample where nobody fits is not a pause.** Follow the flow's own
|
|
365
|
+
recovery first: a fresh sample from the same list. Stop early only for the
|
|
366
|
+
flow's must-pause reasons: missing data, or a source, filter or message
|
|
367
|
+
that still fails its quality floor after that recovery. Then say what
|
|
368
|
+
failed and what you suggest, as one question.
|
|
369
|
+
- **Let the sample finish.** The flow stops at the first passing message; here
|
|
370
|
+
wait until the 15-person sample has finished scoring and writing, because
|
|
371
|
+
the customer is shown three people and a count.
|
|
372
|
+
- **The sender, as soon as there is one.** Check `list_senders` when the
|
|
373
|
+
import finishes and again before you show the sample. When the customer's
|
|
374
|
+
account is there, attach it to the campaign, save what your research found
|
|
375
|
+
about them as sender context, and write or rewrite the sample after that,
|
|
376
|
+
as `references/message-approach.md` describes. A sample written
|
|
377
|
+
before the account was connected is the short form, with nothing about the
|
|
378
|
+
sender.
|
|
379
|
+
- **Check before you show.** For each message you are about to show, compare
|
|
380
|
+
the company it names with the person's own headline. Leave out any that
|
|
381
|
+
disagree and say that you dropped them.
|
|
382
|
+
|
|
383
|
+
**Approval 2: sample.** When the sample is ready, post real people and the
|
|
384
|
+
real message each would receive:
|
|
385
|
+
|
|
386
|
+
```text
|
|
387
|
+
Your sample is ready: {the number you show below} real people and the message each would get.
|
|
388
|
+
|
|
389
|
+
{Name} · {title}, {company} · {why they are here, a few words}
|
|
390
|
+
"{the message they would receive}"
|
|
391
|
+
|
|
392
|
+
{two more like this}
|
|
393
|
+
|
|
394
|
+
{passed} of {checked} people I checked fit. {one line on why the rest did not}
|
|
395
|
+
What I chose: {where the people came from and how many are on the list, and the fit rules in a few words}
|
|
396
|
+
Each message is written for that person and has no pitch on purpose. {the offer} comes once they reply.
|
|
397
|
+
{only if no sending account is connected yet: "These are the short version. Connect your LinkedIn account and I'll rewrite them with a line about you: https://app.sellable.dev/linkedin-accounts"}
|
|
398
|
+
Everyone and every message: {campaign link}
|
|
399
|
+
|
|
400
|
+
Reply approve to use these people and messages like these, or tell me what to change.
|
|
401
|
+
Nothing has been sent.
|
|
402
|
+
```
|
|
403
|
+
|
|
404
|
+
**After "approve".** Reply first: "Approved." plus anything they changed, in
|
|
405
|
+
the same line ("Approved, without Corey."). Mark every person in the sample
|
|
406
|
+
the customer kept as approved in the table, not one or two. A person the
|
|
407
|
+
customer dropped stays unapproved; never approve them later, in bulk or
|
|
408
|
+
otherwise.
|
|
409
|
+
|
|
410
|
+
**Approval 3: launch.** The only thing that can still be missing is the
|
|
411
|
+
sending account. Call `list_senders`.
|
|
412
|
+
|
|
413
|
+
- **No usable sender.** Ask for that one thing: "One thing left: connect the
|
|
414
|
+
LinkedIn account that will send. https://app.sellable.dev/linkedin-accounts
|
|
415
|
+
It takes about two minutes, and it is only used to send what you approve;
|
|
416
|
+
finding and checking people happened outside your account. Tell me when
|
|
417
|
+
it's done." When they say it is done, call `list_senders` again. If nothing
|
|
418
|
+
usable is there yet, say so plainly and give the link again.
|
|
419
|
+
- **One connected sender that is the person you researched.** Use it without
|
|
420
|
+
asking. If the connected account is someone else, or more than one is
|
|
421
|
+
connected, ask which account should send. That is the one extra question
|
|
422
|
+
this section allows.
|
|
423
|
+
- **Get it ready, then ask.** Attach the sender and the recommended sequence
|
|
424
|
+
to the campaign yourself (`update_campaign` with `senderIds`, then
|
|
425
|
+
`attach_recommended_sequence`); this prepares the launch and sends nothing.
|
|
426
|
+
Read the real numbers for that sender (`refill_v3_world_state` for the next
|
|
427
|
+
two days). Then post:
|
|
428
|
+
|
|
429
|
+
```text
|
|
430
|
+
Ready to launch "{campaign name}".
|
|
431
|
+
|
|
432
|
+
*Sender* {name}
|
|
433
|
+
*First sends* {n} approved messages, {how many a day and which days, from the numbers you read}
|
|
434
|
+
*Replies* I'll post each reply here as a draft for you to approve.
|
|
435
|
+
{only if no do-not-contact answer was ever given: "You haven't sent a do-not-contact list. Paste one now if there is anyone I must not message."}
|
|
436
|
+
|
|
437
|
+
Reply launch to start. You can pause it any time by telling me.
|
|
438
|
+
```
|
|
439
|
+
|
|
440
|
+
State only numbers you read. If you could not read one, leave that part out
|
|
441
|
+
rather than guess.
|
|
442
|
+
- **Start only on an explicit yes** ("launch", "yes, start", "go"). Then call
|
|
443
|
+
`start_campaign` and report what happened: what is scheduled and for when,
|
|
444
|
+
and that scheduled is not sent yet.
|
|
445
|
+
- **After the launch, one question about the rest.** Only the sample has been
|
|
446
|
+
checked and written. Ask: "{remaining} more people are on the list. Want me
|
|
447
|
+
to keep going through them and approve the messages that pass the same
|
|
448
|
+
checks, so your sends stay filled? Reply yes, or review first." Save the
|
|
449
|
+
answer as the standing `launchConsent` (`customer_program` `save_context`:
|
|
450
|
+
`autopilot` for yes, `review_first` otherwise). This is the first and only
|
|
451
|
+
time that question is asked during onboarding, and only an explicit yes is
|
|
452
|
+
`autopilot`.
|
|
224
453
|
|
|
225
454
|
## Hard Boundaries
|
|
226
455
|
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: research-sender
|
|
3
|
-
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."
|
|
3
|
+
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."
|
|
4
4
|
visibility: internal
|
|
5
|
-
allowed-tools: mcp__sellable__fetch_linkedin_profile mcp__sellable__fetch_company mcp__sellable__complete_sender_research WebSearch ToolSearch
|
|
5
|
+
allowed-tools: mcp__sellable__fetch_linkedin_profile mcp__sellable__fetch_company mcp__sellable__complete_sender_research WebSearch WebFetch ToolSearch
|
|
6
6
|
---
|
|
7
7
|
|
|
8
8
|
# Research Sender (Parallel-First)
|
|
@@ -64,14 +64,24 @@ Concretely, your next assistant message must contain exactly these five
|
|
|
64
64
|
`tool_use` blocks, in any order, with no leading or trailing prose:
|
|
65
65
|
|
|
66
66
|
1. `fetch_linkedin_profile({ linkedinUrl })` — sender's LinkedIn profile (firstName/lastName, currentCompany, experience, follower count, education) for the brief's "sender background" section.
|
|
67
|
-
2. `fetch_company({
|
|
67
|
+
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`.
|
|
68
68
|
3. `WebSearch({ query: 'site:{companyDomain} ("case study" OR "customer story" OR testimonial OR "success story") "{companyName}"' })` — proof.
|
|
69
69
|
4. `WebSearch({ query: '"{companyName}" {companyDomain} {currentYear} (funding OR raised OR seed OR series OR hiring OR launch OR "press release")' })` — growth/credibility.
|
|
70
70
|
5. `WebSearch({ query: '"{companyName}" about product site:{companyDomain}' })` — positioning.
|
|
71
71
|
|
|
72
|
+
**When only the profile URL is known** (a new customer gave nothing else), the
|
|
73
|
+
company name, domain and LinkedIn company URL come from the profile, so the
|
|
74
|
+
five calls cannot all go in one message. Call `fetch_linkedin_profile` alone
|
|
75
|
+
first, then emit the other four together in the next message. If the profile
|
|
76
|
+
does not show the company's website, call `fetch_company` next on its own and
|
|
77
|
+
take the website from it, then emit the three searches together. Never guess
|
|
78
|
+
the company or its domain from the handle. This order replaces "exactly these
|
|
79
|
+
five … in a single assistant message" above for this case.
|
|
80
|
+
|
|
72
81
|
**Self-check before you reply:** if your reply contains fewer than five
|
|
73
|
-
`tool_use` blocks (after the optional `ToolSearch` setup turn),
|
|
74
|
-
|
|
82
|
+
`tool_use` blocks (after the optional `ToolSearch` setup turn), or fewer than
|
|
83
|
+
four when the profile was fetched first, you are violating the protocol.
|
|
84
|
+
Stop, rewrite the reply with all of them.
|
|
75
85
|
|
|
76
86
|
**Known limitation (`claude -p` headless mode):** Claude often serializes
|
|
77
87
|
these calls one-per-turn even with explicit instructions. That's fine — the
|
|
@@ -137,6 +147,11 @@ signals, AND the campaign fixture/operator told you proof is required, you
|
|
|
137
147
|
MAY issue ONE additional WebFetch on the most promising case-study URL from
|
|
138
148
|
the proof WebSearch. Cap at one WebFetch. No subagents, no second WebSearch round.
|
|
139
149
|
|
|
150
|
+
If the proof and positioning searches returned nothing from the company's own
|
|
151
|
+
site (common when the company name is an ordinary word), you MAY issue that
|
|
152
|
+
ONE WebFetch on the company's homepage instead, to read what it says it does
|
|
153
|
+
and any customers it names. Same cap.
|
|
154
|
+
|
|
140
155
|
If that still yields nothing, set `proofItemsFound: 0` in
|
|
141
156
|
`complete_sender_research` and proceed; the brief can ship without proof.
|
|
142
157
|
|
|
@@ -157,16 +172,21 @@ If no reliable evidence is found, set counts to 0 and include that in `notes`.
|
|
|
157
172
|
|
|
158
173
|
## Progress UX
|
|
159
174
|
|
|
160
|
-
|
|
175
|
+
In a chat host where every line you write is posted to the customer (Slack),
|
|
176
|
+
write nothing here: the customer was already told research is under way, and
|
|
177
|
+
the next thing they should read is the result. Tool names, "in parallel",
|
|
178
|
+
time estimates and counts of proof items are not for customers.
|
|
179
|
+
|
|
180
|
+
In a terminal host, before issuing the batch:
|
|
161
181
|
|
|
162
182
|
```
|
|
163
|
-
|
|
183
|
+
Researching the sender and company…
|
|
164
184
|
```
|
|
165
185
|
|
|
166
186
|
After synthesis:
|
|
167
187
|
|
|
168
188
|
```
|
|
169
|
-
Sender research ready
|
|
189
|
+
Sender research ready.
|
|
170
190
|
```
|
|
171
191
|
|
|
172
192
|
## What Changed From The Previous Protocol
|