@sellable/mcp 0.1.31 → 0.1.33
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/auth.d.ts +25 -0
- package/dist/auth.js +30 -0
- package/dist/server.js +13 -5
- package/dist/tools/auth.d.ts +8 -0
- package/dist/tools/auth.js +20 -1
- package/dist/tools/campaigns.d.ts +10 -97
- package/dist/tools/campaigns.js +29 -150
- package/dist/tools/cli-login.d.ts +86 -0
- package/dist/tools/cli-login.js +208 -0
- package/dist/tools/flow-preflight.d.ts +3 -16
- package/dist/tools/flow-preflight.js +6 -25
- package/dist/tools/linkedin.d.ts +14 -1
- package/dist/tools/linkedin.js +10 -2
- package/dist/tools/navigation.js +2 -2
- package/dist/tools/one-off.d.ts +15 -2
- package/dist/tools/one-off.js +23 -6
- package/dist/tools/prompts.d.ts +2 -2
- package/dist/tools/prompts.js +2 -2
- package/dist/tools/readiness.d.ts +1 -0
- package/dist/tools/readiness.js +4 -2
- package/dist/tools/sequencer.d.ts +7 -0
- package/dist/tools/sequencer.js +8 -2
- package/package.json +1 -1
- package/skills/create-campaign/SKILL.md +82 -5
- package/skills/create-campaign/references/ai-native-tokens.md +266 -0
- package/skills/create-campaign/references/token-fill-examples.md +93 -0
- package/skills/create-campaign-brief/references/brief-template.md +11 -0
- package/skills/create-campaign-v2/SKILL.md +44 -24
- package/skills/create-campaign-v2/core/flow.v2.json +0 -2
- package/skills/generate-messages/SKILL.md +38 -0
- package/skills/research-sender/SKILL.md +78 -36
package/dist/tools/prompts.js
CHANGED
|
@@ -97,8 +97,8 @@ export const promptToolDefinitions = [
|
|
|
97
97
|
properties: {
|
|
98
98
|
depth: {
|
|
99
99
|
type: "string",
|
|
100
|
-
enum: ["minimal-verification", "deep-proof"],
|
|
101
|
-
description: "Research depth used: minimal
|
|
100
|
+
enum: ["minimal-verification", "deep-proof", "parallel-batch"],
|
|
101
|
+
description: "Research depth used: minimal-verification (proofDigest already had strong signals), deep-proof (extra search agents spawned), or parallel-batch (Phase 114+ default — single 5-call parallel batch with fetch_linkedin_profile + fetch_company + 3x WebSearch).",
|
|
102
102
|
},
|
|
103
103
|
proofItemsFound: {
|
|
104
104
|
type: "number",
|
|
@@ -20,6 +20,7 @@ export declare function resolveWaitTimeout(timeoutMs?: number): {
|
|
|
20
20
|
effectiveTimeoutMs: number;
|
|
21
21
|
guardApplied: boolean;
|
|
22
22
|
};
|
|
23
|
+
export declare function hasNonWaitableMissing(missing: string[]): boolean;
|
|
23
24
|
export declare const readinessToolDefinitions: ({
|
|
24
25
|
name: string;
|
|
25
26
|
description: string;
|
package/dist/tools/readiness.js
CHANGED
|
@@ -32,9 +32,11 @@ export function resolveWaitTimeout(timeoutMs) {
|
|
|
32
32
|
guardApplied: requestedTimeoutMs > effectiveTimeoutMs,
|
|
33
33
|
};
|
|
34
34
|
}
|
|
35
|
-
function hasNonWaitableMissing(missing) {
|
|
35
|
+
export function hasNonWaitableMissing(missing) {
|
|
36
|
+
// Phase 114: clientProspectId is no longer non-waitable. Net-new campaigns
|
|
37
|
+
// can be created with senderLinkedinUrl instead, with downstream resolution
|
|
38
|
+
// happening at send-time.
|
|
36
39
|
const nonWaitable = new Set([
|
|
37
|
-
"clientProspectId",
|
|
38
40
|
"campaignBrief.content",
|
|
39
41
|
"leadSourceType",
|
|
40
42
|
'leadSourceType="new"',
|
|
@@ -2,6 +2,7 @@ export type CreateWorkflowTableInput = {
|
|
|
2
2
|
name: string;
|
|
3
3
|
senderId: string;
|
|
4
4
|
clientProspectId?: string;
|
|
5
|
+
senderLinkedinUrl?: string;
|
|
5
6
|
sequenceActions?: string[];
|
|
6
7
|
};
|
|
7
8
|
export type CreateWorkflowTableResponse = {
|
|
@@ -58,6 +59,10 @@ export declare const sequencerToolDefinitions: ({
|
|
|
58
59
|
type: string;
|
|
59
60
|
description: string;
|
|
60
61
|
};
|
|
62
|
+
senderLinkedinUrl: {
|
|
63
|
+
type: string;
|
|
64
|
+
description: string;
|
|
65
|
+
};
|
|
61
66
|
sequenceActions: {
|
|
62
67
|
type: string;
|
|
63
68
|
items: {
|
|
@@ -93,6 +98,7 @@ export declare const sequencerToolDefinitions: ({
|
|
|
93
98
|
name?: undefined;
|
|
94
99
|
senderId?: undefined;
|
|
95
100
|
clientProspectId?: undefined;
|
|
101
|
+
senderLinkedinUrl?: undefined;
|
|
96
102
|
sequenceActions?: undefined;
|
|
97
103
|
campaignId?: undefined;
|
|
98
104
|
};
|
|
@@ -115,6 +121,7 @@ export declare const sequencerToolDefinitions: ({
|
|
|
115
121
|
name?: undefined;
|
|
116
122
|
senderId?: undefined;
|
|
117
123
|
clientProspectId?: undefined;
|
|
124
|
+
senderLinkedinUrl?: undefined;
|
|
118
125
|
sequenceActions?: undefined;
|
|
119
126
|
tableId?: undefined;
|
|
120
127
|
template?: undefined;
|
package/dist/tools/sequencer.js
CHANGED
|
@@ -7,7 +7,8 @@ export const sequencerToolDefinitions = [
|
|
|
7
7
|
"All tables include LinkedIn URL, Name, and Approved. " +
|
|
8
8
|
"Additional columns depend on the planned actions: INVITE/DM -> Message, " +
|
|
9
9
|
"INMAIL_* -> Subject + Message, COMMENT -> Post URL + Comment + Reaction Type. " +
|
|
10
|
-
"Campaign brief, rubrics, positioning, and prospect linkage remain optional follow-on metadata
|
|
10
|
+
"Campaign brief, rubrics, positioning, and prospect linkage remain optional follow-on metadata.\n\n" +
|
|
11
|
+
"INPUTS: Pass EITHER clientProspectId OR senderLinkedinUrl. Both optional — if neither is supplied, the table is created without prospect linkage.",
|
|
11
12
|
inputSchema: {
|
|
12
13
|
type: "object",
|
|
13
14
|
properties: {
|
|
@@ -21,7 +22,11 @@ export const sequencerToolDefinitions = [
|
|
|
21
22
|
},
|
|
22
23
|
clientProspectId: {
|
|
23
24
|
type: "string",
|
|
24
|
-
description: "Optional
|
|
25
|
+
description: "Optional. EnrichedProspect ID. Pass this OR senderLinkedinUrl.",
|
|
26
|
+
},
|
|
27
|
+
senderLinkedinUrl: {
|
|
28
|
+
type: "string",
|
|
29
|
+
description: "Optional. Sender's LinkedIn profile URL — used to lazy-resolve the prospect at send-time when clientProspectId is not yet available.",
|
|
25
30
|
},
|
|
26
31
|
sequenceActions: {
|
|
27
32
|
type: "array",
|
|
@@ -87,6 +92,7 @@ export async function createWorkflowTable(input) {
|
|
|
87
92
|
name: input.name,
|
|
88
93
|
senderId: input.senderId,
|
|
89
94
|
clientProspectId: input.clientProspectId,
|
|
95
|
+
senderLinkedinUrl: input.senderLinkedinUrl,
|
|
90
96
|
...(input.sequenceActions
|
|
91
97
|
? { sequenceActions: input.sequenceActions }
|
|
92
98
|
: {}),
|
package/package.json
CHANGED
|
@@ -4,6 +4,8 @@ description: Create a Sellable campaign through the approval-gated workflow.
|
|
|
4
4
|
visibility: public
|
|
5
5
|
allowed-tools:
|
|
6
6
|
- mcp__sellable__get_auth_status
|
|
7
|
+
- mcp__sellable__start_cli_login
|
|
8
|
+
- mcp__sellable__wait_for_cli_login
|
|
7
9
|
- mcp__sellable__bootstrap_create_campaign
|
|
8
10
|
- mcp__sellable__get_subskill_prompt
|
|
9
11
|
- mcp__sellable__search_subskill_prompts
|
|
@@ -323,14 +325,89 @@ plugin cache paths, MCP namespaces, or runbooks in customer-facing progress
|
|
|
323
325
|
updates.
|
|
324
326
|
|
|
325
327
|
1. Call `mcp__sellable__get_auth_status({})`.
|
|
326
|
-
2. If auth is not OK
|
|
327
|
-
|
|
328
|
-
|
|
328
|
+
2. If auth is not OK with `error.type === "config"` or `error.type === "auth"`,
|
|
329
|
+
the user has not signed in yet. Run the FTUX magic-link handoff:
|
|
330
|
+
|
|
331
|
+
a. Say to the user verbatim:
|
|
332
|
+
|
|
333
|
+
```text
|
|
334
|
+
Welcome to Sellable. What's your email?
|
|
335
|
+
```
|
|
336
|
+
|
|
337
|
+
b. Wait for the user to paste their email in normal chat. Do NOT use
|
|
338
|
+
`AskUserQuestion` / `request_user_input` for this — it's free-text input.
|
|
339
|
+
|
|
340
|
+
c. Call `mcp__sellable__start_cli_login({ email })` with the email the user
|
|
341
|
+
typed.
|
|
342
|
+
|
|
343
|
+
d. If `start_cli_login` returns `ok: false`, surface `error.guidance` to the
|
|
344
|
+
user and stop. Do not retry automatically.
|
|
345
|
+
|
|
346
|
+
e. On `ok: true`, say to the user verbatim (substituting the email exactly
|
|
347
|
+
as the user typed it):
|
|
348
|
+
|
|
349
|
+
```text
|
|
350
|
+
Magic link sent to {email}. Click it from your inbox — I'll wait. (If your team already has a Sellable workspace, ask an admin to invite you instead — that gets you straight into their data.)
|
|
351
|
+
```
|
|
352
|
+
|
|
353
|
+
f. Call `mcp__sellable__wait_for_cli_login({ sessionId })` using the
|
|
354
|
+
`sessionId` returned by `start_cli_login`.
|
|
355
|
+
|
|
356
|
+
- If the result is `error.type === "tool_timeout_guard"`, IMMEDIATELY
|
|
357
|
+
re-call `mcp__sellable__wait_for_cli_login({ sessionId })` with the
|
|
358
|
+
SAME sessionId. Do not narrate anything to the user. Do not call
|
|
359
|
+
`start_cli_login` again — that would send a new magic link and confuse
|
|
360
|
+
them. Loop on `tool_timeout_guard` until you get a different result.
|
|
361
|
+
|
|
362
|
+
- If `error.type === "expired"` or `error.type === "timeout"`, say to the
|
|
363
|
+
user verbatim and stop:
|
|
364
|
+
|
|
365
|
+
```text
|
|
366
|
+
That magic link expired. Run /sellable:create-campaign again to retry.
|
|
367
|
+
```
|
|
368
|
+
|
|
369
|
+
- If `error.type === "already_consumed"` or any other error, surface
|
|
370
|
+
`error.guidance` and stop.
|
|
371
|
+
|
|
372
|
+
- On `ok: true`, the user is signed in and `~/.sellable/config.json` has
|
|
373
|
+
been written. Your IMMEDIATE next visible message branches on
|
|
374
|
+
`isReturningUser` from the tool result:
|
|
375
|
+
|
|
376
|
+
- If `isReturningUser === true`, prepend ONE line acknowledging the
|
|
377
|
+
reused workspace, then the locked Step 3 narration verbatim
|
|
378
|
+
(substituting `activeWorkspaceName` exactly):
|
|
379
|
+
|
|
380
|
+
```text
|
|
381
|
+
You're in — using your {activeWorkspaceName} workspace.
|
|
382
|
+
|
|
383
|
+
Now — paste the LinkedIn profile URL of the person you want to send from.
|
|
384
|
+
```
|
|
385
|
+
|
|
386
|
+
- If `isReturningUser === false`, prepend ONE line confirming the new
|
|
387
|
+
workspace, then the locked Step 3 narration verbatim:
|
|
388
|
+
|
|
389
|
+
```text
|
|
390
|
+
Welcome to Sellable — created {activeWorkspaceName} for you.
|
|
391
|
+
|
|
392
|
+
Now — paste the LinkedIn profile URL of the person you want to send from.
|
|
393
|
+
```
|
|
394
|
+
|
|
395
|
+
No other lines. No "all set", no "signed in", no other acknowledgement.
|
|
396
|
+
|
|
397
|
+
After the user pastes the URL, proceed with the existing identity-first
|
|
398
|
+
sender flow (Step 3 onwards in the v2 subskill prompt — sender
|
|
399
|
+
enrichment via `fetch_linkedin_profile` / `enrich_sender`).
|
|
400
|
+
|
|
401
|
+
3. If auth is not OK with `error.type === "workspace"` (token valid, no active
|
|
402
|
+
workspace), stop and show the returned guidance — that's not a fresh-user
|
|
403
|
+
scenario; the user needs to run `set_active_workspace`.
|
|
404
|
+
4. Detect optional campaign id in the user request (`cmp_...`).
|
|
405
|
+
5. If no campaign id is provided, stay in fresh-create mode and do not call campaign discovery/resume helpers to find one.
|
|
329
406
|
- Do not call `mcp__sellable__get_campaigns`.
|
|
330
407
|
- Do not call `mcp__sellable__get_campaign` to hunt for IDs.
|
|
331
408
|
- Do not call `mcp__sellable__create_campaign({ campaignId: ... })` unless the user supplied that id.
|
|
332
|
-
|
|
333
|
-
|
|
409
|
+
6. Call `mcp__sellable__bootstrap_create_campaign({ flowVersion: "v2", campaignId? })`.
|
|
410
|
+
7. If `safeToProceed !== true`, stop and show `blockingErrors` + `nextStep`.
|
|
334
411
|
|
|
335
412
|
## Execute Workflow
|
|
336
413
|
|
|
@@ -0,0 +1,266 @@
|
|
|
1
|
+
# AI-Native Tokens
|
|
2
|
+
|
|
3
|
+
The canonical spec for personalization tokens in Sellable campaign messages.
|
|
4
|
+
|
|
5
|
+
## Why this exists
|
|
6
|
+
|
|
7
|
+
Old-school personalization tokens (`{{first_name}}`, `{{company}}`,
|
|
8
|
+
`{{workflow_context}}`) are field substitutions: a string is looked up and
|
|
9
|
+
spliced into a fixed sentence. That works for atomic facts (first name,
|
|
10
|
+
company name). It fails for anything richer — because the _grammatical shape_
|
|
11
|
+
of the surrounding sentence locks in what the substituted value must look
|
|
12
|
+
like, and the rules about what makes a good substitution live in a separate
|
|
13
|
+
"Token Fill Rules" table that the generation model can easily ignore or
|
|
14
|
+
forget.
|
|
15
|
+
|
|
16
|
+
Concrete failure mode this caused in production: the template
|
|
17
|
+
`Thought of {{company}} because of your {{workflow_context}}.` was filled
|
|
18
|
+
with `Moneyball dashboard for CEOs` for a prospect who _built_ that product
|
|
19
|
+
at his company. The rendered line —
|
|
20
|
+
`Thought of Hatchproof because of your Moneyball dashboard for CEOs.` —
|
|
21
|
+
read as if we mistook the founder for a customer of his own product.
|
|
22
|
+
Field-substitution tokens have no built-in defense against that.
|
|
23
|
+
|
|
24
|
+
**AI-native tokens fix this.** The token _names what job the line does_ and
|
|
25
|
+
carries its rules _inline_. The model writes a SENTENCE (not fills a slot)
|
|
26
|
+
that satisfies the inline contract, or omits the line entirely. The grammar
|
|
27
|
+
trap disappears because the model is composing the line from scratch instead
|
|
28
|
+
of plugging a noun into a fixed frame.
|
|
29
|
+
|
|
30
|
+
## The contract
|
|
31
|
+
|
|
32
|
+
An AI-native token is a bracketed instruction in the message template:
|
|
33
|
+
|
|
34
|
+
```text
|
|
35
|
+
[ALL_CAPS_NAME — instructions describing what the line should do, with
|
|
36
|
+
DO / DON'T rules and a FALLBACK clause]
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
Three properties define it:
|
|
40
|
+
|
|
41
|
+
1. **Named by intent, not by field.** `[PERSONALIZATION_LINE — ...]` says
|
|
42
|
+
what JOB the line does. `{{workflow_context}}` only names a slot. When
|
|
43
|
+
the model sees an intent name, it has a target to write toward.
|
|
44
|
+
2. **Rules live inline.** DO / DON'T / FALLBACK appear inside the bracket,
|
|
45
|
+
adjacent to where the line gets written. The model doesn't have to
|
|
46
|
+
consult a faraway rules table and remember to apply it.
|
|
47
|
+
3. **Sentence-shaped, not noun-shaped.** The model produces an entire
|
|
48
|
+
sentence (or no sentence). There is no fixed frame around the token, so
|
|
49
|
+
no grammar trap. A weak fill is still a complete sentence; a wrong fill
|
|
50
|
+
is detectable at the sentence level.
|
|
51
|
+
|
|
52
|
+
The bracket is replaced at generation time by either (a) the rendered
|
|
53
|
+
sentence, or (b) nothing (omit). The brackets themselves never appear in
|
|
54
|
+
the final message.
|
|
55
|
+
|
|
56
|
+
## Required clauses inside an AI-native token
|
|
57
|
+
|
|
58
|
+
Every well-specified AI-native token must include four things:
|
|
59
|
+
|
|
60
|
+
- **Intent.** One sentence on what job this line does in the message. Why
|
|
61
|
+
the line exists. What it should accomplish for the buyer.
|
|
62
|
+
- **DO.** 2-4 specific shapes the model should produce. Verb-led
|
|
63
|
+
prescriptions. Examples that work.
|
|
64
|
+
- **DON'T.** 2-4 anti-patterns the model must reject. Failure modes
|
|
65
|
+
observed in real campaigns. Bad shapes paired with WHY they're bad
|
|
66
|
+
when the bad-ness isn't obvious from the shape alone.
|
|
67
|
+
- **FALLBACK.** What to do when none of the DO shapes can be satisfied
|
|
68
|
+
cleanly from the row data. The default fallback is OMIT THE ENTIRE LINE.
|
|
69
|
+
Aggressive omit beats awkward fill, every time. Never silently
|
|
70
|
+
substitute a generic noun.
|
|
71
|
+
|
|
72
|
+
A token without all four clauses is under-specified. The model will
|
|
73
|
+
hallucinate the missing rules from priors, and those priors include
|
|
74
|
+
generic mail-merge failure modes.
|
|
75
|
+
|
|
76
|
+
## The canonical example
|
|
77
|
+
|
|
78
|
+
This is the original Sellable gold-standard message. It uses two AI-native
|
|
79
|
+
tokens. Both have the contract baked in (loosely — even an under-specified
|
|
80
|
+
intent-name dramatically beats a field token, but the formal contract makes
|
|
81
|
+
it bulletproof):
|
|
82
|
+
|
|
83
|
+
```text
|
|
84
|
+
hey [name],
|
|
85
|
+
|
|
86
|
+
saw you raise your hand for claude + gtm (creepy to reach out based on
|
|
87
|
+
that, i know) - but this felt too on the nose to ignore.
|
|
88
|
+
|
|
89
|
+
i'm building sellable, the only gtm platform that runs natively on claude
|
|
90
|
+
code.
|
|
91
|
+
|
|
92
|
+
we're looking for design partners - and [PERSONALIZED REASON - their team
|
|
93
|
+
size, role, or why they're a perfect fit].
|
|
94
|
+
|
|
95
|
+
two options:
|
|
96
|
+
|
|
97
|
+
a) 15-min call - i'll show you how you could book more meetings with
|
|
98
|
+
[THEIR ICP - who they want to reach], and if you like it we launch a pilot
|
|
99
|
+
right there
|
|
100
|
+
|
|
101
|
+
b) i send you the video of me using sellable to write and send this exact
|
|
102
|
+
message to you (yes, it's that meta)
|
|
103
|
+
|
|
104
|
+
p.s. yes, this message was entirely written and sent via claude code 😊
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
Note what the tokens DO:
|
|
108
|
+
|
|
109
|
+
- `[PERSONALIZED REASON - their team size, role, or why they're a perfect fit]`
|
|
110
|
+
— the brackets name an intent (PERSONALIZED REASON) and offer three
|
|
111
|
+
acceptable input sources (team size, role, fit reason). The model picks
|
|
112
|
+
whichever is supported by the row.
|
|
113
|
+
- `[THEIR ICP - who they want to reach]` — names what the slot is FOR
|
|
114
|
+
("their ICP") and clarifies in plain English ("who they want to reach"),
|
|
115
|
+
so the model doesn't need a glossary.
|
|
116
|
+
|
|
117
|
+
Both feel hand-crafted in every rendered version because the model is
|
|
118
|
+
_writing_, not _filling_.
|
|
119
|
+
|
|
120
|
+
## The fully-specified contract (use this for new campaigns)
|
|
121
|
+
|
|
122
|
+
When authoring a new brief, write tokens in the full contract form. The
|
|
123
|
+
canonical Sellable example is loose because it predates the contract; it
|
|
124
|
+
still works because the intent names are crystal clear, but new tokens
|
|
125
|
+
should err toward explicit:
|
|
126
|
+
|
|
127
|
+
```text
|
|
128
|
+
[PERSONALIZATION_LINE — write ONE short sentence (8-15 words) that anchors
|
|
129
|
+
this message to {{first_name}} as a person. Name what they personally DO,
|
|
130
|
+
write about, focus on, or build TOWARD (not what their company SELLS).
|
|
131
|
+
DO: use verb-led shapes — "Saw your work helping X", "Your writing on Y
|
|
132
|
+
caught my eye", "Your focus on Z is rare in this segment".
|
|
133
|
+
DON'T: name a product their company sells ("your dashboard for X", "your
|
|
134
|
+
platform for Y") — they BUILD the product, they don't have one.
|
|
135
|
+
DON'T: use source-citation phrasing ("Saw your post about", "Your bio
|
|
136
|
+
says").
|
|
137
|
+
DON'T: use generic noun substitutes ("your work", "your stack", "your
|
|
138
|
+
team") — those add no relevance.
|
|
139
|
+
FALLBACK: if you can't satisfy the DOs cleanly from the row data, OMIT
|
|
140
|
+
this entire line. The message reads cleanly without it. Aggressive omit >
|
|
141
|
+
awkward fill.]
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
That bracket is ~15 lines. It produces ONE sentence in the rendered
|
|
145
|
+
message (or zero). The verbosity inside the bracket is the price of
|
|
146
|
+
predictable output across thousands of rows.
|
|
147
|
+
|
|
148
|
+
## The grammar test
|
|
149
|
+
|
|
150
|
+
Every personalization line should pass this test before rendering: read
|
|
151
|
+
`your <X>` (or whatever possessive frame the line uses) out loud. If "X"
|
|
152
|
+
reads as if the recipient _uses_ or _has_ the thing, but the recipient
|
|
153
|
+
actually _builds_ or _sells_ the thing, the fill is wrong → OMIT.
|
|
154
|
+
|
|
155
|
+
Examples:
|
|
156
|
+
|
|
157
|
+
| Filled value | Pass test? |
|
|
158
|
+
| ----------------------------------------- | ------------------------------------ |
|
|
159
|
+
| `your monetization work for AI founders` | ✅ recipient does this work |
|
|
160
|
+
| `your founder voice on team performance` | ✅ recipient has this voice |
|
|
161
|
+
| `your Moneyball dashboard for CEOs` | ❌ recipient _builds_ this dashboard |
|
|
162
|
+
| `your monetization layer for AI builders` | ❌ recipient _sells_ this product |
|
|
163
|
+
| `your AI scoring engine` | ❌ product they sell |
|
|
164
|
+
| `your work` | ❌ generic, adds no relevance |
|
|
165
|
+
| `your team` | ❌ generic, adds no relevance |
|
|
166
|
+
|
|
167
|
+
When in doubt, omit. The message must read cleanly without the
|
|
168
|
+
personalization line. If it doesn't, the rest of the message is
|
|
169
|
+
under-specified — fix that, don't paper over it with weak personalization.
|
|
170
|
+
|
|
171
|
+
## Field-substitution tokens still have a role
|
|
172
|
+
|
|
173
|
+
AI-native tokens replace personalization sentences. They do NOT replace
|
|
174
|
+
atomic field substitutions like `{{first_name}}` and `{{company}}`. Use
|
|
175
|
+
field tokens when:
|
|
176
|
+
|
|
177
|
+
- The value is an atomic noun that goes in a fixed slot (greeting:
|
|
178
|
+
`Hey {{first_name}},`, subject: `{{company}} outbound + ...`)
|
|
179
|
+
- The value comes directly from a row column with no judgment required
|
|
180
|
+
- Failure mode is "missing field" not "wrong shape" — and the fallback is
|
|
181
|
+
trivial (`there` instead of first name, omit the soft-bridge instead of
|
|
182
|
+
company)
|
|
183
|
+
|
|
184
|
+
Use AI-native tokens when:
|
|
185
|
+
|
|
186
|
+
- The value is a SENTENCE that requires judgment about what to write
|
|
187
|
+
- The value depends on synthesizing multiple row fields (bio + recent
|
|
188
|
+
posts + company context) into a human-sounding line
|
|
189
|
+
- Failure mode is "wrong shape" or "awkward grammar" — needs rules to
|
|
190
|
+
prevent
|
|
191
|
+
- The line is optional (you'd rather have NO line than a bad line)
|
|
192
|
+
|
|
193
|
+
Most campaigns will use both: field tokens for greeting/subject/company,
|
|
194
|
+
AI-native tokens for any personalization or context-bridging lines.
|
|
195
|
+
|
|
196
|
+
## Brief authoring guide
|
|
197
|
+
|
|
198
|
+
When writing a campaign brief, document each token in two places:
|
|
199
|
+
|
|
200
|
+
### 1. In the message template itself
|
|
201
|
+
|
|
202
|
+
Inline AI-native tokens go directly in the template body:
|
|
203
|
+
|
|
204
|
+
```text
|
|
205
|
+
Hey {{first_name}},
|
|
206
|
+
|
|
207
|
+
[FIRST_LINE — opener that anchors to {{company}}'s current situation.
|
|
208
|
+
DO: ... DON'T: ... FALLBACK: ...]
|
|
209
|
+
|
|
210
|
+
[BODY — ... ]
|
|
211
|
+
```
|
|
212
|
+
|
|
213
|
+
### 2. In the Token Fill Rules section
|
|
214
|
+
|
|
215
|
+
A small table cataloging every token, marked by type:
|
|
216
|
+
|
|
217
|
+
```markdown
|
|
218
|
+
| Token | Type | Source / Instructions | Fallback |
|
|
219
|
+
| ------------------------------ | --------- | --------------------------------------------- | ----------------------- |
|
|
220
|
+
| `{{first_name}}` | Field | Sales Nav `firstName` | `there` |
|
|
221
|
+
| `{{company}}` | Field | Sales Nav `organization.name`; strip suffixes | omit dependent sentence |
|
|
222
|
+
| `[PERSONALIZATION_LINE — ...]` | AI-native | Inline. See bracket. | Omit the line. |
|
|
223
|
+
```
|
|
224
|
+
|
|
225
|
+
The AI-native row in the table doesn't need to repeat the instructions —
|
|
226
|
+
they live inline. The table just confirms the token exists, what type it
|
|
227
|
+
is, and where the fallback resolves.
|
|
228
|
+
|
|
229
|
+
## Common authoring mistakes
|
|
230
|
+
|
|
231
|
+
- **Naming the token by source field instead of intent.**
|
|
232
|
+
`[LINKEDIN_BIO_PHRASE — ...]` is bad. `[PERSONALIZATION_LINE — ...]` is
|
|
233
|
+
good. The intent name tells the model what job to do; the source field
|
|
234
|
+
tells the model where to look but not what good looks like.
|
|
235
|
+
- **Rules in a separate table, intent in the template.** Keep them
|
|
236
|
+
together. The model's attention budget is finite — separating intent
|
|
237
|
+
from rules invites the model to forget the rules.
|
|
238
|
+
- **No FALLBACK clause.** Without it, the model assumes a fill is always
|
|
239
|
+
required and substitutes a generic noun ("your work") rather than
|
|
240
|
+
omitting. Always include FALLBACK.
|
|
241
|
+
- **Vague DO list.** `DO: be specific and human` is not a DO. Spell out
|
|
242
|
+
the verb-led shapes: `DO: use "your work helping X", "your writing on
|
|
243
|
+
Y", "Saw you're pushing into Z"`. The model needs prescriptions, not
|
|
244
|
+
vibes.
|
|
245
|
+
- **DON'T list that only describes obvious failures.** The DON'T clause
|
|
246
|
+
is most valuable when it names the non-obvious failures: product-noun
|
|
247
|
+
substitution, source-citation phrasing, mind-reading from signals,
|
|
248
|
+
generic noun substitutes.
|
|
249
|
+
- **Bracket containing freeform prose.** Keep the bracket's clauses
|
|
250
|
+
labeled (DO / DON'T / FALLBACK). Prose paragraphs are harder for the
|
|
251
|
+
model to scan and easier to ignore.
|
|
252
|
+
|
|
253
|
+
## Cross-references
|
|
254
|
+
|
|
255
|
+
- `mcp/sellable/skills/generate-messages/SKILL.md` — the skill that
|
|
256
|
+
invokes this token system at message-generation time. Carries the
|
|
257
|
+
hard-invariant rules (no product-noun substitution, omit-fallback as
|
|
258
|
+
default, etc.).
|
|
259
|
+
- `mcp/sellable/skills/create-campaign/references/token-fill-examples.md`
|
|
260
|
+
— example archive showing good/bad fills across many shapes.
|
|
261
|
+
- `mcp/sellable/skills/create-campaign-brief/references/brief-template.md`
|
|
262
|
+
— the brief template that authors should populate with AI-native tokens
|
|
263
|
+
by default.
|
|
264
|
+
- `mcp/sellable/skills/create-campaign-brief/references/phase75-active-runtime-message-pack.md`
|
|
265
|
+
— the runtime gold examples, including the original Sellable message
|
|
266
|
+
with `[PERSONALIZED REASON — ...]` and `[THEIR ICP — ...]`.
|
|
@@ -2,6 +2,16 @@
|
|
|
2
2
|
|
|
3
3
|
Use this file when template-style personalization is in play.
|
|
4
4
|
|
|
5
|
+
> **Read `ai-native-tokens.md` first.** The Sellable canonical token style is
|
|
6
|
+
> AI-native: bracketed `[ALL_CAPS_NAME — instructions]` placeholders that
|
|
7
|
+
> name what JOB the line does and carry DO / DON'T / FALLBACK rules inline.
|
|
8
|
+
> The model writes a sentence (or omits) per the inline contract — it does
|
|
9
|
+
> NOT fill a fixed slot. That spec is the source of truth; this file shows
|
|
10
|
+
> good/bad examples across shapes. Old-style `{{field}}` substitutions still
|
|
11
|
+
> apply for atomic values (first name, company name) but are insufficient for
|
|
12
|
+
> personalization sentences — see the Product-Noun Substitution section
|
|
13
|
+
> below for the failure mode they enable.
|
|
14
|
+
|
|
5
15
|
## Good Token Fill
|
|
6
16
|
|
|
7
17
|
Good token fill feels like a human wrote the sentence after seeing the
|
|
@@ -79,3 +89,86 @@ Revvix / security positioning:
|
|
|
79
89
|
- the token makes the sentence longer without making it better
|
|
80
90
|
- the token sounds like a compliment sandwich
|
|
81
91
|
- the token could be swapped into any message without changing meaning
|
|
92
|
+
|
|
93
|
+
## Product-Noun Substitution (HARD INVARIANT — block before fill)
|
|
94
|
+
|
|
95
|
+
When a token sits inside a possessive frame like `your {{X}}`, `at your {{X}}`,
|
|
96
|
+
or `because of your {{X}}`, the filled value must describe something the
|
|
97
|
+
recipient personally **does** — their work, focus, or activity. It must NOT
|
|
98
|
+
describe a product their company **builds or sells**.
|
|
99
|
+
|
|
100
|
+
The grammar test: read `your <filled-value>` out loud. If it reads as if the
|
|
101
|
+
recipient _uses_ or _has_ the thing, but the recipient actually _builds/sells_
|
|
102
|
+
the thing, the fill is wrong → OMIT the entire sentence.
|
|
103
|
+
|
|
104
|
+
**Allowed (buyer activities):**
|
|
105
|
+
|
|
106
|
+
- `monetization research for indie devs`
|
|
107
|
+
- `founder-led-sales experiments`
|
|
108
|
+
- `GTM Engineering writing`
|
|
109
|
+
- `outbound work for AI founders`
|
|
110
|
+
- `pricing-strategy advisory`
|
|
111
|
+
- `B2B SaaS GTM background` (history is fine — it describes the person)
|
|
112
|
+
|
|
113
|
+
**Blocked (product nouns from the prospect's company):**
|
|
114
|
+
|
|
115
|
+
- `Moneyball dashboard for CEOs` ← the prospect BUILDS this; they do not have one of their own
|
|
116
|
+
- `monetization layer for AI builders` ← product description, not buyer activity
|
|
117
|
+
- `AI scoring engine` / `platform for X` / `tool for X` / `API for X` ← any company-output noun
|
|
118
|
+
- `Series A funding round` ← event, not activity
|
|
119
|
+
|
|
120
|
+
Why this matters: the personalization line is supposed to make the buyer feel
|
|
121
|
+
recognized as a person. A product-noun fill instead makes the message sound
|
|
122
|
+
like it was written by an enrichment scraper — and worse, it implies we
|
|
123
|
+
mistook the founder for a customer of their own product.
|
|
124
|
+
|
|
125
|
+
**The omit-fallback is the safe default.** When you cannot produce a
|
|
126
|
+
buyer-activity phrase that survives the grammar test, OMIT the sentence
|
|
127
|
+
entirely. The message must read cleanly without the soft-bridge line. Do NOT
|
|
128
|
+
substitute a generic noun (`your work`, `your stack`, `your team`) — those
|
|
129
|
+
add no relevance and sound mail-merge-y. Aggressive omit > awkward fill.
|
|
130
|
+
|
|
131
|
+
When the brief defines a `{{workflow_context}}` token (or any
|
|
132
|
+
buyer-activity-shaped token), include this rule verbatim in the brief's Token
|
|
133
|
+
Fill Rules section so per-row generation has the constraint in scope.
|
|
134
|
+
|
|
135
|
+
## AI-Native Tokens (the canonical Sellable pattern)
|
|
136
|
+
|
|
137
|
+
Personalization sentences should be authored as AI-native tokens — bracketed
|
|
138
|
+
instructions, not field substitutions. Field substitutions (`{{first_name}}`,
|
|
139
|
+
`{{company}}`) work for atomic values that drop into a fixed slot. But any
|
|
140
|
+
sentence that requires _judgment_ about what to write should be authored as:
|
|
141
|
+
|
|
142
|
+
```text
|
|
143
|
+
[ALL_CAPS_NAME — Intent. DO: ... DON'T: ... FALLBACK: omit the line.]
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
The bracket lives inline in the message template. The model writes a sentence
|
|
147
|
+
(or omits the entire line) following the inline contract. The bracket itself
|
|
148
|
+
is replaced by the rendered sentence — or by nothing.
|
|
149
|
+
|
|
150
|
+
**Why:** every personalization failure documented in this file (mail-merge
|
|
151
|
+
phrasing, product-noun substitution, source-citation, generic noun
|
|
152
|
+
substitutes) shares one root cause: the model was filling a fixed-frame slot
|
|
153
|
+
under rules that lived elsewhere. AI-native tokens fix that by making the
|
|
154
|
+
model COMPOSE a sentence under rules that live INLINE.
|
|
155
|
+
|
|
156
|
+
**The original Sellable gold message (the canonical example)** uses two
|
|
157
|
+
AI-native tokens:
|
|
158
|
+
|
|
159
|
+
```text
|
|
160
|
+
we're looking for design partners — and [PERSONALIZED REASON — their team
|
|
161
|
+
size, role, or why they're a perfect fit].
|
|
162
|
+
|
|
163
|
+
a) 15-min call — i'll show you how you could book more meetings with
|
|
164
|
+
[THEIR ICP — who they want to reach], and if you like it we launch a pilot
|
|
165
|
+
right there
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
Notice: `[PERSONALIZED REASON]` and `[THEIR ICP]` name what JOB the line
|
|
169
|
+
does. The model produces a different rendered version per row, but every
|
|
170
|
+
version is grammatically clean because the model is _writing_, not
|
|
171
|
+
_filling_.
|
|
172
|
+
|
|
173
|
+
**Full spec, contract requirements, and brief-authoring guide:** see
|
|
174
|
+
`ai-native-tokens.md` in this same references directory.
|
|
@@ -162,6 +162,17 @@ next step, not a section the customer needs to study in detail.
|
|
|
162
162
|
Use 0-3 bullets max. If there is nothing special to note yet, say:
|
|
163
163
|
`None yet — validate from the first lead sample.`
|
|
164
164
|
|
|
165
|
+
**Personalization tokens — default to AI-native, not field substitution.**
|
|
166
|
+
Atomic field tokens (`{{first_name}}`, `{{company}}`) are fine for greeting,
|
|
167
|
+
subject, and any sentence that drops a single value into a fixed slot. Any
|
|
168
|
+
personalization SENTENCE (a hook line that anchors the message to the
|
|
169
|
+
prospect) should be authored as an AI-native bracket token in the message
|
|
170
|
+
template — `[INTENT_NAME — Intent. DO: ... DON'T: ... FALLBACK: omit the
|
|
171
|
+
line.]` — not as a `{{field}}` slot inside a fixed frame. The model writes
|
|
172
|
+
the sentence per the inline contract; if it can't satisfy the DOs cleanly,
|
|
173
|
+
it omits the entire line. Aggressive omit > awkward fill. Full spec and
|
|
174
|
+
examples: `mcp/sellable/skills/create-campaign/references/ai-native-tokens.md`.
|
|
175
|
+
|
|
165
176
|
## Next Steps
|
|
166
177
|
|
|
167
178
|
Keep this section simple and action-oriented. The customer should understand the
|