@sellable/mcp 0.1.32 → 0.1.34
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/server.js +3 -1
- package/dist/tools/auth.d.ts +8 -0
- package/dist/tools/auth.js +33 -1
- package/dist/tools/campaigns.js +13 -10
- package/dist/tools/cli-login.d.ts +6 -0
- package/dist/tools/cli-login.js +2 -1
- package/dist/tools/linkedin.d.ts +14 -1
- package/dist/tools/linkedin.js +10 -2
- package/package.json +1 -1
- package/skills/create-campaign/SKILL.md +48 -8
- 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/generate-messages/SKILL.md +38 -0
package/dist/server.js
CHANGED
|
@@ -412,7 +412,9 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
412
412
|
result = await fetchLinkedInPosts(args?.linkedinUrl, args?.limit);
|
|
413
413
|
break;
|
|
414
414
|
case "fetch_linkedin_profile":
|
|
415
|
-
result = await fetchLinkedInProfile(args?.linkedinUrl
|
|
415
|
+
result = await fetchLinkedInProfile(args?.linkedinUrl, {
|
|
416
|
+
full: args?.full === true,
|
|
417
|
+
});
|
|
416
418
|
break;
|
|
417
419
|
case "fetch_company":
|
|
418
420
|
result = await fetchCompany(args?.companyUrl);
|
package/dist/tools/auth.d.ts
CHANGED
|
@@ -18,6 +18,14 @@ export type AuthStatus = {
|
|
|
18
18
|
message: string;
|
|
19
19
|
guidance: string;
|
|
20
20
|
};
|
|
21
|
+
/**
|
|
22
|
+
* Verbatim instruction for the AGENT (not the user) when auth is missing.
|
|
23
|
+
* The agent MUST follow this verbatim and MUST NOT show error.guidance to
|
|
24
|
+
* the user — that's a non-interactive fallback. This field is the single
|
|
25
|
+
* source of truth for the FTUX magic-link flow so even a stale skill copy
|
|
26
|
+
* does the right thing.
|
|
27
|
+
*/
|
|
28
|
+
agentInstruction?: string;
|
|
21
29
|
};
|
|
22
30
|
export declare const authToolDefinitions: {
|
|
23
31
|
name: string;
|
package/dist/tools/auth.js
CHANGED
|
@@ -22,7 +22,7 @@ function appendUpdateNotice(notice, update) {
|
|
|
22
22
|
export const authToolDefinitions = [
|
|
23
23
|
{
|
|
24
24
|
name: "get_auth_status",
|
|
25
|
-
description: "Verify Sellable authentication early. Call this before any campaign work. Also performs a cached Sellable package update check. Returns ok=false with clear guidance when the token is missing, revoked, or the active workspace is not set. IMPORTANT: When auth succeeds, ALWAYS tell the user the _userNotice message so they know which workspace is active and whether an update is available.",
|
|
25
|
+
description: "Verify Sellable authentication early. Call this before any campaign work. Also performs a cached Sellable package update check. Returns ok=false with clear guidance when the token is missing, revoked, or the active workspace is not set. IMPORTANT: When auth succeeds, ALWAYS tell the user the _userNotice message so they know which workspace is active and whether an update is available. CRITICAL: When auth FAILS with a missing/invalid token (error.type is 'config' or 'auth'), the response includes an `agentInstruction` field with the verbatim FTUX magic-link signup flow. Follow agentInstruction VERBATIM. Do NOT show the user error.guidance — that's a fallback for non-interactive contexts only.",
|
|
26
26
|
inputSchema: {
|
|
27
27
|
type: "object",
|
|
28
28
|
properties: {},
|
|
@@ -110,6 +110,36 @@ export async function getAuthStatus() {
|
|
|
110
110
|
};
|
|
111
111
|
}
|
|
112
112
|
catch (error) {
|
|
113
|
+
// FTUX directive: when the user is not yet signed in, tell the AGENT
|
|
114
|
+
// (not the user) to run the magic-link CLI handoff verbatim. Treat this
|
|
115
|
+
// as the single source of truth so even a stale skill does the right
|
|
116
|
+
// thing instead of dumping the technical fallback guidance.
|
|
117
|
+
const ftuxAgentInstruction = "AGENT INSTRUCTION (verbatim, do not paraphrase, do not show the user `error.guidance`): " +
|
|
118
|
+
"The user has not signed in to Sellable yet. Run the magic-link CLI handoff. " +
|
|
119
|
+
"1) Say to the user verbatim:\\n" +
|
|
120
|
+
"`Welcome to Sellable! I'll help you launch a LinkedIn outbound campaign right here, all via chat — leads, messages, the whole thing.\\n\\n" +
|
|
121
|
+
"First, let's connect your Sellable account:\\n\\n" +
|
|
122
|
+
" 1. Drop your email below\\n" +
|
|
123
|
+
" 2. I'll send a magic login link to your inbox\\n" +
|
|
124
|
+
" 3. Click it, come back here, and we'll keep going\\n\\n" +
|
|
125
|
+
"What email should I use?` " +
|
|
126
|
+
"2) Wait for the user to type their email in normal chat (do NOT use AskUserQuestion / request_user_input). " +
|
|
127
|
+
"3) Call `mcp__sellable__start_cli_login({ email })` with that email. " +
|
|
128
|
+
"4) On `ok: true`, say verbatim (substituting the email exactly as typed):\\n" +
|
|
129
|
+
"`Magic link sent to {email}.\\n\\n" +
|
|
130
|
+
"─────────────────────────────────────────────\\n" +
|
|
131
|
+
" Your turn — check your inbox\\n" +
|
|
132
|
+
"─────────────────────────────────────────────\\n\\n" +
|
|
133
|
+
" 1. Open the email from Sellable\\n" +
|
|
134
|
+
" 2. Click the magic link\\n" +
|
|
135
|
+
" 3. Come back here when you're done\\n\\n" +
|
|
136
|
+
"I'll be waiting right here.\\n\\n" +
|
|
137
|
+
" (If your team already uses Sellable, ask an admin to invite you into their shared workspace instead — that gets you straight in.)` " +
|
|
138
|
+
"5) Call `mcp__sellable__wait_for_cli_login({ sessionId })` using the sessionId returned by start_cli_login. " +
|
|
139
|
+
"6) If the result is `error.type === 'tool_timeout_guard'`, IMMEDIATELY re-call wait_for_cli_login with the SAME sessionId — do not narrate, do not call start_cli_login again. Loop until you get a different result. " +
|
|
140
|
+
"7) On `ok: true`, the user is signed in and `~/.sellable/config.json` has been written. Branch on `isReturningUser`: " +
|
|
141
|
+
"if true, say `You're in — {activeWorkspaceName} workspace, ready to roll.\\n\\nNow — paste the LinkedIn profile URL of the person you'll be sending campaigns from. Usually that's you (the founder), or whoever's voice the messages should sound like.\\n\\ne.g. https://www.linkedin.com/in/your-handle`; " +
|
|
142
|
+
"if false, say `You're set up — your {activeWorkspaceName} workspace is ready.\\n\\nNow — paste the LinkedIn profile URL of the person you'll be sending campaigns from. Usually that's you (the founder), or whoever's voice the messages should sound like.\\n\\ne.g. https://www.linkedin.com/in/your-handle`";
|
|
113
143
|
if (error instanceof SellableApiError && error.isAuthError) {
|
|
114
144
|
return {
|
|
115
145
|
...base,
|
|
@@ -120,6 +150,7 @@ export async function getAuthStatus() {
|
|
|
120
150
|
guidance: error.guidance ||
|
|
121
151
|
`Update ${configPath} with a valid token, then retry get_auth_status.`,
|
|
122
152
|
},
|
|
153
|
+
agentInstruction: ftuxAgentInstruction,
|
|
123
154
|
};
|
|
124
155
|
}
|
|
125
156
|
const message = error instanceof Error ? error.message : String(error);
|
|
@@ -136,6 +167,7 @@ export async function getAuthStatus() {
|
|
|
136
167
|
message,
|
|
137
168
|
guidance,
|
|
138
169
|
},
|
|
170
|
+
agentInstruction: isConfigError ? ftuxAgentInstruction : undefined,
|
|
139
171
|
};
|
|
140
172
|
}
|
|
141
173
|
}
|
package/dist/tools/campaigns.js
CHANGED
|
@@ -123,7 +123,7 @@ export const campaignToolDefinitions = [
|
|
|
123
123
|
},
|
|
124
124
|
{
|
|
125
125
|
name: "create_campaign",
|
|
126
|
-
description: 'Create a new campaign offer OR resume an existing one. Low-level write tool: load create-campaign workflow instructions first via get_subskill_prompt({ subskillName: "create-campaign" }) (or bootstrap_create_campaign + nextStep). If campaignId is provided, this tool returns the watchUrl + state for that campaign instead of creating a new campaign.\n\nINPUTS:\n-
|
|
126
|
+
description: 'Create a new campaign offer OR resume an existing one. Low-level write tool: load create-campaign workflow instructions first via get_subskill_prompt({ subskillName: "create-campaign" }) (or bootstrap_create_campaign + nextStep). If campaignId is provided, this tool returns the watchUrl + state for that campaign instead of creating a new campaign.\n\nINPUTS:\n- `clientProspectId` is REQUIRED for net-new campaigns. It is the EnrichedProspect row ID for the campaign sender (used by ICP scoring, message generation, brief rendering — every downstream consumer reads sender context from `campaign.clientProspect`).\n- To obtain a `clientProspectId`, run the sender-enrichment flow first: this materializes an EnrichedProspect row from the sender\'s LinkedIn profile URL and returns its ID.\n- `senderLinkedinUrl` may be passed as informational metadata but is NOT a substitute for `clientProspectId` — the API will reject the create if `clientProspectId` is missing.\n\nPREREQUISITES:\n- The /research-sender skill must have been run for this sender (produces enrichment data and the EnrichedProspect row whose ID is `clientProspectId`).',
|
|
127
127
|
inputSchema: {
|
|
128
128
|
type: "object",
|
|
129
129
|
properties: {
|
|
@@ -137,13 +137,13 @@ export const campaignToolDefinitions = [
|
|
|
137
137
|
},
|
|
138
138
|
clientProspectId: {
|
|
139
139
|
type: "string",
|
|
140
|
-
description: "
|
|
140
|
+
description: "REQUIRED for net-new campaigns. EnrichedProspect row ID for the campaign sender. Produced by the sender-enrichment flow (research-sender). The API will reject the create with a 400 if this is missing.",
|
|
141
141
|
},
|
|
142
|
-
//
|
|
142
|
+
// `senderLinkedinUrl` (not `linkedinUrl`) to disambiguate from
|
|
143
143
|
// prospect-side `linkedinUrl` fields used in other tools (research-prospect etc.).
|
|
144
144
|
senderLinkedinUrl: {
|
|
145
145
|
type: "string",
|
|
146
|
-
description: "Optional
|
|
146
|
+
description: "Optional informational metadata: the sender's LinkedIn profile URL. NOT a substitute for clientProspectId — to materialize a prospect from a URL, run the sender-enrichment flow first.",
|
|
147
147
|
},
|
|
148
148
|
offerPositioning: {
|
|
149
149
|
type: "object",
|
|
@@ -597,12 +597,15 @@ export async function createCampaign(input) {
|
|
|
597
597
|
missing.push("name");
|
|
598
598
|
if (!input.campaignBrief)
|
|
599
599
|
missing.push("campaignBrief");
|
|
600
|
-
|
|
601
|
-
|
|
600
|
+
// clientProspectId is required for net-new campaigns. Every downstream
|
|
601
|
+
// consumer (ICP scoring, message generation, brief rendering) reads sender
|
|
602
|
+
// context from `campaign.clientProspect`. senderLinkedinUrl is informational
|
|
603
|
+
// metadata only — it is NOT a substitute and the API will reject the create
|
|
604
|
+
// if clientProspectId is missing.
|
|
605
|
+
if (!input.clientProspectId) {
|
|
606
|
+
missing.push("clientProspectId");
|
|
602
607
|
}
|
|
603
|
-
//
|
|
604
|
-
// Don't deep-validate; just reject obvious garbage so the downstream
|
|
605
|
-
// flow doesn't try to fetch a non-LinkedIn URL.
|
|
608
|
+
// Cheap URL sanity check on senderLinkedinUrl when supplied.
|
|
606
609
|
if (input.senderLinkedinUrl &&
|
|
607
610
|
typeof input.senderLinkedinUrl === "string" &&
|
|
608
611
|
!input.senderLinkedinUrl.includes("linkedin.com")) {
|
|
@@ -614,7 +617,7 @@ export async function createCampaign(input) {
|
|
|
614
617
|
`or all create fields. Missing: ${missing.join(", ")}.\n\n` +
|
|
615
618
|
"Remediation:\n" +
|
|
616
619
|
'- For full workflow, call get_subskill_prompt({ subskillName: "create-campaign" }) and follow it.\n' +
|
|
617
|
-
|
|
620
|
+
"- For net-new campaign creation: run the `research-sender` flow first to materialize an EnrichedProspect for the sender from their LinkedIn profile URL, then pass the resulting prospect ID as `clientProspectId`. The campaign sender context (used by ICP scoring, message generation, brief rendering) is read from `campaign.clientProspect`, so this row must exist before the campaign is minted.\n" +
|
|
618
621
|
"- For resume, call create_campaign with campaignId only.");
|
|
619
622
|
}
|
|
620
623
|
assertNetNewCreateCampaignResearchReady();
|
|
@@ -38,6 +38,12 @@ type WaitForCliLoginSuccess = {
|
|
|
38
38
|
activeWorkspaceId: string;
|
|
39
39
|
activeWorkspaceName?: string;
|
|
40
40
|
configPath: string;
|
|
41
|
+
/**
|
|
42
|
+
* True if the server reused the user's existing default workspace
|
|
43
|
+
* (vs creating a new one). The skill uses this to branch the
|
|
44
|
+
* post-auth greeting copy.
|
|
45
|
+
*/
|
|
46
|
+
isReturningUser: boolean;
|
|
41
47
|
/** Locked verbatim string the agent shows the user. */
|
|
42
48
|
humanMessage: string;
|
|
43
49
|
};
|
package/dist/tools/cli-login.js
CHANGED
|
@@ -131,7 +131,7 @@ export async function handleStartCliLogin(args) {
|
|
|
131
131
|
sessionId: body.sessionId,
|
|
132
132
|
confirmUrl: body.confirmUrl,
|
|
133
133
|
expiresInSec: body.expiresInSec,
|
|
134
|
-
humanMessage: `Magic link sent to ${args.email}
|
|
134
|
+
humanMessage: `Magic link sent to ${args.email}.\n\n─────────────────────────────────────────────\n Your turn — check your inbox\n─────────────────────────────────────────────\n\n 1. Open the email from Sellable\n 2. Click the magic link\n 3. Come back here when you're done\n\nI'll be waiting right here.\n\n (If your team already uses Sellable, ask an admin to invite you into their shared workspace instead — that gets you straight in.)`,
|
|
135
135
|
};
|
|
136
136
|
}
|
|
137
137
|
export async function handleWaitForCliLogin(args) {
|
|
@@ -187,6 +187,7 @@ export async function handleWaitForCliLogin(args) {
|
|
|
187
187
|
? { activeWorkspaceName: body.activeWorkspaceName }
|
|
188
188
|
: {}),
|
|
189
189
|
configPath,
|
|
190
|
+
isReturningUser: body.isReturningUser ?? false,
|
|
190
191
|
humanMessage: "Signed in. Continuing.",
|
|
191
192
|
};
|
|
192
193
|
}
|
package/dist/tools/linkedin.d.ts
CHANGED
|
@@ -15,6 +15,7 @@ export declare const linkedinToolDefinitions: ({
|
|
|
15
15
|
};
|
|
16
16
|
postUrl?: undefined;
|
|
17
17
|
sources?: undefined;
|
|
18
|
+
full?: undefined;
|
|
18
19
|
companyUrl?: undefined;
|
|
19
20
|
sortBy?: undefined;
|
|
20
21
|
linkedin_url?: undefined;
|
|
@@ -44,6 +45,7 @@ export declare const linkedinToolDefinitions: ({
|
|
|
44
45
|
default: number;
|
|
45
46
|
};
|
|
46
47
|
linkedinUrl?: undefined;
|
|
48
|
+
full?: undefined;
|
|
47
49
|
companyUrl?: undefined;
|
|
48
50
|
sortBy?: undefined;
|
|
49
51
|
linkedin_url?: undefined;
|
|
@@ -61,6 +63,11 @@ export declare const linkedinToolDefinitions: ({
|
|
|
61
63
|
type: string;
|
|
62
64
|
description: string;
|
|
63
65
|
};
|
|
66
|
+
full: {
|
|
67
|
+
type: string;
|
|
68
|
+
description: string;
|
|
69
|
+
default: boolean;
|
|
70
|
+
};
|
|
64
71
|
limit?: undefined;
|
|
65
72
|
postUrl?: undefined;
|
|
66
73
|
sources?: undefined;
|
|
@@ -85,6 +92,7 @@ export declare const linkedinToolDefinitions: ({
|
|
|
85
92
|
limit?: undefined;
|
|
86
93
|
postUrl?: undefined;
|
|
87
94
|
sources?: undefined;
|
|
95
|
+
full?: undefined;
|
|
88
96
|
sortBy?: undefined;
|
|
89
97
|
linkedin_url?: undefined;
|
|
90
98
|
max_posts?: undefined;
|
|
@@ -115,6 +123,7 @@ export declare const linkedinToolDefinitions: ({
|
|
|
115
123
|
linkedinUrl?: undefined;
|
|
116
124
|
postUrl?: undefined;
|
|
117
125
|
sources?: undefined;
|
|
126
|
+
full?: undefined;
|
|
118
127
|
linkedin_url?: undefined;
|
|
119
128
|
max_posts?: undefined;
|
|
120
129
|
};
|
|
@@ -139,6 +148,7 @@ export declare const linkedinToolDefinitions: ({
|
|
|
139
148
|
limit?: undefined;
|
|
140
149
|
postUrl?: undefined;
|
|
141
150
|
sources?: undefined;
|
|
151
|
+
full?: undefined;
|
|
142
152
|
companyUrl?: undefined;
|
|
143
153
|
sortBy?: undefined;
|
|
144
154
|
};
|
|
@@ -158,6 +168,7 @@ export declare const linkedinToolDefinitions: ({
|
|
|
158
168
|
limit?: undefined;
|
|
159
169
|
postUrl?: undefined;
|
|
160
170
|
sources?: undefined;
|
|
171
|
+
full?: undefined;
|
|
161
172
|
companyUrl?: undefined;
|
|
162
173
|
sortBy?: undefined;
|
|
163
174
|
max_posts?: undefined;
|
|
@@ -177,7 +188,9 @@ export declare function fetchLinkedInPosts(linkedinUrl: string, limit?: number):
|
|
|
177
188
|
posts: SerializedPost[];
|
|
178
189
|
count: number;
|
|
179
190
|
}>;
|
|
180
|
-
export declare function fetchLinkedInProfile(linkedinUrl: string
|
|
191
|
+
export declare function fetchLinkedInProfile(linkedinUrl: string, options?: {
|
|
192
|
+
full?: boolean;
|
|
193
|
+
}): Promise<unknown>;
|
|
181
194
|
export declare function fetchCompany(companyUrl: string): Promise<unknown>;
|
|
182
195
|
export declare function fetchCompanyPosts(companyUrl: string, limit?: number, sortBy?: "recent" | "top"): Promise<{
|
|
183
196
|
posts: SerializedPost[];
|
package/dist/tools/linkedin.js
CHANGED
|
@@ -46,7 +46,7 @@ export const linkedinToolDefinitions = [
|
|
|
46
46
|
},
|
|
47
47
|
{
|
|
48
48
|
name: "fetch_linkedin_profile",
|
|
49
|
-
description: "Fetch LinkedIn profile details
|
|
49
|
+
description: "Fetch LinkedIn profile details. Defaults to the cheaper 'main' payload — same envelope shape with most-recent role, headline, about, location, education, top skills, certifications, languages, projects, etc. Pass full=true ONLY when you need the long-tail experience history (positions 6+) or complete skill list (skills 3+) for deep research. Most personalization, qualification, and outreach use cases work with main; reach for full only when the user explicitly asks for full work history or comprehensive skill audit.",
|
|
50
50
|
inputSchema: {
|
|
51
51
|
type: "object",
|
|
52
52
|
properties: {
|
|
@@ -54,6 +54,11 @@ export const linkedinToolDefinitions = [
|
|
|
54
54
|
type: "string",
|
|
55
55
|
description: "Full LinkedIn profile URL",
|
|
56
56
|
},
|
|
57
|
+
full: {
|
|
58
|
+
type: "boolean",
|
|
59
|
+
description: "Opt into the full payload (~38% more expensive, slightly slower). Set true only when you specifically need the long-tail experience history or full skill list. Default false (main payload).",
|
|
60
|
+
default: false,
|
|
61
|
+
},
|
|
57
62
|
},
|
|
58
63
|
required: ["linkedinUrl"],
|
|
59
64
|
},
|
|
@@ -152,9 +157,12 @@ export async function fetchLinkedInPosts(linkedinUrl, limit = 25) {
|
|
|
152
157
|
count: posts.length,
|
|
153
158
|
};
|
|
154
159
|
}
|
|
155
|
-
export async function fetchLinkedInProfile(linkedinUrl) {
|
|
160
|
+
export async function fetchLinkedInProfile(linkedinUrl, options = {}) {
|
|
156
161
|
const api = getApi();
|
|
157
162
|
const params = new URLSearchParams({ linkedinUrl });
|
|
163
|
+
if (options.full) {
|
|
164
|
+
params.set("full", "true");
|
|
165
|
+
}
|
|
158
166
|
return api.get(`/api/v1/scrape/linkedin/profile?${params.toString()}`);
|
|
159
167
|
}
|
|
160
168
|
export async function fetchCompany(companyUrl) {
|
package/package.json
CHANGED
|
@@ -331,7 +331,15 @@ updates.
|
|
|
331
331
|
a. Say to the user verbatim:
|
|
332
332
|
|
|
333
333
|
```text
|
|
334
|
-
Welcome to Sellable
|
|
334
|
+
Welcome to Sellable! I'll help you launch a LinkedIn outbound campaign right here, all via chat — leads, messages, the whole thing.
|
|
335
|
+
|
|
336
|
+
First, let's connect your Sellable account:
|
|
337
|
+
|
|
338
|
+
1. Drop your email below
|
|
339
|
+
2. I'll send a magic login link to your inbox
|
|
340
|
+
3. Click it, come back here, and we'll keep going
|
|
341
|
+
|
|
342
|
+
What email should I use?
|
|
335
343
|
```
|
|
336
344
|
|
|
337
345
|
b. Wait for the user to paste their email in normal chat. Do NOT use
|
|
@@ -347,7 +355,19 @@ updates.
|
|
|
347
355
|
as the user typed it):
|
|
348
356
|
|
|
349
357
|
```text
|
|
350
|
-
Magic link sent to {email}.
|
|
358
|
+
Magic link sent to {email}.
|
|
359
|
+
|
|
360
|
+
─────────────────────────────────────────────
|
|
361
|
+
Your turn — check your inbox
|
|
362
|
+
─────────────────────────────────────────────
|
|
363
|
+
|
|
364
|
+
1. Open the email from Sellable
|
|
365
|
+
2. Click the magic link
|
|
366
|
+
3. Come back here when you're done
|
|
367
|
+
|
|
368
|
+
I'll be waiting right here.
|
|
369
|
+
|
|
370
|
+
(If your team already uses Sellable, ask an admin to invite you into their shared workspace instead — that gets you straight in.)
|
|
351
371
|
```
|
|
352
372
|
|
|
353
373
|
f. Call `mcp__sellable__wait_for_cli_login({ sessionId })` using the
|
|
@@ -370,13 +390,33 @@ updates.
|
|
|
370
390
|
`error.guidance` and stop.
|
|
371
391
|
|
|
372
392
|
- On `ok: true`, the user is signed in and `~/.sellable/config.json` has
|
|
373
|
-
been written. Your IMMEDIATE next visible message
|
|
374
|
-
|
|
375
|
-
signed in", no acknowledgement of any kind):
|
|
393
|
+
been written. Your IMMEDIATE next visible message branches on
|
|
394
|
+
`isReturningUser` from the tool result:
|
|
376
395
|
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
396
|
+
- If `isReturningUser === true`, prepend ONE line acknowledging the
|
|
397
|
+
reused workspace, then the locked Step 3 narration verbatim
|
|
398
|
+
(substituting `activeWorkspaceName` exactly):
|
|
399
|
+
|
|
400
|
+
```text
|
|
401
|
+
You're in — {activeWorkspaceName} workspace, ready to roll.
|
|
402
|
+
|
|
403
|
+
Now — paste the LinkedIn profile URL of the person you'll be sending campaigns from. Usually that's you (the founder), or whoever's voice the messages should sound like.
|
|
404
|
+
|
|
405
|
+
e.g. https://www.linkedin.com/in/your-handle
|
|
406
|
+
```
|
|
407
|
+
|
|
408
|
+
- If `isReturningUser === false`, prepend ONE line confirming the new
|
|
409
|
+
workspace, then the locked Step 3 narration verbatim:
|
|
410
|
+
|
|
411
|
+
```text
|
|
412
|
+
You're set up — your {activeWorkspaceName} workspace is ready.
|
|
413
|
+
|
|
414
|
+
Now — paste the LinkedIn profile URL of the person you'll be sending campaigns from. Usually that's you (the founder), or whoever's voice the messages should sound like.
|
|
415
|
+
|
|
416
|
+
e.g. https://www.linkedin.com/in/your-handle
|
|
417
|
+
```
|
|
418
|
+
|
|
419
|
+
No other lines. No "all set", no "signed in", no other acknowledgement.
|
|
380
420
|
|
|
381
421
|
After the user pastes the URL, proceed with the existing identity-first
|
|
382
422
|
sender flow (Step 3 onwards in the v2 subskill prompt — sender
|
|
@@ -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
|
|
@@ -1064,11 +1064,17 @@ proof:`, `p.s. useful proof:`, `p.s. proof:`, or `p.s. social proof:`.
|
|
|
1064
1064
|
`{{psLine}}`, and do not use `{{recent_signal_quote}}`; use fields that exist
|
|
1065
1065
|
on the enriched prospect row instead.
|
|
1066
1066
|
- the live body under `## Approved Message Template` must be sender-ready copy.
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1067
|
+
Bracketed text in the body is allowed only when it is a fully-specified
|
|
1068
|
+
AI-native token (`[ALL_CAPS_NAME — Intent. DO: ... DON'T: ... FALLBACK:
|
|
1069
|
+
omit the line.]` — see
|
|
1070
|
+
`mcp/sellable/skills/create-campaign/references/ai-native-tokens.md` for the
|
|
1071
|
+
contract). Bracketed instruction placeholders that lack a contract are
|
|
1072
|
+
BLOCKED — including `[ROW BRIDGE ...]`, `[insert ...]`, `[generated ...]`,
|
|
1073
|
+
and any prose that defers per-row composition to a later step without
|
|
1074
|
+
giving the model the rules. Either upgrade the placeholder to a full
|
|
1075
|
+
AI-native token (Intent / DO / DON'T / FALLBACK), put the per-row rules in
|
|
1076
|
+
`## Token Fill Rules` with concrete enriched-row fields, or route to
|
|
1077
|
+
`revise-messaging`.
|
|
1072
1078
|
- `approval-packet.md` and the live campaign brief passed to `create_campaign`
|
|
1073
1079
|
must include `## Approved Message Template`, `## Token Fill Rules`, and
|
|
1074
1080
|
`## Token Fill Examples`. `## Token Fill Examples` must copy the approved
|
|
@@ -1391,25 +1397,39 @@ Exact sequence:
|
|
|
1391
1397
|
`campaignBrief.content` contains `{{...}}`; without this marker it will
|
|
1392
1398
|
use full-generation mode and may rewrite the approved message.
|
|
1393
1399
|
The live template body must contain only sender-ready copy plus supported
|
|
1394
|
-
|
|
1395
|
-
|
|
1396
|
-
|
|
1397
|
-
|
|
1398
|
-
|
|
1399
|
-
|
|
1400
|
-
|
|
1401
|
-
|
|
1402
|
-
|
|
1403
|
-
|
|
1404
|
-
|
|
1405
|
-
|
|
1406
|
-
|
|
1407
|
-
|
|
1408
|
-
|
|
1409
|
-
|
|
1410
|
-
|
|
1411
|
-
|
|
1412
|
-
|
|
1400
|
+
tokens. Two token shapes are allowed:
|
|
1401
|
+
- `{{snake_case}}` field-substitution tokens for atomic values that drop
|
|
1402
|
+
directly into a fixed slot (greeting, subject, company name).
|
|
1403
|
+
- `[ALL_CAPS_NAME — Intent. DO: ... DON'T: ... FALLBACK: omit the line.]`
|
|
1404
|
+
AI-native tokens for personalization sentences. The bracket carries
|
|
1405
|
+
the contract inline; the model writes a sentence (or omits the line)
|
|
1406
|
+
per the inline rules. Full spec:
|
|
1407
|
+
`mcp/sellable/skills/create-campaign/references/ai-native-tokens.md`.
|
|
1408
|
+
No other bracketed placeholders are allowed — bracketed prose without an
|
|
1409
|
+
explicit Intent / DO / DON'T / FALLBACK contract (e.g., `[ROW BRIDGE]`,
|
|
1410
|
+
`[insert ...]`, `[generated ...]`) is BLOCKED because it leaves the
|
|
1411
|
+
per-row generation under-specified.
|
|
1412
|
+
Include `## Token Fill Rules` for every token in the template. Field
|
|
1413
|
+
tokens may be row fields such as `{{first_name}}`, `{{company}}`,
|
|
1414
|
+
`{{role}}`, `{{headline}}`, `{{profile_summary}}`, `{{post_context}}`,
|
|
1415
|
+
`{{comment_summary}}`, `{{source_post_topic}}`, or `{{row_proof_note}}`
|
|
1416
|
+
when those exact fields are supported by the row/enrichment data. AI-native
|
|
1417
|
+
tokens are listed in the rules table by name and type with a pointer back
|
|
1418
|
+
to the inline contract (the rules themselves live inside the bracket — do
|
|
1419
|
+
not duplicate them in the table). Do not document static sender identity,
|
|
1420
|
+
product name, proof points, casing style, `{{recent_signal_quote}}`, or
|
|
1421
|
+
abstract slot tokens such as `{{hookLine}}`, `{{painLine}}`,
|
|
1422
|
+
`{{productLine}}`, `{{closeLine}}`, or `{{psLine}}` — those are abstract
|
|
1423
|
+
slots without a contract; convert them to AI-native bracket tokens with
|
|
1424
|
+
real Intent / DO / DON'T / FALLBACK clauses, or cut them.
|
|
1425
|
+
Include `## Token Fill Examples` copied from the approved message review and
|
|
1426
|
+
validation artifacts. It must preserve `Good token fill:`, `Good omit:`,
|
|
1427
|
+
`Bad token fill:`, `Why bad:`, `Fallback if missing:`, and
|
|
1428
|
+
`Token fill basis:` so the minted campaign brief teaches future row
|
|
1429
|
+
generation how to fill tokens, what fills are blocked, and what to do when
|
|
1430
|
+
row data is missing. If the brief does not contain `## Token Fill Rules` and
|
|
1431
|
+
`## Token Fill Examples`, do not call `create_campaign`; route back to
|
|
1432
|
+
message review or approval packet generation.
|
|
1413
1433
|
3. Call `bootstrap_create_campaign({ flowVersion: "v2" })`. Respect
|
|
1414
1434
|
`safeToProceed`; on blocking errors, surface and stop.
|
|
1415
1435
|
4. Call
|
|
@@ -1139,6 +1139,44 @@ yours` are not enough when a safe row token would make the line feel more
|
|
|
1139
1139
|
version sounds like mail merge. A good token should either make the sentence
|
|
1140
1140
|
more concrete in normal language or be omitted. If the row signal is weak,
|
|
1141
1141
|
write the segment-level line and document the omit rule.
|
|
1142
|
+
- **No product-noun substitution in possessive frames (HARD INVARIANT):** when
|
|
1143
|
+
a token sits inside a possessive frame like `your {{X}}`, `at your {{X}}`,
|
|
1144
|
+
or `because of your {{X}}`, the filled value MUST describe something the
|
|
1145
|
+
recipient personally **does** — their work, focus, or activity. It must NOT
|
|
1146
|
+
describe a product their company **builds or sells**. Grammar test: read
|
|
1147
|
+
`your <filled-value>` out loud. If it reads as if the recipient _uses_ or
|
|
1148
|
+
_has_ the thing, but they actually _build/sell_ the thing, the fill is wrong
|
|
1149
|
+
→ OMIT the entire sentence. Examples: `your monetization layer for AI
|
|
1150
|
+
builders` (BLOCKED — product description from the company), `your Moneyball
|
|
1151
|
+
dashboard for CEOs` (BLOCKED — the recipient builds this product, doesn't
|
|
1152
|
+
have one of their own), `your AI scoring engine` (BLOCKED — product), vs
|
|
1153
|
+
`your monetization research for AI founders` (ALLOWED — buyer activity),
|
|
1154
|
+
`your founder-led-sales experiments` (ALLOWED — activity), `your GTM
|
|
1155
|
+
Engineering writing` (ALLOWED — what they do publicly). The omit-fallback
|
|
1156
|
+
is the safe default. Aggressive omit > awkward fill. Do NOT substitute
|
|
1157
|
+
generic nouns (`your work`, `your stack`, `your team`) — those add no
|
|
1158
|
+
relevance and read as mail merge.
|
|
1159
|
+
- **AI-native tokens — write the sentence, don't fill the slot (HARD
|
|
1160
|
+
INVARIANT):** when the message template contains a bracketed instruction
|
|
1161
|
+
token of the form \`[ALL_CAPS_NAME — instructions]\`, that placeholder is
|
|
1162
|
+
NOT a field-substitution slot. It is an inline instruction. Read the
|
|
1163
|
+
contents of the bracket. The bracket will name an Intent, DO shapes,
|
|
1164
|
+
DON'T shapes, and a FALLBACK clause. Your job: COMPOSE a complete
|
|
1165
|
+
sentence (or sentences, if the intent calls for it) that satisfies all
|
|
1166
|
+
the DO rules and avoids all the DON'T rules, then replace the bracket
|
|
1167
|
+
with that rendered sentence. If you cannot satisfy the DOs cleanly from
|
|
1168
|
+
the row data, REPLACE THE BRACKET WITH NOTHING (the entire line is
|
|
1169
|
+
omitted, including any leading/trailing whitespace; the surrounding
|
|
1170
|
+
message must read cleanly without it). Aggressive omit > awkward fill.
|
|
1171
|
+
Never leave the bracket itself in the rendered output. Never substitute
|
|
1172
|
+
a placeholder, ellipsis, or generic noun (\`your work\`, \`your team\`)
|
|
1173
|
+
when the FALLBACK clause says omit. Canonical example from the runtime
|
|
1174
|
+
gold pack: \`[PERSONALIZED REASON — their team size, role, or why
|
|
1175
|
+
they're a perfect fit]\` — the model picks whichever input the row supports
|
|
1176
|
+
and writes one short sentence in the sender's voice. Full spec:
|
|
1177
|
+
\`references/ai-native-tokens.md\`. The \`[ALL_CAPS_NAME — ...]\` shape
|
|
1178
|
+
is reserved for AI-native tokens; field substitutions stay as
|
|
1179
|
+
\`{{snake_case}}\` and continue to work as direct value injections.
|
|
1142
1180
|
- **No internal profile-signal token:** `{{profile_signal}}` is never allowed
|
|
1143
1181
|
in customer-facing copy, message-review templates, rendered examples, token
|
|
1144
1182
|
notes, or approval-packet message bodies. It names how enrichment classified
|