@sellable/mcp 0.1.123 → 0.1.125
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/agents/source-scout-linkedin-engagement.md +18 -9
- package/agents/source-scout-sales-nav.md +19 -2
- package/dist/tools/leads.d.ts +74 -0
- package/dist/tools/leads.js +64 -5
- package/package.json +1 -1
- package/skills/create-campaign-v2/SKILL.md +47 -23
- package/skills/create-campaign-v2/core/flow.v2.json +28 -11
- package/skills/create-campaign-v2/references/step-13-import-leads.md +19 -4
- package/skills/providers/signal-discovery.md +36 -13
|
@@ -47,11 +47,13 @@ currentStep: "signal-discovery" })` before sampling so the watched Signal
|
|
|
47
47
|
6. Fetch or sample engagers for promoted posts and score rough ICP fit from
|
|
48
48
|
visible headline/display-name cues only. Do not enrich people during
|
|
49
49
|
viability estimation.
|
|
50
|
-
7. Compute capacity before recommending the source: target good-fit
|
|
51
|
-
(default
|
|
50
|
+
7. Compute capacity before recommending the source: source target good-fit
|
|
51
|
+
leads (default 300 unless the parent supplies a target), reachable engagers,
|
|
52
52
|
sampled ICP-fit rate as `n/N` plus an easy percentage/range, expected usable
|
|
53
|
-
leads per
|
|
54
|
-
|
|
53
|
+
leads per 100 engagers before and after a conservative dedupe/cleanup
|
|
54
|
+
factor, required engagers to scrape (`source target / fit rate`), average
|
|
55
|
+
reachable engagers per right-content post, expected usable leads per
|
|
56
|
+
right-content post after dedupe/cleanup, and posts needed to hit the target.
|
|
55
57
|
8. Select/promote enough right-content posts to plausibly hit the target. If the
|
|
56
58
|
warm Signals pool is useful but too small, return the expected warm range and
|
|
57
59
|
recommend Sales Nav/Prospeo for scale instead of padding with noisy posts.
|
|
@@ -64,9 +66,12 @@ Return a concise structured result with:
|
|
|
64
66
|
- `keyword_lanes` with timeframe, raw posts found, finalist posts reviewed
|
|
65
67
|
- `selected_posts` with URL/title, author/topic, age, engager count, sampled engagers, good fits as n/N, estimated usable prospects per post, use/discard
|
|
66
68
|
- `sample_leads`, if any
|
|
67
|
-
- `approval_math` with eligible posts, target good-fit leads, sampled
|
|
68
|
-
ICP-fit rate as `n/N` plus percentage/range,
|
|
69
|
-
|
|
69
|
+
- `approval_math` with eligible posts, source target good-fit leads, sampled
|
|
70
|
+
engagers, ICP-fit rate as `n/N` plus percentage/range, good-fit prospects per
|
|
71
|
+
100 engagers, required engagers to scrape, average reachable engagers per
|
|
72
|
+
post, expected usable prospects per post after cleanup, posts needed for
|
|
73
|
+
target, selected post count, review-batch import limit, expected usable lead
|
|
74
|
+
range, and scale fallback
|
|
70
75
|
- `estimated_good_fit_range`
|
|
71
76
|
- `message_context_strength`, directional and source-specific
|
|
72
77
|
- `false_positive_patterns`
|
|
@@ -82,7 +87,11 @@ Evidence standards:
|
|
|
82
87
|
GTM/outbound/buyer pain, workflow, or role context that makes the campaign
|
|
83
88
|
relevant.
|
|
84
89
|
- Do not make the user infer capacity. Say, plainly, how many eligible posts
|
|
85
|
-
exist,
|
|
86
|
-
|
|
90
|
+
exist, how many sampled engagers looked in-ICP, how many good-fit prospects
|
|
91
|
+
that implies per 100 engagers, how many usable prospects one right-content
|
|
92
|
+
post should yield after cleanup, how many engagers must be scraped for the
|
|
93
|
+
300-good-fit source target, how many posts are needed for that source target,
|
|
94
|
+
and which posts you would use. Also say the first campaign import remains the
|
|
95
|
+
bounded 15-row review batch.
|
|
87
96
|
- If `fetch_post_engagers` is unavailable or fails, report that explicitly and mark the estimate lower-confidence.
|
|
88
97
|
- Keep LinkedIn Engagement viable when selected posts can produce roughly 150+ ICP-fit warm prospects before final filtering, even if Sales Nav is more scalable.
|
|
@@ -26,8 +26,16 @@ Process:
|
|
|
26
26
|
2. Preserve target role names with `CURRENT_TITLE` lookups; do not rely on seniority alone when the brief names concrete roles.
|
|
27
27
|
3. When `lookup_sales_nav_filter` returns multiple title options, choose the closest semantic title match instead of the first result.
|
|
28
28
|
4. Build a broad-but-reasonable baseline from role/title, geography, company size, industry/account context, and recent LinkedIn activity when relevant.
|
|
29
|
-
5.
|
|
30
|
-
|
|
29
|
+
5. Check scale against the source target good-fit lead count (default 300
|
|
30
|
+
unless the parent supplies a target). If raw preview volume or projected usable volume
|
|
31
|
+
is below target, do not present the tiny result as the scale fallback yet.
|
|
32
|
+
Loosen nonessential filters in order: remove recent-activity first, widen
|
|
33
|
+
adjacent title variants, widen geography/company-size constraints, and only
|
|
34
|
+
keep hard ICP requirements from the brief.
|
|
35
|
+
6. Run the baseline plus 1-2 refinements or loosening passes if the first pass
|
|
36
|
+
is noisy or under-scaled. Label the final pool as constrained if it still
|
|
37
|
+
cannot plausibly reach the target after loosening.
|
|
38
|
+
7. Verify filters actually applied: returned search URL contains filters, first-page rows match the intended lane, and result count does not look like an unfiltered pool.
|
|
31
39
|
|
|
32
40
|
Return a concise structured result with:
|
|
33
41
|
|
|
@@ -36,6 +44,11 @@ Return a concise structured result with:
|
|
|
36
44
|
- `exact_filter_recipe`
|
|
37
45
|
- `lookup_ids_used`
|
|
38
46
|
- `raw_result_count`
|
|
47
|
+
- `scale_check` with source target good-fit lead count, preview/raw volume, sampled
|
|
48
|
+
good fits as n/N, projected usable count, and whether the pool can reach the
|
|
49
|
+
target
|
|
50
|
+
- `loosening_attempts` with what was removed or widened when the pool was too
|
|
51
|
+
tight
|
|
39
52
|
- `sampled_people` and good fits as n/N
|
|
40
53
|
- `estimated_good_fit_range_after_cleanup`
|
|
41
54
|
- `expected_acceptance_rate_range`, directional if inferred
|
|
@@ -49,5 +62,9 @@ Evidence standards:
|
|
|
49
62
|
|
|
50
63
|
- Optimize for a useful prospect pool, not max volume at any cost.
|
|
51
64
|
- Bias toward `POSTED_ON_LINKEDIN` for reply-likelihood when the pool still has enough scale.
|
|
65
|
+
- Do not over-tighten fallback filters into a pool that cannot be meaningfully
|
|
66
|
+
larger than the warm-post path. If Sales Nav is offered for scale, it should
|
|
67
|
+
either project to the target good-fit count or clearly say it is too tight and
|
|
68
|
+
name the next broadening/Prospeo option.
|
|
52
69
|
- Do not hand-wave missing filter IDs.
|
|
53
70
|
- If Sales Nav returns a giant unfiltered pool, discard that result and retry with valid filters before recommending it.
|
package/dist/tools/leads.d.ts
CHANGED
|
@@ -1,4 +1,16 @@
|
|
|
1
1
|
export declare function normalizeTargetLeadCount(targetLeadCount: unknown, maxImportCount: number): number | undefined;
|
|
2
|
+
type SignalPostForImportSelection = {
|
|
3
|
+
likes: number;
|
|
4
|
+
comments: number;
|
|
5
|
+
};
|
|
6
|
+
export declare function selectSignalPostsForImport<T extends SignalPostForImportSelection>(posts: T[], options: {
|
|
7
|
+
targetEngagerCount?: number;
|
|
8
|
+
maxPostsToScrape?: number;
|
|
9
|
+
}): {
|
|
10
|
+
posts: T[];
|
|
11
|
+
estimatedEngagers: number;
|
|
12
|
+
limited: boolean;
|
|
13
|
+
};
|
|
2
14
|
export type GetProviderPromptInput = {
|
|
3
15
|
provider: "apollo" | "sales-nav" | "prospeo" | "signal-discovery";
|
|
4
16
|
campaignOfferId?: string;
|
|
@@ -112,6 +124,8 @@ export type ImportLeadsInput = {
|
|
|
112
124
|
searchName?: string;
|
|
113
125
|
leadListName?: string;
|
|
114
126
|
headlineICPCriteria?: string[];
|
|
127
|
+
targetEngagerCount?: number;
|
|
128
|
+
maxPostsToScrape?: number;
|
|
115
129
|
rubricGuidelines?: string[];
|
|
116
130
|
};
|
|
117
131
|
type ImportLeadProvider = NonNullable<ImportLeadsInput["provider"]>;
|
|
@@ -213,6 +227,8 @@ export declare const leadToolDefinitions: ({
|
|
|
213
227
|
sourceLeadListId?: undefined;
|
|
214
228
|
targetLeadCount?: undefined;
|
|
215
229
|
mode?: undefined;
|
|
230
|
+
targetEngagerCount?: undefined;
|
|
231
|
+
maxPostsToScrape?: undefined;
|
|
216
232
|
tableId?: undefined;
|
|
217
233
|
campaignName?: undefined;
|
|
218
234
|
keepInSync?: undefined;
|
|
@@ -386,6 +402,8 @@ export declare const leadToolDefinitions: ({
|
|
|
386
402
|
sourceLeadListId?: undefined;
|
|
387
403
|
targetLeadCount?: undefined;
|
|
388
404
|
mode?: undefined;
|
|
405
|
+
targetEngagerCount?: undefined;
|
|
406
|
+
maxPostsToScrape?: undefined;
|
|
389
407
|
tableId?: undefined;
|
|
390
408
|
campaignName?: undefined;
|
|
391
409
|
keepInSync?: undefined;
|
|
@@ -458,6 +476,8 @@ export declare const leadToolDefinitions: ({
|
|
|
458
476
|
sourceLeadListId?: undefined;
|
|
459
477
|
targetLeadCount?: undefined;
|
|
460
478
|
mode?: undefined;
|
|
479
|
+
targetEngagerCount?: undefined;
|
|
480
|
+
maxPostsToScrape?: undefined;
|
|
461
481
|
tableId?: undefined;
|
|
462
482
|
campaignName?: undefined;
|
|
463
483
|
keepInSync?: undefined;
|
|
@@ -602,6 +622,8 @@ export declare const leadToolDefinitions: ({
|
|
|
602
622
|
sourceLeadListId?: undefined;
|
|
603
623
|
targetLeadCount?: undefined;
|
|
604
624
|
mode?: undefined;
|
|
625
|
+
targetEngagerCount?: undefined;
|
|
626
|
+
maxPostsToScrape?: undefined;
|
|
605
627
|
tableId?: undefined;
|
|
606
628
|
campaignName?: undefined;
|
|
607
629
|
keepInSync?: undefined;
|
|
@@ -688,6 +710,8 @@ export declare const leadToolDefinitions: ({
|
|
|
688
710
|
sourceLeadListId?: undefined;
|
|
689
711
|
targetLeadCount?: undefined;
|
|
690
712
|
mode?: undefined;
|
|
713
|
+
targetEngagerCount?: undefined;
|
|
714
|
+
maxPostsToScrape?: undefined;
|
|
691
715
|
tableId?: undefined;
|
|
692
716
|
campaignName?: undefined;
|
|
693
717
|
keepInSync?: undefined;
|
|
@@ -783,6 +807,8 @@ export declare const leadToolDefinitions: ({
|
|
|
783
807
|
sourceLeadListId?: undefined;
|
|
784
808
|
targetLeadCount?: undefined;
|
|
785
809
|
mode?: undefined;
|
|
810
|
+
targetEngagerCount?: undefined;
|
|
811
|
+
maxPostsToScrape?: undefined;
|
|
786
812
|
tableId?: undefined;
|
|
787
813
|
campaignName?: undefined;
|
|
788
814
|
keepInSync?: undefined;
|
|
@@ -860,6 +886,8 @@ export declare const leadToolDefinitions: ({
|
|
|
860
886
|
sourceLeadListId?: undefined;
|
|
861
887
|
targetLeadCount?: undefined;
|
|
862
888
|
mode?: undefined;
|
|
889
|
+
targetEngagerCount?: undefined;
|
|
890
|
+
maxPostsToScrape?: undefined;
|
|
863
891
|
tableId?: undefined;
|
|
864
892
|
campaignName?: undefined;
|
|
865
893
|
keepInSync?: undefined;
|
|
@@ -1475,6 +1503,8 @@ export declare const leadToolDefinitions: ({
|
|
|
1475
1503
|
sourceLeadListId?: undefined;
|
|
1476
1504
|
targetLeadCount?: undefined;
|
|
1477
1505
|
mode?: undefined;
|
|
1506
|
+
targetEngagerCount?: undefined;
|
|
1507
|
+
maxPostsToScrape?: undefined;
|
|
1478
1508
|
tableId?: undefined;
|
|
1479
1509
|
campaignName?: undefined;
|
|
1480
1510
|
keepInSync?: undefined;
|
|
@@ -1604,6 +1634,8 @@ export declare const leadToolDefinitions: ({
|
|
|
1604
1634
|
sourceLeadListId?: undefined;
|
|
1605
1635
|
targetLeadCount?: undefined;
|
|
1606
1636
|
mode?: undefined;
|
|
1637
|
+
targetEngagerCount?: undefined;
|
|
1638
|
+
maxPostsToScrape?: undefined;
|
|
1607
1639
|
tableId?: undefined;
|
|
1608
1640
|
campaignName?: undefined;
|
|
1609
1641
|
keepInSync?: undefined;
|
|
@@ -1666,6 +1698,14 @@ export declare const leadToolDefinitions: ({
|
|
|
1666
1698
|
minItems?: undefined;
|
|
1667
1699
|
maxItems?: undefined;
|
|
1668
1700
|
};
|
|
1701
|
+
targetEngagerCount: {
|
|
1702
|
+
type: string;
|
|
1703
|
+
description: string;
|
|
1704
|
+
};
|
|
1705
|
+
maxPostsToScrape: {
|
|
1706
|
+
type: string;
|
|
1707
|
+
description: string;
|
|
1708
|
+
};
|
|
1669
1709
|
rubricGuidelines: {
|
|
1670
1710
|
type: string;
|
|
1671
1711
|
items: {
|
|
@@ -1791,6 +1831,8 @@ export declare const leadToolDefinitions: ({
|
|
|
1791
1831
|
sourceLeadListId?: undefined;
|
|
1792
1832
|
targetLeadCount?: undefined;
|
|
1793
1833
|
mode?: undefined;
|
|
1834
|
+
targetEngagerCount?: undefined;
|
|
1835
|
+
maxPostsToScrape?: undefined;
|
|
1794
1836
|
campaignName?: undefined;
|
|
1795
1837
|
keepInSync?: undefined;
|
|
1796
1838
|
jobId?: undefined;
|
|
@@ -1882,6 +1924,8 @@ export declare const leadToolDefinitions: ({
|
|
|
1882
1924
|
headlineICPCriteria?: undefined;
|
|
1883
1925
|
rubricGuidelines?: undefined;
|
|
1884
1926
|
mode?: undefined;
|
|
1927
|
+
targetEngagerCount?: undefined;
|
|
1928
|
+
maxPostsToScrape?: undefined;
|
|
1885
1929
|
tableId?: undefined;
|
|
1886
1930
|
selections?: undefined;
|
|
1887
1931
|
selectionMode?: undefined;
|
|
@@ -1983,6 +2027,8 @@ export declare const leadToolDefinitions: ({
|
|
|
1983
2027
|
sourceLeadListId?: undefined;
|
|
1984
2028
|
targetLeadCount?: undefined;
|
|
1985
2029
|
mode?: undefined;
|
|
2030
|
+
targetEngagerCount?: undefined;
|
|
2031
|
+
maxPostsToScrape?: undefined;
|
|
1986
2032
|
tableId?: undefined;
|
|
1987
2033
|
campaignName?: undefined;
|
|
1988
2034
|
keepInSync?: undefined;
|
|
@@ -2060,6 +2106,8 @@ export declare const leadToolDefinitions: ({
|
|
|
2060
2106
|
sourceLeadListId?: undefined;
|
|
2061
2107
|
targetLeadCount?: undefined;
|
|
2062
2108
|
mode?: undefined;
|
|
2109
|
+
targetEngagerCount?: undefined;
|
|
2110
|
+
maxPostsToScrape?: undefined;
|
|
2063
2111
|
tableId?: undefined;
|
|
2064
2112
|
campaignName?: undefined;
|
|
2065
2113
|
keepInSync?: undefined;
|
|
@@ -2318,6 +2366,8 @@ export declare function importLeads(input: ImportLeadsInput): Promise<{
|
|
|
2318
2366
|
searchName?: string;
|
|
2319
2367
|
leadListName?: string;
|
|
2320
2368
|
headlineICPCriteria?: string[];
|
|
2369
|
+
targetEngagerCount?: number;
|
|
2370
|
+
maxPostsToScrape?: number;
|
|
2321
2371
|
rubricGuidelines?: string[];
|
|
2322
2372
|
};
|
|
2323
2373
|
}[];
|
|
@@ -2328,6 +2378,10 @@ export declare function importLeads(input: ImportLeadsInput): Promise<{
|
|
|
2328
2378
|
jobId?: undefined;
|
|
2329
2379
|
estimatedEngagers?: undefined;
|
|
2330
2380
|
selectedPostCount?: undefined;
|
|
2381
|
+
availableSelectedPostCount?: undefined;
|
|
2382
|
+
targetEngagerCount?: undefined;
|
|
2383
|
+
maxPostsToScrape?: undefined;
|
|
2384
|
+
limitedSelectedPosts?: undefined;
|
|
2331
2385
|
targetLeadCount?: undefined;
|
|
2332
2386
|
existingCount?: undefined;
|
|
2333
2387
|
createdLeadList?: undefined;
|
|
@@ -2353,6 +2407,8 @@ export declare function importLeads(input: ImportLeadsInput): Promise<{
|
|
|
2353
2407
|
searchName?: string;
|
|
2354
2408
|
leadListName?: string;
|
|
2355
2409
|
headlineICPCriteria?: string[];
|
|
2410
|
+
targetEngagerCount?: number;
|
|
2411
|
+
maxPostsToScrape?: number;
|
|
2356
2412
|
rubricGuidelines?: string[];
|
|
2357
2413
|
};
|
|
2358
2414
|
}[];
|
|
@@ -2360,6 +2416,10 @@ export declare function importLeads(input: ImportLeadsInput): Promise<{
|
|
|
2360
2416
|
jobId?: undefined;
|
|
2361
2417
|
estimatedEngagers?: undefined;
|
|
2362
2418
|
selectedPostCount?: undefined;
|
|
2419
|
+
availableSelectedPostCount?: undefined;
|
|
2420
|
+
targetEngagerCount?: undefined;
|
|
2421
|
+
maxPostsToScrape?: undefined;
|
|
2422
|
+
limitedSelectedPosts?: undefined;
|
|
2363
2423
|
targetLeadCount?: undefined;
|
|
2364
2424
|
existingCount?: undefined;
|
|
2365
2425
|
createdLeadList?: undefined;
|
|
@@ -2370,6 +2430,10 @@ export declare function importLeads(input: ImportLeadsInput): Promise<{
|
|
|
2370
2430
|
jobId: string;
|
|
2371
2431
|
estimatedEngagers: number;
|
|
2372
2432
|
selectedPostCount: number;
|
|
2433
|
+
availableSelectedPostCount: number;
|
|
2434
|
+
targetEngagerCount: number | null;
|
|
2435
|
+
maxPostsToScrape: number | null;
|
|
2436
|
+
limitedSelectedPosts: boolean;
|
|
2373
2437
|
targetLeadCount: number | null;
|
|
2374
2438
|
message: string;
|
|
2375
2439
|
error?: undefined;
|
|
@@ -2402,6 +2466,8 @@ export declare function importLeads(input: ImportLeadsInput): Promise<{
|
|
|
2402
2466
|
searchName?: string;
|
|
2403
2467
|
leadListName?: string;
|
|
2404
2468
|
headlineICPCriteria?: string[];
|
|
2469
|
+
targetEngagerCount?: number;
|
|
2470
|
+
maxPostsToScrape?: number;
|
|
2405
2471
|
rubricGuidelines?: string[];
|
|
2406
2472
|
};
|
|
2407
2473
|
}[];
|
|
@@ -2409,6 +2475,10 @@ export declare function importLeads(input: ImportLeadsInput): Promise<{
|
|
|
2409
2475
|
jobId?: undefined;
|
|
2410
2476
|
estimatedEngagers?: undefined;
|
|
2411
2477
|
selectedPostCount?: undefined;
|
|
2478
|
+
availableSelectedPostCount?: undefined;
|
|
2479
|
+
targetEngagerCount?: undefined;
|
|
2480
|
+
maxPostsToScrape?: undefined;
|
|
2481
|
+
limitedSelectedPosts?: undefined;
|
|
2412
2482
|
targetLeadCount?: undefined;
|
|
2413
2483
|
createdLeadList?: undefined;
|
|
2414
2484
|
jobResult?: undefined;
|
|
@@ -2427,6 +2497,10 @@ export declare function importLeads(input: ImportLeadsInput): Promise<{
|
|
|
2427
2497
|
modeOptions?: undefined;
|
|
2428
2498
|
estimatedEngagers?: undefined;
|
|
2429
2499
|
selectedPostCount?: undefined;
|
|
2500
|
+
availableSelectedPostCount?: undefined;
|
|
2501
|
+
targetEngagerCount?: undefined;
|
|
2502
|
+
maxPostsToScrape?: undefined;
|
|
2503
|
+
limitedSelectedPosts?: undefined;
|
|
2430
2504
|
existingCount?: undefined;
|
|
2431
2505
|
}>;
|
|
2432
2506
|
export declare function cancelLeadImport(input: CancelLeadImportInput): Promise<{
|
package/dist/tools/leads.js
CHANGED
|
@@ -265,6 +265,46 @@ export function normalizeTargetLeadCount(targetLeadCount, maxImportCount) {
|
|
|
265
265
|
}
|
|
266
266
|
return Math.min(numeric, maxImportCount);
|
|
267
267
|
}
|
|
268
|
+
function normalizePositiveInteger(value) {
|
|
269
|
+
if (value === null || value === undefined) {
|
|
270
|
+
return undefined;
|
|
271
|
+
}
|
|
272
|
+
const numeric = typeof value === "number" ? value : Number(value);
|
|
273
|
+
if (!Number.isFinite(numeric) || numeric <= 0) {
|
|
274
|
+
return undefined;
|
|
275
|
+
}
|
|
276
|
+
return Math.floor(numeric);
|
|
277
|
+
}
|
|
278
|
+
export function selectSignalPostsForImport(posts, options) {
|
|
279
|
+
const normalizedTargetEngagers = normalizePositiveInteger(options.targetEngagerCount);
|
|
280
|
+
const normalizedMaxPosts = normalizePositiveInteger(options.maxPostsToScrape);
|
|
281
|
+
if (!normalizedTargetEngagers && !normalizedMaxPosts) {
|
|
282
|
+
return {
|
|
283
|
+
posts,
|
|
284
|
+
estimatedEngagers: posts.reduce((sum, post) => sum + post.likes + post.comments, 0),
|
|
285
|
+
limited: false,
|
|
286
|
+
};
|
|
287
|
+
}
|
|
288
|
+
const ranked = [...posts].sort((a, b) => b.likes + b.comments - (a.likes + a.comments));
|
|
289
|
+
const selected = [];
|
|
290
|
+
let estimatedEngagers = 0;
|
|
291
|
+
for (const post of ranked) {
|
|
292
|
+
if (normalizedMaxPosts && selected.length >= normalizedMaxPosts) {
|
|
293
|
+
break;
|
|
294
|
+
}
|
|
295
|
+
selected.push(post);
|
|
296
|
+
estimatedEngagers += post.likes + post.comments;
|
|
297
|
+
if (normalizedTargetEngagers &&
|
|
298
|
+
estimatedEngagers >= normalizedTargetEngagers) {
|
|
299
|
+
break;
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
return {
|
|
303
|
+
posts: selected.length > 0 ? selected : ranked,
|
|
304
|
+
estimatedEngagers,
|
|
305
|
+
limited: selected.length > 0 && selected.length < posts.length,
|
|
306
|
+
};
|
|
307
|
+
}
|
|
268
308
|
function toDate(value) {
|
|
269
309
|
if (!value)
|
|
270
310
|
return null;
|
|
@@ -967,6 +1007,14 @@ export const leadToolDefinitions = [
|
|
|
967
1007
|
items: { type: "string" },
|
|
968
1008
|
description: "Signal Discovery: headline ICP criteria (3-5 rules). Uses campaign.headlineICPCriteria if not provided.",
|
|
969
1009
|
},
|
|
1010
|
+
targetEngagerCount: {
|
|
1011
|
+
type: "number",
|
|
1012
|
+
description: "Signal Discovery: target number of post engagers to scrape. Compute from source target good-fit leads / sampled fit rate; e.g. 300 good fits at 20% fit requires about 1500 engagers. Limits selected posts before starting scrape.",
|
|
1013
|
+
},
|
|
1014
|
+
maxPostsToScrape: {
|
|
1015
|
+
type: "number",
|
|
1016
|
+
description: "Signal Discovery: optional hard cap on selected posts to scrape after ranking selected posts by engagement.",
|
|
1017
|
+
},
|
|
970
1018
|
rubricGuidelines: {
|
|
971
1019
|
type: "array",
|
|
972
1020
|
items: { type: "string" },
|
|
@@ -1845,7 +1893,7 @@ export async function searchSignals(input) {
|
|
|
1845
1893
|
}
|
|
1846
1894
|
export async function importLeads(input) {
|
|
1847
1895
|
const api = getApi();
|
|
1848
|
-
const { campaignOfferId, currentStep, sourceLeadListId: inputSourceLeadListId, searchId, targetLeadCount, mode, searchName, leadListName, headlineICPCriteria, rubricGuidelines, confirmed, } = input;
|
|
1896
|
+
const { campaignOfferId, currentStep, sourceLeadListId: inputSourceLeadListId, searchId, targetLeadCount, mode, searchName, leadListName, headlineICPCriteria, targetEngagerCount, maxPostsToScrape, rubricGuidelines, confirmed, } = input;
|
|
1849
1897
|
assertInteractionApproval({
|
|
1850
1898
|
campaignId: campaignOfferId,
|
|
1851
1899
|
action: "import-leads",
|
|
@@ -1989,12 +2037,17 @@ export async function importLeads(input) {
|
|
|
1989
2037
|
}
|
|
1990
2038
|
}
|
|
1991
2039
|
const uniqueSelectedPosts = Array.from(uniqueByUrl.values());
|
|
2040
|
+
const importSelection = selectSignalPostsForImport(uniqueSelectedPosts, {
|
|
2041
|
+
targetEngagerCount,
|
|
2042
|
+
maxPostsToScrape,
|
|
2043
|
+
});
|
|
2044
|
+
const postsToScrape = importSelection.posts;
|
|
1992
2045
|
const effectiveHeadlineICPCriteria = headlineICPCriteria && headlineICPCriteria.length > 0
|
|
1993
2046
|
? headlineICPCriteria
|
|
1994
2047
|
: rubricGuidelines;
|
|
1995
2048
|
// Start the scrape job
|
|
1996
2049
|
const result = await api.post(`/api/v3/campaigns/${campaignOfferId}/signal-leads/create`, {
|
|
1997
|
-
posts:
|
|
2050
|
+
posts: postsToScrape,
|
|
1998
2051
|
...(effectiveHeadlineICPCriteria &&
|
|
1999
2052
|
effectiveHeadlineICPCriteria.length > 0
|
|
2000
2053
|
? {
|
|
@@ -2014,9 +2067,15 @@ export async function importLeads(input) {
|
|
|
2014
2067
|
leadListId: result.tableId,
|
|
2015
2068
|
jobId: result.jobId,
|
|
2016
2069
|
estimatedEngagers: result.estimatedEngagers,
|
|
2017
|
-
selectedPostCount:
|
|
2070
|
+
selectedPostCount: postsToScrape.length,
|
|
2071
|
+
availableSelectedPostCount: uniqueSelectedPosts.length,
|
|
2072
|
+
targetEngagerCount: normalizePositiveInteger(targetEngagerCount) ?? null,
|
|
2073
|
+
maxPostsToScrape: normalizePositiveInteger(maxPostsToScrape) ?? null,
|
|
2074
|
+
limitedSelectedPosts: importSelection.limited,
|
|
2018
2075
|
targetLeadCount: cappedTargetLeadCount ?? null,
|
|
2019
|
-
message: `Started scraping ${
|
|
2076
|
+
message: `Started scraping ${postsToScrape.length} posts (~${result.estimatedEngagers} engagers). Leads will appear as scraping completes.${importSelection.limited
|
|
2077
|
+
? ` Limited from ${uniqueSelectedPosts.length} selected posts by the approved source-capacity scrape plan.`
|
|
2078
|
+
: ""}`,
|
|
2020
2079
|
};
|
|
2021
2080
|
}
|
|
2022
2081
|
// === SALES NAV / PROSPEO FLOW ===
|
|
@@ -2449,7 +2508,7 @@ export async function selectPromisingPosts(input) {
|
|
|
2449
2508
|
selectedCount: selectionResult.selectedCount,
|
|
2450
2509
|
unselectedCount: selectionResult.unselectedCount,
|
|
2451
2510
|
criteriaCount: selectionResult.criteriaCount,
|
|
2452
|
-
message: `Selected ${selectionResult.selectedCount} posts with ${selectionResult.criteriaCount} ICP criteria (persisted). Criteria are already saved to the campaign. If the user has already approved the lead-source decision for create-campaign-v2, call import_leads with provider: "signal-discovery", confirmed: true,
|
|
2511
|
+
message: `Selected ${selectionResult.selectedCount} posts with ${selectionResult.criteriaCount} ICP criteria (persisted). Criteria are already saved to the campaign. If the user has already approved the lead-source decision for create-campaign-v2, call import_leads with provider: "signal-discovery", confirmed: true, the approved source-capacity targetLeadCount, and targetEngagerCount/postsNeeded from the approval math. Otherwise ask the user to confirm before importing.`,
|
|
2453
2512
|
};
|
|
2454
2513
|
}
|
|
2455
2514
|
export async function setHeadlineICPCriteria(input) {
|
package/package.json
CHANGED
|
@@ -826,7 +826,9 @@ Required behavior:
|
|
|
826
826
|
2. If Signals does not have enough recent, relevant, ICP-looking engagers,
|
|
827
827
|
switch to Sales Nav with recent activity when the target can be expressed
|
|
828
828
|
as title/persona/company filters. Run preview filters, inspect preview rows,
|
|
829
|
-
validate the filters applied, and estimate scalable-fit volume.
|
|
829
|
+
validate the filters applied, and estimate scalable-fit volume. If the
|
|
830
|
+
preview/projected usable pool is below the target good-fit count, loosen
|
|
831
|
+
nonessential filters before presenting Sales Nav as the scale fallback.
|
|
830
832
|
3. If recent-activity Sales Nav is too small or noisy, broaden to normal Sales
|
|
831
833
|
Nav title + company filters and call out the weaker activity context.
|
|
832
834
|
4. Use Prospeo Contact only when the campaign has a domain/account path, the
|
|
@@ -877,10 +879,13 @@ Required behavior:
|
|
|
877
879
|
broad anchor-only lanes as fallback inventory only, and retarget before
|
|
878
880
|
sampling if the first pass is broad.
|
|
879
881
|
- Signals viability is based on estimated ICP-fit reachable engagers, not raw
|
|
880
|
-
post count and not whether it beats Sales Nav scale.
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
882
|
+
post count and not whether it beats Sales Nav scale. Keep source-capacity
|
|
883
|
+
planning separate from the 15-row review-batch import: the default source
|
|
884
|
+
target is 300 likely good-fit leads, while `import.importLimit` remains 15 for
|
|
885
|
+
the first campaign review batch. If 5-8 selected posts can plausibly produce
|
|
886
|
+
~150+ ICP-fit warm prospects before final filtering, keep Signals as a viable
|
|
887
|
+
focused option, but say it is below the default 300-lead source target unless
|
|
888
|
+
the math shows otherwise.
|
|
884
889
|
- When Signals is viable-but-smaller and Sales Nav or Prospeo is more scalable,
|
|
885
890
|
the user-facing lead review must present the choice: Signals for a warmer,
|
|
886
891
|
smaller, higher-reply-upside first batch versus Sales Nav/Prospeo for broader
|
|
@@ -901,8 +906,13 @@ fit x cleanup factor = ~300-500 likely usable warm leads`.
|
|
|
901
906
|
last 30 days, prefer the last 7-14 days when quality is comparable, and call
|
|
902
907
|
out any older post as a deliberate tradeoff. Do not hide post age inside the
|
|
903
908
|
raw search count
|
|
904
|
-
- default source quality target is
|
|
905
|
-
outbound unless the campaign or user supplies a different target.
|
|
909
|
+
- default source quality target is 300 likely good-fit leads for scalable
|
|
910
|
+
outbound unless the campaign or user supplies a different source target.
|
|
911
|
+
Compute required engagers from the observed fit rate: `target good-fit leads /
|
|
912
|
+
sampled fit rate`. For example, 20 good fits per 100 engagers means a 300
|
|
913
|
+
good-fit source target needs about 1,500 engagers scraped; use average
|
|
914
|
+
reachable engagers per right-content post to calculate how many posts are
|
|
915
|
+
needed. Accept
|
|
906
916
|
~150+ likely ICP-fit warm prospects as viable for a focused Signals-first
|
|
907
917
|
campaign or first review batch only when the user is choosing warmth over
|
|
908
918
|
scale. Name the volume tradeoff in `lead-review.md` and the source approval
|
|
@@ -1025,8 +1035,10 @@ not a research memo. It must include only:
|
|
|
1025
1035
|
activity filters. For Signals, name the selected post set. For Prospeo, name
|
|
1026
1036
|
the account/domain and title recipe.
|
|
1027
1037
|
- `Math:` with these labels and values:
|
|
1028
|
-
`Eligible posts`, `Sample`, `ICP-fit`, `
|
|
1029
|
-
`
|
|
1038
|
+
`Eligible posts`, `Sample`, `ICP-fit`, `Good-fit / 100 engagers`,
|
|
1039
|
+
`Required engagers`, `Avg engagers/post`, `Good-fit / post`, `Source target`,
|
|
1040
|
+
`Posts needed`, `Selected`, `Review batch`, `Expected good-fit leads`, and
|
|
1041
|
+
`Scale option`.
|
|
1030
1042
|
- `Why this source:` with at most two bullets.
|
|
1031
1043
|
- `Watch link: {watchUrl}` only when the campaign shell exists and the host
|
|
1032
1044
|
needs a handoff link.
|
|
@@ -1036,14 +1048,22 @@ Do not include keyword-lane tables, LinkedIn-post-sampled tables, sample-lead
|
|
|
1036
1048
|
lists, or long tradeoff prose in the default approval packet. Keep those details
|
|
1037
1049
|
in `lead-review.md` or campaign/source decision state. The math block is the
|
|
1038
1050
|
bottom-line arithmetic that makes the recommendation obvious. For example:
|
|
1039
|
-
`Eligible posts:
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1051
|
+
`Eligible posts: 12 outbound + Claude posts. Sample: 25/125 ICP fits. ICP-fit:
|
|
1052
|
+
~20%, directional. Good-fit / 100 engagers: ~20 before cleanup. Required
|
|
1053
|
+
engagers: ~1,500 to reach 300 good-fit prospects. Avg engagers/post: ~300
|
|
1054
|
+
reachable. Good-fit / post: ~60. Source target: 300 good-fit leads. Posts
|
|
1055
|
+
needed: about 5 right-content posts. Selected: 5 posts. Review batch: import
|
|
1056
|
+
only 15 campaign rows first. Expected good-fit leads: ~300 before stricter
|
|
1057
|
+
filtering. Scale option: Sales Nav can broaden to founder/GTM/RevOps filters if
|
|
1058
|
+
you want colder scale, but loses the engaged-with-this-content message hook.` If
|
|
1059
|
+
the selected posts only support the 15-row review batch or a smaller warm pilot,
|
|
1060
|
+
say that plainly and do not imply the lane can hit the source target. For
|
|
1061
|
+
Sales Nav/Prospeo
|
|
1062
|
+
alternatives, include preview/raw volume, sampled usable rows as `n/N` plus
|
|
1063
|
+
percentage/range, estimated usable range, and the warmth or context tradeoff.
|
|
1064
|
+
If Sales Nav preview volume is too tight, broaden nonessential filters before
|
|
1065
|
+
using it as the scale option, or label it constrained and name the next fallback.
|
|
1066
|
+
Do not use percent-only fit rates or unsupported reply-rate claims.
|
|
1047
1067
|
|
|
1048
1068
|
The first sentence of the visible decision must make the actual choice clear:
|
|
1049
1069
|
`I recommend {primary source} using {exact filter/source recipe}. The runner-up
|
|
@@ -1091,12 +1111,16 @@ For Signals, default to a quick capacity test before final recommendation. Pick
|
|
|
1091
1111
|
a narrow first sample of fresh, high-density posts, and in campaign-attached
|
|
1092
1112
|
watch runs promote those posts with `select_promising_posts` before sampling so
|
|
1093
1113
|
the user sees which posts are being tested. Then sample engagers, show ICP-fit
|
|
1094
|
-
rate as `n/N` plus percentage/range,
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1114
|
+
rate as `n/N` plus percentage/range, convert that to good-fit prospects per 100
|
|
1115
|
+
engagers, estimate average reachable engagers per right-content post, apply a
|
|
1116
|
+
conservative dedupe/cleanup factor, calculate required engagers to scrape,
|
|
1117
|
+
calculate good-fit prospects per post, then calculate how many right-content
|
|
1118
|
+
posts are needed to reach the source target good-fit lead count. Select/promote
|
|
1119
|
+
only enough right-content posts to hit that source target; do not scrape every
|
|
1120
|
+
promoted sample post by default. If the first sample is good but volume is low,
|
|
1121
|
+
say how many more posts would be needed and offer the Sales Nav/Prospeo scale
|
|
1122
|
+
alternative instead of recommending an under-sized source as if it can produce
|
|
1123
|
+
the default 300 good-fit source target.
|
|
1100
1124
|
|
|
1101
1125
|
Keep discarded paths and full sample rows in the campaign/source decision
|
|
1102
1126
|
context. In explicit debug/UAT runs they may also be copied into
|
|
@@ -525,7 +525,7 @@
|
|
|
525
525
|
"suppliedProfilesOrCsv": "supplied-list",
|
|
526
526
|
"explicitCompare": "compare-requested-sources"
|
|
527
527
|
},
|
|
528
|
-
"quickViabilityRule": "Run only enough of the current lane to decide whether it can supply relevant, reachable ICP-looking leads. For Signals, viability requires sampled ICP-fit rate
|
|
528
|
+
"quickViabilityRule": "Run only enough of the current lane to decide whether it can supply relevant, reachable ICP-looking leads. For Signals, viability requires sampled ICP-fit rate normalized to good-fit prospects per 100 engagers, required engagers to scrape, average reachable engagers per right-content post, projected good-fit prospects per post after cleanup, and posts-needed math against the 300-good-fit source target, not raw post count or the 15-row review-batch import limit. Stop on the first viable source unless the user asked for comparison.",
|
|
529
529
|
"parallelAllowedOnlyWhen": [
|
|
530
530
|
"user explicitly requested source comparison",
|
|
531
531
|
"resuming already-started parallel scouts",
|
|
@@ -568,7 +568,7 @@
|
|
|
568
568
|
},
|
|
569
569
|
"when": "after_source_selection_checkpoint_before_first_source_attempt",
|
|
570
570
|
"requiresPriorVisibleStep": "pick-provider",
|
|
571
|
-
"rule": "Choose the first visible source lane from explicit source direction when present, otherwise default to Signal Discovery. Only run this after the Pick Provider source-selection checkpoint has been made visible and explained in chat/watchNarration. Send watchNarration with stage find-leads that says why this lane is being tried now, what quick sample or filter gate will pass/fail it, and why that helps this campaign. For Signal Discovery sampling, make it clear this is a viability path before lead import: generate intersection-first keyword lanes that combine the campaign anchor with the buyer pain/use case/ICP role, not broad anchor-only lanes. For Claude/GTM campaigns, prefer lanes like Claude outbound, Claude Code LinkedIn outreach, AI SDR Claude, GTM automation Claude, and founder-led sales Claude over generic Claude Code, MCP, AI agents, or agentic coding. If the first pass is broad, retarget around the campaign wedge before selecting posts. Promote/select the first narrow sample posts with select_promising_posts before fetch_post_engagers so the watched table shows the exact posts being sampled; the guide copy should say Codex is pulling sample engagers from these posts to confirm the ICP is actually engaging and estimate whether enough right-content posts exist for the
|
|
571
|
+
"rule": "Choose the first visible source lane from explicit source direction when present, otherwise default to Signal Discovery. Only run this after the Pick Provider source-selection checkpoint has been made visible and explained in chat/watchNarration. Send watchNarration with stage find-leads that says why this lane is being tried now, what quick sample or filter gate will pass/fail it, and why that helps this campaign. For Signal Discovery sampling, make it clear this is a viability path before lead import: generate intersection-first keyword lanes that combine the campaign anchor with the buyer pain/use case/ICP role, not broad anchor-only lanes. For Claude/GTM campaigns, prefer lanes like Claude outbound, Claude Code LinkedIn outreach, AI SDR Claude, GTM automation Claude, and founder-led sales Claude over generic Claude Code, MCP, AI agents, or agentic coding. If the first pass is broad, retarget around the campaign wedge before selecting posts. Promote/select the first narrow sample posts with select_promising_posts before fetch_post_engagers so the watched table shows the exact posts being sampled; the guide copy should say Codex is pulling sample engagers from these posts to confirm the ICP is actually engaging and estimate whether enough right-content posts exist for the 300-good-fit source target. Before final source approval, compute eligible posts, sampled ICP-fit rate, good-fit prospects per 100 engagers, required engagers to scrape, average reachable engagers per right-content post, expected good-fit prospects per post after cleanup, source target good-fit lead count, posts needed, selected posts, expected good-fit range, and scale fallback. If the lane fails, update currentStep and watchNarration before moving to Sales Nav recent activity, normal Sales Nav, or Prospeo."
|
|
572
572
|
},
|
|
573
573
|
{
|
|
574
574
|
"action": "run_first_campaign_attached_source_search",
|
|
@@ -617,7 +617,7 @@
|
|
|
617
617
|
"action": "run_subskill",
|
|
618
618
|
"target": "find-leads",
|
|
619
619
|
"mode": "campaign-attached-required",
|
|
620
|
-
"sourceScoutRule": "Shell-first flow requires the CampaignOffer campaignId from durable state. Pass campaignId as campaignOfferId into every provider prompt/search that can persist source state (`get_provider_prompt({ provider, campaignOfferId, confirmed: true })`, `search_signals`, `search_sales_nav`, `search_prospeo`) and include currentStep for tools that accept it so the user can watch the selected source inside the campaign. Use the default sequential source viability funnel when the user did not specify a source: Signal Discovery first, then Sales Nav with recent activity, then general Sales Nav, then Prospeo only as fallback. For Signal Discovery, use intersection-first keyword lanes that combine the campaign anchor with the buyer pain/use case/ICP role; broad anchor-only lanes are fallback inventory, not selectable source lanes while narrower wedge lanes exist. Do not recommend from raw post count; sample engagers, calculate ICP-fit rate, target good-fit lead count, posts needed, selected posts, expected good-fit range, and scale fallback. Stop on the first viable source unless the user explicitly asked to compare. If the user names hiring signals, domains/accounts, supplied lists, posts/comments, or title/persona filters, start with the matching source instead. Parallel source scouts only when the user requested comparison, an existing parallel run is being resumed, or the first viable source is borderline and one cheap fallback check is needed. The later import_leads call must use the same campaignOfferId. Do not import, confirm, enrich, queue, or start leads during source discovery."
|
|
620
|
+
"sourceScoutRule": "Shell-first flow requires the CampaignOffer campaignId from durable state. Pass campaignId as campaignOfferId into every provider prompt/search that can persist source state (`get_provider_prompt({ provider, campaignOfferId, confirmed: true })`, `search_signals`, `search_sales_nav`, `search_prospeo`) and include currentStep for tools that accept it so the user can watch the selected source inside the campaign. Use the default sequential source viability funnel when the user did not specify a source: Signal Discovery first, then Sales Nav with recent activity, then general Sales Nav, then Prospeo only as fallback. For Signal Discovery, use intersection-first keyword lanes that combine the campaign anchor with the buyer pain/use case/ICP role; broad anchor-only lanes are fallback inventory, not selectable source lanes while narrower wedge lanes exist. Do not recommend from raw post count; sample engagers, calculate ICP-fit rate, good-fit prospects per 100 engagers, required engagers to scrape, average reachable engagers per right-content post, expected good-fit prospects per post after cleanup, source target good-fit lead count (default 300), posts needed, selected posts, expected good-fit range, and scale fallback. For Sales Nav fallback, do not over-tighten filters into a tiny pool; if preview/projected usable volume is below the target, loosen nonessential filters before presenting Sales Nav as the scale option and label it constrained if it still cannot reach the target. Stop on the first viable source unless the user explicitly asked to compare. If the user names hiring signals, domains/accounts, supplied lists, posts/comments, or title/persona filters, start with the matching source instead. Parallel source scouts only when the user requested comparison, an existing parallel run is being resumed, or the first viable source is borderline and one cheap fallback check is needed. The later import_leads call must use the same campaignOfferId and, for Signal Discovery, targetEngagerCount from the approved math so it does not scrape every selected sample post by default. Do not import, confirm, enrich, queue, or start leads during source discovery."
|
|
621
621
|
},
|
|
622
622
|
{
|
|
623
623
|
"action": "optional_debug_artifacts",
|
|
@@ -720,9 +720,14 @@
|
|
|
720
720
|
"Eligible posts",
|
|
721
721
|
"Sample",
|
|
722
722
|
"ICP-fit",
|
|
723
|
-
"
|
|
723
|
+
"Good-fit / 100 engagers",
|
|
724
|
+
"Required engagers",
|
|
725
|
+
"Avg engagers/post",
|
|
726
|
+
"Good-fit / post",
|
|
727
|
+
"Source target",
|
|
724
728
|
"Posts needed",
|
|
725
729
|
"Selected",
|
|
730
|
+
"Review batch",
|
|
726
731
|
"Expected good-fit leads",
|
|
727
732
|
"Scale option",
|
|
728
733
|
"Why this source"
|
|
@@ -764,13 +769,20 @@
|
|
|
764
769
|
"reachable engagers count",
|
|
765
770
|
"sampled engagers count",
|
|
766
771
|
"sampled ICP fits as n/N plus percentage or range",
|
|
767
|
-
"
|
|
772
|
+
"good-fit prospects per 100 engagers",
|
|
773
|
+
"required engagers to scrape",
|
|
774
|
+
"average reachable engagers per post",
|
|
775
|
+
"expected good-fit prospects per post after cleanup",
|
|
776
|
+
"dedupe or cleanup factor",
|
|
777
|
+
"source target good-fit lead count default 300",
|
|
768
778
|
"posts needed for target",
|
|
769
779
|
"selected posts count",
|
|
780
|
+
"15-row review-batch import limit",
|
|
770
781
|
"estimated usable prospects range",
|
|
771
782
|
"scale alternative source",
|
|
772
783
|
"scale alternative raw or preview volume",
|
|
773
784
|
"scale alternative sampled usable rows as n/N plus percentage or range",
|
|
785
|
+
"Sales Nav broadening or loosening attempts when fallback volume is too tight",
|
|
774
786
|
"warmth or message-context tradeoff",
|
|
775
787
|
"what to choose if the user wants more volume"
|
|
776
788
|
],
|
|
@@ -794,7 +806,7 @@
|
|
|
794
806
|
"doNotCompressToSummaryOnly": false,
|
|
795
807
|
"doNotRenderArtifactLinksOnly": true,
|
|
796
808
|
"sourceRecommendationReadyWatchRule": "When the source recommendation decision card is ready in chat with counts and sample quality, and before asking for source approval, call update_campaign with leadSourceType `new`, leadSourceProvider set to the recommended primary provider, currentStep set to that provider lane (`sales-nav`, `signal-discovery`, or `prospeo`), and find-leads watchNarration. If the recommendation changed from the lane last sampled, switch the watched provider page to the recommended lane first so the user can inspect what they are approving. Use a headline like `Review the source in Codex`, body copy that says the browser is showing the evaluated source/results, and nextAction like `Approve in Codex`. Do not keep future-tense copy like `I'll show a source recommendation` after the decision is visible. Include a safety note that no leads import until the user approves the source.",
|
|
797
|
-
"chatRenderRule": "Show a compact rendered-Markdown decision card only, never a fenced code block. The first sentence must make the decision explicit: 'I recommend {primary source} using {exact filter/source recipe}. The runner-up is {source} because {reason}.' Use only these sections: Lead source decision, Source recommendation, Math, Why this source, and the concrete approval question. The Math section must include labeled lines for Eligible posts, Sample, ICP-fit,
|
|
809
|
+
"chatRenderRule": "Show a compact rendered-Markdown decision card only, never a fenced code block. The first sentence must make the decision explicit: 'I recommend {primary source} using {exact filter/source recipe}. The runner-up is {source} because {reason}.' Use only these sections: Lead source decision, Source recommendation, Math, Why this source, and the concrete approval question. The Math section must include labeled lines for Eligible posts, Sample, ICP-fit, Good-fit / 100 engagers, Required engagers, Avg engagers/post, Good-fit / post, Source target, Posts needed, Selected, Review batch, Expected good-fit leads, and Scale option. Show fit as n/N plus an easy percentage or range, never percent-only. Do not include keyword-lane tables, LinkedIn-post-sampled tables, sample-lead lists, or long tradeoff prose in the default approval packet; keep those details in lead-review.md or campaign/source decision state. Do not forecast connection acceptance rates, reply rates, meetings, pipeline, revenue, or ROI unless the user supplied verified benchmark data for this exact workspace/sender. For Signals, do not recommend from raw post count. Compute eligible right-content posts, reachable engagers, sampled ICP-fit rate, good-fit prospects per 100 engagers, required engagers to scrape, average reachable engagers per right-content post, expected good-fit prospects per post after cleanup, source target good-fit lead count defaulting to 300, posts needed, selected posts, expected good-fit range, and scale fallback. Distinguish source-capacity math from the 15-row review-batch import limit. If the selected warm posts only support a review batch or smaller pilot, say they do not hit 300 instead of calling them scalable. If the user wants more volume than the warm post-engagement estimate, explicitly say which Sales Nav or Prospeo alternative provides that volume and what message-context warmth is lost; Sales Nav must be broadened/loosened before it is presented as the scale option if the initial filter pool is too tight. If no engagers could be fetched, say that explicitly and lower confidence. Do not show plain filesystem paths unless links cannot be created."
|
|
798
810
|
},
|
|
799
811
|
{
|
|
800
812
|
"action": "render_post_lead_parallel_progress",
|
|
@@ -816,7 +828,7 @@
|
|
|
816
828
|
{
|
|
817
829
|
"action": "ask_continue_revise_or_confirm_only_if_needed",
|
|
818
830
|
"approvalQuestionRule": "If asking the user, the question and options must name the concrete decision. Prefer: 'Approve Sales Nav with {filters}, or use warmer Signals instead?' over 'Approve this lead source?' Option labels must name the source choice, such as 'Approve Sales Nav filters', 'Use warmer Signals posts', 'Try Prospeo/account search', and 'Revise source'.",
|
|
819
|
-
"approvalMathRule": "Immediately above the approval question, render the compact Math block: given this many eligible relevant posts, this many reachable engagers, this sampled ICP fit as n/N plus percentage/range, this target good-fit lead count, and this many posts needed, we expect this usable lead range from the selected posts;
|
|
831
|
+
"approvalMathRule": "Immediately above the approval question, render the compact Math block: given this many eligible relevant posts, this many reachable engagers, this sampled ICP fit as n/N plus percentage/range, this many good-fit prospects per 100 engagers, this many required engagers to scrape, this average reachable engagers per post, this expected good-fit prospects per post after cleanup, this source target good-fit lead count defaulting to 300, and this many posts needed, we expect this usable lead range from the selected posts; also state that approval only imports a 15-row campaign review batch first after the source list is materialized. If the user wants more volume, name the Sales Nav or Prospeo alternative, its preview/sample math, any Sales Nav loosening used to avoid an over-tight pool, and the colder/weaker-context tradeoff. Do not ask the approval question until this math is visible.",
|
|
820
832
|
"autoContinueWhen": {
|
|
821
833
|
"status": "confirmed",
|
|
822
834
|
"confidenceIn": [
|
|
@@ -902,10 +914,10 @@
|
|
|
902
914
|
"manifest": "lead-source-intake.json",
|
|
903
915
|
"defaultWhenMissing": "escalate_to_re_run_find_leads_with_campaignOfferId",
|
|
904
916
|
"branches": {
|
|
905
|
-
"normal-discovery": "Reuse the campaign-attached source selections/searches created during find-leads. Do not replay searches in Step 13; if no selected source/search exists, route back to find-leads with campaignOfferId.",
|
|
917
|
+
"normal-discovery": "Reuse the campaign-attached source selections/searches created during find-leads. Do not replay searches in Step 13; if no selected source/search exists, route back to find-leads with campaignOfferId. Provider import materializes the source lead list against the approved source-capacity plan, not the 15-row review batch.",
|
|
906
918
|
"supplied-linkedin-profiles": "Use the campaign-attached preview/list choice. Materialize the CSV into a source lead list when needed, then confirm_lead_list({ sourceLeadListId: leadListId, targetLeadCount: import.importLimit }).",
|
|
907
919
|
"existing-lead-list": "Use the campaign-attached existingLeadListId as selectedLeadListId and call confirm_lead_list({ sourceLeadListId: existingLeadListId, targetLeadCount: import.importLimit }).",
|
|
908
|
-
"supplied-domains": "Reuse the campaign-attached domainFilterId and Prospeo search created during find-leads, then import_leads({ campaignOfferId, targetLeadCount:
|
|
920
|
+
"supplied-domains": "Reuse the campaign-attached domainFilterId and Prospeo search created during find-leads, then import_leads({ campaignOfferId, targetLeadCount: sourceTargetGoodFitLeadCount default 300 }) to materialize the source list before confirm_lead_list imports the bounded review batch."
|
|
909
921
|
},
|
|
910
922
|
"persistMaterializedIds": [
|
|
911
923
|
"leadListId",
|
|
@@ -925,7 +937,7 @@
|
|
|
925
937
|
},
|
|
926
938
|
{
|
|
927
939
|
"action": "watch_mode_orient",
|
|
928
|
-
"watchNarrationRule": "Before import_leads or confirm_lead_list starts, align the guide with chat by setting review-batch watchNarration to current-tense
|
|
940
|
+
"watchNarrationRule": "Before import_leads or confirm_lead_list starts, align the guide with chat by setting review-batch watchNarration to current-tense source materialization copy. Also persist the approved source provider/currentStep first: Sales Nav approval must set leadSourceProvider `sales-nav` and currentStep `sales-nav`, Signals approval must set `signal-discovery`, and Prospeo approval must set `prospeo`. Use a headline like `Preparing the source list`; explain that the browser is on the approved source lane while Codex materializes only the approved source-capacity plan, then imports only the bounded 15-row review batch into the campaign table; include a no-launch safety note."
|
|
929
941
|
},
|
|
930
942
|
{
|
|
931
943
|
"tool": "import_leads",
|
|
@@ -934,7 +946,12 @@
|
|
|
934
946
|
"campaignOfferId",
|
|
935
947
|
"targetLeadCount"
|
|
936
948
|
],
|
|
937
|
-
"targetLeadCountFromConfig": "import.importLimit
|
|
949
|
+
"targetLeadCountFromConfig": "sourceTargetGoodFitLeadCount (default 300) for provider source-list materialization, not import.importLimit",
|
|
950
|
+
"signalDiscoveryRequiredFields": [
|
|
951
|
+
"targetEngagerCount from approved source math",
|
|
952
|
+
"maxPostsToScrape or selected post plan when available"
|
|
953
|
+
],
|
|
954
|
+
"signalDiscoveryRule": "For Signal Discovery, pass targetEngagerCount = ceil(sourceTargetGoodFitLeads / sampledFitRateAfterCleanup) and maxPostsToScrape = postsNeeded when known. This prevents import_leads from scraping every selected/promoted sample post by default. The first campaign review batch is still limited by confirm_lead_list targetLeadCount import.importLimit.",
|
|
938
955
|
"onZeroLeads": "escalate_hard_fail",
|
|
939
956
|
"modeAddHandshake": {
|
|
940
957
|
"firstCallReturns": "needsModeSelection=true + existingLeadListId",
|
|
@@ -46,7 +46,9 @@ Supported branches:
|
|
|
46
46
|
- **No manifest / normal discovery** — reuse the approved provider source that
|
|
47
47
|
is already attached to this campaign, then import. Never call a bare
|
|
48
48
|
`import_leads({ campaignOfferId })` for Signal Discovery unless selected posts
|
|
49
|
-
already exist on the campaign.
|
|
49
|
+
already exist on the campaign. Provider import materializes the source lead
|
|
50
|
+
list from the approved source-capacity plan; it is not the same target as the
|
|
51
|
+
15-row campaign review batch.
|
|
50
52
|
- **Supplied LinkedIn profile CSV** — confirm `load_csv_linkedin_leads` only
|
|
51
53
|
after rubrics and the approved message set are ready. Batch/materialize the
|
|
52
54
|
uploaded CSV into a Sellable lead-list table. Persist the returned
|
|
@@ -65,7 +67,8 @@ Supported branches:
|
|
|
65
67
|
- **Supplied domain/account CSV** — reuse or confirm `load_csv_domains`, keep
|
|
66
68
|
its `domainFilterId`, run a campaign-associated Prospeo people search with
|
|
67
69
|
`campaignOfferId`, provider prompt preflight, and `domainFilterId`, then
|
|
68
|
-
|
|
70
|
+
materialize the source list from that search before importing the bounded
|
|
71
|
+
review batch into the campaign table.
|
|
69
72
|
|
|
70
73
|
## Campaign-Attached Source Contract
|
|
71
74
|
|
|
@@ -90,12 +93,16 @@ the domain constraint.
|
|
|
90
93
|
|
|
91
94
|
## First-Import Path
|
|
92
95
|
|
|
93
|
-
Default case — no existing lead list bound to the campaign
|
|
96
|
+
Default case — no existing lead list bound to the campaign. This creates or
|
|
97
|
+
fills the source lead list; it does **not** clone rows into the campaign table:
|
|
94
98
|
|
|
95
99
|
```text
|
|
96
100
|
import_leads({
|
|
97
101
|
campaignOfferId,
|
|
98
|
-
targetLeadCount: <
|
|
102
|
+
targetLeadCount: <sourceTargetGoodFitLeads; default 300>,
|
|
103
|
+
// Signal Discovery only:
|
|
104
|
+
targetEngagerCount: <ceil(sourceTargetGoodFitLeads / sampledFitRateAfterCleanup)>,
|
|
105
|
+
maxPostsToScrape: <postsNeeded from approved math>
|
|
99
106
|
})
|
|
100
107
|
```
|
|
101
108
|
|
|
@@ -104,6 +111,14 @@ and should already be saved on the campaign before import. Response returns
|
|
|
104
111
|
`{ imported, skipped, duplicates }` — surface all three to the escalation logic
|
|
105
112
|
so dedup ratios are visible.
|
|
106
113
|
|
|
114
|
+
For Signal Discovery, do not scrape every currently selected/promoted sample
|
|
115
|
+
post by default. Before `import_leads`, reconcile selected posts with the
|
|
116
|
+
approved source math. If the math says 20 good fits per 100 engagers and the
|
|
117
|
+
source target is 300 good-fit leads, pass `targetEngagerCount` around 1,500 and
|
|
118
|
+
only enough posts to reach that engager count. The subsequent
|
|
119
|
+
`confirm_lead_list` call still uses `targetLeadCount: <importLimit>` so only the
|
|
120
|
+
bounded review batch enters the campaign table.
|
|
121
|
+
|
|
107
122
|
For supplied direct lists, `confirm_lead_list` must receive
|
|
108
123
|
`targetLeadCount: <importLimit>` or explicit `sourceRowIds` so a 100-row source
|
|
109
124
|
imports only the bounded review batch before greenlight.
|
|
@@ -89,6 +89,20 @@ You must estimate:
|
|
|
89
89
|
- how many sampled people matched the rough headline rubric
|
|
90
90
|
- `passRate`
|
|
91
91
|
- sampled pass rate used for extrapolation
|
|
92
|
+
- `goodFitPer100Engagers`
|
|
93
|
+
- normalized fit rate as good-fit prospects per 100 engagers, before and
|
|
94
|
+
after a conservative dedupe/cleanup factor
|
|
95
|
+
- `avgReachableEngagersPerPost`
|
|
96
|
+
- average reachable engagers per right-content post used for capacity math
|
|
97
|
+
- `goodFitProspectsPerPost`
|
|
98
|
+
- expected good-fit prospects per right-content post after dedupe/cleanup
|
|
99
|
+
- `postsNeededForTarget`
|
|
100
|
+
- number of right-content posts needed to reach the target good-fit lead
|
|
101
|
+
count, defaulting to 300 unless the campaign says otherwise
|
|
102
|
+
- `requiredEngagersToScrape`
|
|
103
|
+
- `ceil(targetGoodFitLeads / sampledFitRate)`; for example 20 good-fit
|
|
104
|
+
prospects per 100 engagers means a 300-good-fit source target needs about
|
|
105
|
+
1,500 engagers scraped
|
|
92
106
|
- `projectedRange`
|
|
93
107
|
- the conservative range implied by the observed sample
|
|
94
108
|
|
|
@@ -112,7 +126,7 @@ Use conservative logic:
|
|
|
112
126
|
- stale posts
|
|
113
127
|
- adjacent communities that are not actual buyers
|
|
114
128
|
4. Explain the pass-through briefly and explicitly:
|
|
115
|
-
- preferred: "`sampledCount`: 40, `passCount`: 9, `passRate`: ~22%, `recentStrongPostCount`: 15-25, `freshEnoughPostCount`: 8-12, `
|
|
129
|
+
- preferred: "`sampledCount`: 40, `passCount`: 9, `passRate`: ~22%, `goodFitPer100Engagers`: ~22 before cleanup / ~13-16 after cleanup, `requiredEngagersToScrape`: ~1,875-2,300 for a 300-good-fit target after cleanup, `avgReachableEngagersPerPost`: 240, `goodFitProspectsPerPost`: ~31-38, `postsNeededForTarget`: ~8-10, `recentStrongPostCount`: 15-25, `freshEnoughPostCount`: 8-12, `projectedRange`: 300-380 from selected posts"
|
|
116
130
|
- fallback: "I could not fetch engagers, so this is inferred from post and author quality only"
|
|
117
131
|
5. If the evidence is too weak, say so and return a low-confidence estimate instead of pretending precision.
|
|
118
132
|
6. Do not manually enumerate dozens of engagers in the final answer. Summarize the sample by `sampledCount`, `passCount`, `passRate`, `projectedRange`, and 3-6 representative examples.
|
|
@@ -364,17 +378,21 @@ For `create-campaign-v2` source approval, do not treat the default
|
|
|
364
378
|
`selectionTarget` of 3 posts as enough by itself. Before the final source
|
|
365
379
|
recommendation, estimate source capacity from real sample math:
|
|
366
380
|
|
|
367
|
-
- target good-fit leads (default
|
|
381
|
+
- source target good-fit leads (default 300 unless the campaign says otherwise)
|
|
368
382
|
- eligible right-content posts by lane/content type
|
|
369
383
|
- reachable engagers from those posts
|
|
370
384
|
- sampled ICP-fit rate as `n/N` plus an easy percentage/range
|
|
385
|
+
- good-fit prospects per 100 engagers before and after conservative cleanup
|
|
386
|
+
- required engagers to scrape for the source target
|
|
387
|
+
- average reachable engagers per right-content post
|
|
371
388
|
- expected good-fit leads per selected post after dedupe/cleanup
|
|
372
389
|
- posts needed to reach the target
|
|
373
390
|
|
|
374
|
-
Then select
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
391
|
+
Then select the smallest right-content post set that plausibly hits the source
|
|
392
|
+
target. Do not scrape every promoted sample post by default; promoted sampling
|
|
393
|
+
state and final scrape plan are separate. If the math says the warm post lane
|
|
394
|
+
only supports a smaller first batch, say that and name the Sales Nav or Prospeo
|
|
395
|
+
scale fallback rather than padding the selection with noisy posts.
|
|
378
396
|
|
|
379
397
|
```json
|
|
380
398
|
select_promising_posts({
|
|
@@ -420,13 +438,16 @@ as `select_promising_posts`; ask a clear yes/no question and wait for the user
|
|
|
420
438
|
to say **yes**.
|
|
421
439
|
|
|
422
440
|
For `create-campaign-v2` after the user has already approved the lead-source
|
|
423
|
-
decision, that approval is the explicit confirmation
|
|
424
|
-
|
|
441
|
+
decision, that approval is the explicit confirmation to materialize the approved
|
|
442
|
+
source list and then import only the bounded campaign review batch. In that
|
|
443
|
+
tail flow, follow `create-campaign-v2-tail`: call
|
|
425
444
|
`select_promising_posts({ campaignOfferId, selectionMode: "replace",
|
|
426
445
|
selections, headlineICPCriteria })`, then call
|
|
427
446
|
`import_leads({ campaignOfferId, provider: "signal-discovery",
|
|
428
|
-
targetLeadCount })` for the approved
|
|
429
|
-
|
|
447
|
+
targetLeadCount, targetEngagerCount, maxPostsToScrape })` for the approved
|
|
448
|
+
source-capacity plan without asking for another yes/no gate. Then
|
|
449
|
+
`confirm_lead_list` imports only the bounded review batch into the campaign
|
|
450
|
+
table. Do not confuse the source scrape target with the 15-row review batch.
|
|
430
451
|
|
|
431
452
|
The promotion/select step is required campaign state. Use post IDs from the
|
|
432
453
|
current campaign-scoped search result, not stale IDs copied from a source review
|
|
@@ -434,12 +455,14 @@ or scout summary. If `select_promising_posts` returns `selectedCount: 0`, stop:
|
|
|
434
455
|
do not call `import_leads`, do not switch providers automatically, and do not
|
|
435
456
|
claim the source failed. Re-run a narrow campaign-scoped search to recover
|
|
436
457
|
current post rows, or have the user promote the posts in the UI and then retry
|
|
437
|
-
the
|
|
458
|
+
the source-list materialization.
|
|
438
459
|
|
|
439
460
|
```json
|
|
440
461
|
import_leads({
|
|
441
462
|
"campaignOfferId": "cmp_xxx",
|
|
442
|
-
"targetLeadCount":
|
|
463
|
+
"targetLeadCount": 300,
|
|
464
|
+
"targetEngagerCount": 1500,
|
|
465
|
+
"maxPostsToScrape": 5
|
|
443
466
|
})
|
|
444
467
|
```
|
|
445
468
|
|
|
@@ -451,7 +474,7 @@ After the lead list finishes and the user confirms it looks good, call:
|
|
|
451
474
|
confirm_lead_list({
|
|
452
475
|
"campaignOfferId": "cmp_xxx",
|
|
453
476
|
"jobId": "<jobId from import_leads>",
|
|
454
|
-
"targetLeadCount":
|
|
477
|
+
"targetLeadCount": 15
|
|
455
478
|
})
|
|
456
479
|
```
|
|
457
480
|
|