top-rep-playbook.ts48.4 KBView on GitHub /**
* Mock "Top rep playbook" — a set of linked Cedar Docs.
*
* Main doc (user/files/top-rep-playbook)
* ## Org-wide conversion rate
* ## Per rep conversion rate — cards link to each rep's subdocument
* ## Playbook
* ### Follow-up — meaningful response/follow-up stats by outcome
* ### Stage 1–4 — common patterns · objections · messaging ·
* features asked · pain points (all with win rates)
*
* Per-rep subdocs (user/files/users/<slug>) — that rep vs the team average + playbook
* Per-objection subdocs (user/files/playbook/<stage>/objections/<slug>) — talk-track stats
*
* Every source is mock-backed, so the whole set renders with no database access.
* `playbookDocuments[0]` is the main doc; the rest are linked subdocuments.
*/
export interface PlaybookDocument {
path: string;
title: string;
markdown: string;
}
// ── helpers ───────────────────────────────────────────────────────────────────
let specSeq = 0;
function fence(spec: Record<string, unknown>): string {
spec.spec_version = '2.0';
spec.id = (spec.id as string) ?? `mock_${specSeq++}`;
return '```dashboard\n' + JSON.stringify(spec, null, 2) + '\n```';
}
const USERS_BASE = 'user/files/users';
const OBJ_BASE = 'user/files/playbook';
const MAIN_PATH = 'user/files/top-rep-playbook';
const slug = (s: string) =>
s.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-+|-+$/g, '').slice(0, 48);
const hours = (h: number) => Math.round(h * 3600);
const days = (d: number) => Math.round(d * 86400);
// ── reps ───────────────────────────────────────────────────────────────────────
const STAGE_LABELS = ['Discovery', 'Demo', 'Negot.', 'Close'];
const steps = (counts: number[]) => counts.map((count, i) => ({ label: STAGE_LABELS[i], count }));
interface PlaybookStageCard {
num: string;
title: string;
goal?: string;
objection?: string;
reply?: string;
tactic?: string;
deliverable?: string;
}
interface Rep {
slug: string;
name: string;
initials: string;
label: string;
top: boolean;
steps: { label: string; count: number }[];
demo: number;
negotiation: number;
close: number;
winRate: number;
firstResponseSec: number;
followupSec: number;
replyRate: number;
stageWin: { stage: string; win: number }[];
playbook: PlaybookStageCard[];
}
const willemPlaybook: PlaybookStageCard[] = [
{
num: 'S1',
title: 'Discovery',
goal: 'Prospect names one use case + success metric in writing',
objection: "Can you send the deck and pricing? I'll loop people in.",
reply:
'Happy to send both. Before I do — what one outcome would you need to see in 90 days to expand beyond a pilot?',
tactic: 'Never send pricing before a written use case. Anchor on a success metric.',
deliverable: 'Written use case + 6 scoping answers',
},
{
num: 'S2',
title: 'Demo',
goal: "Live aha on the prospect's own pipeline + named champion",
objection: "We're already evaluating Gong and Clari.",
reply:
'Totally fair — Gong wins on call coaching, Clari on forecast hygiene. We win on the writing + follow-up layer. Want a side-by-side on your top 5 open deals?',
tactic: "Acknowledge the competitor's strength, draw the boundary, propose a head-to-head.",
deliverable: 'Head-to-head demo scheduled on real data',
},
{
num: 'S3',
title: 'Negotiation',
goal: 'Term sheet with a clear concession ladder',
objection: 'Procurement caps us at 12-month terms.',
reply: '12 months at list, or 24 months at −22% with a 90-day opt-out. Commit by Friday to hold 22%.',
tactic: 'Trade term length for discount — never discount without a quid pro quo.',
deliverable: '12mo @ list / 24mo @ −22% (expires Fri)',
},
{
num: 'S4',
title: 'Close',
goal: 'Countersigned MSA + order form within 48h of verbal yes',
objection: 'Legal says liability + SOC2 redlines will take 3 weeks.',
reply:
"We've pre-cleared liability at 12× annual and ship SOC2 Type II yearly — a 30-min legal-to-legal compresses this to days.",
tactic: 'Pre-empt the 3 standard redlines (liability, IP, DPA). MSA + OF same day.',
deliverable: 'MSA · OF · NET 30 · liability 12× · SOC2 Type II',
},
];
const genericPlaybook = (name: string): PlaybookStageCard[] => [
{
num: 'S1',
title: 'Discovery',
goal: 'Qualify budget + timeline',
objection: 'Just send me some info.',
reply: 'Sure — what is the one metric that would make this a priority this quarter?',
tactic: `${name} sends collateral early; anchoring on a written success metric first lifts conversion.`,
deliverable: 'Use case captured',
},
{
num: 'S2',
title: 'Demo',
goal: 'Show value on prospect data',
objection: 'Looks similar to what we have.',
reply: 'Where it differs is the follow-up layer — want me to run it on your last 5 stalled deals?',
tactic: 'Differentiate on the follow-up + writing layer, not feature parity.',
deliverable: 'Tailored demo booked',
},
{
num: 'S3',
title: 'Negotiation',
goal: 'Hold margin',
objection: 'Your competitor is cheaper.',
reply: 'Happy to compare total cost — what is driving the price comparison?',
tactic: 'Trade concessions for term length; avoid unilateral discounts.',
deliverable: 'Term sheet sent',
},
];
const reps: Rep[] = [
{
slug: 'willem',
name: 'Willem',
initials: 'WE',
label: 'Top rep',
top: true,
steps: steps([100, 82, 53, 38]),
demo: 82,
negotiation: 53,
close: 38,
winRate: 38,
firstResponseSec: hours(1.2),
followupSec: days(1.8),
replyRate: 71,
stageWin: [
{ stage: 'Discovery', win: 41 },
{ stage: 'Demo', win: 46 },
{ stage: 'Negotiation', win: 58 },
{ stage: 'Close', win: 72 },
],
playbook: willemPlaybook,
},
{
slug: 'sarah',
name: 'Sarah',
initials: 'SK',
label: 'Account Exec',
top: false,
steps: steps([100, 74, 38, 21]),
demo: 74,
negotiation: 38,
close: 21,
winRate: 21,
firstResponseSec: hours(3.6),
followupSec: days(3.1),
replyRate: 52,
stageWin: [
{ stage: 'Discovery', win: 28 },
{ stage: 'Demo', win: 31 },
{ stage: 'Negotiation', win: 34 },
{ stage: 'Close', win: 49 },
],
playbook: genericPlaybook('Sarah'),
},
{
slug: 'marcus',
name: 'Marcus',
initials: 'MR',
label: 'Account Exec',
top: false,
steps: steps([100, 61, 23, 10]),
demo: 61,
negotiation: 23,
close: 10,
winRate: 10,
firstResponseSec: hours(6.2),
followupSec: days(5.4),
replyRate: 38,
stageWin: [
{ stage: 'Discovery', win: 19 },
{ stage: 'Demo', win: 21 },
{ stage: 'Negotiation', win: 23 },
{ stage: 'Close', win: 38 },
],
playbook: genericPlaybook('Marcus'),
},
{
slug: 'lily',
name: 'Lily',
initials: 'LP',
label: 'Account Exec',
top: false,
steps: steps([100, 68, 31, 15]),
demo: 68,
negotiation: 31,
close: 15,
winRate: 15,
firstResponseSec: hours(4.1),
followupSec: days(2.6),
replyRate: 47,
stageWin: [
{ stage: 'Discovery', win: 24 },
{ stage: 'Demo', win: 27 },
{ stage: 'Negotiation', win: 31 },
{ stage: 'Close', win: 55 },
],
playbook: genericPlaybook('Lily'),
},
];
const mean = (xs: number[]) => Math.round(xs.reduce((a, b) => a + b, 0) / xs.length);
const meanF = (xs: number[]) => xs.reduce((a, b) => a + b, 0) / xs.length;
const teamRow = {
demo: mean(reps.map((r) => r.demo)),
negotiation: mean(reps.map((r) => r.negotiation)),
close: mean(reps.map((r) => r.close)),
winRate: mean(reps.map((r) => r.winRate)),
};
/** Human duration from seconds (mirrors the renderer's `duration` format). */
function dur(sec: number): string {
const d = sec / 86400;
if (d >= 1) return `${Math.round(d * 10) / 10}d`;
const h = sec / 3600;
if (h >= 1) return `${Math.round(h * 10) / 10}h`;
const m = sec / 60;
if (m >= 1) return `${Math.round(m)}m`;
return `${Math.round(sec)}s`;
}
// Call intelligence per rep (mock): cadence, talk:listen ratio, call length,
// discovery questions asked, and rate of calls that end with a booked next step.
interface CallStats {
perWeek: number;
talkRatio: number; // % of call the rep is talking
avgCallMin: number;
discoveryQs: number; // avg discovery questions asked
nextStepRate: number; // % of calls ending with a booked next step
monologueSec: number; // longest uninterrupted rep monologue
}
const callStats: Record<string, CallStats> = {
willem: { perWeek: 14, talkRatio: 43, avgCallMin: 32, discoveryQs: 11, nextStepRate: 88, monologueSec: 95 },
sarah: { perWeek: 11, talkRatio: 58, avgCallMin: 27, discoveryQs: 7, nextStepRate: 64, monologueSec: 150 },
marcus: { perWeek: 9, talkRatio: 67, avgCallMin: 24, discoveryQs: 4, nextStepRate: 41, monologueSec: 220 },
lily: { perWeek: 12, talkRatio: 51, avgCallMin: 29, discoveryQs: 8, nextStepRate: 72, monologueSec: 130 },
};
const teamCall = {
perWeek: Math.round(meanF(reps.map((r) => callStats[r.slug].perWeek))),
talkRatio: Math.round(meanF(reps.map((r) => callStats[r.slug].talkRatio))),
avgCallMin: Math.round(meanF(reps.map((r) => callStats[r.slug].avgCallMin))),
discoveryQs: Math.round(meanF(reps.map((r) => callStats[r.slug].discoveryQs))),
nextStepRate: Math.round(meanF(reps.map((r) => callStats[r.slug].nextStepRate))),
monologueSec: Math.round(meanF(reps.map((r) => callStats[r.slug].monologueSec))),
};
// Follow-up content that works for each rep (mock).
const followupContent: Record<string, { template: string; replyRate: number; win: number }[]> = {
willem: [
{ template: 'Recap + single CTA', replyRate: 74, win: 41 },
{ template: 'Mutual action plan', replyRate: 66, win: 37 },
{ template: 'Multi-thread intro to exec', replyRate: 58, win: 33 },
],
sarah: [
{ template: 'Recap + single CTA', replyRate: 55, win: 24 },
{ template: '"Still interested?" nudge', replyRate: 38, win: 17 },
],
marcus: [
{ template: 'Re-send deck, no CTA', replyRate: 24, win: 9 },
{ template: 'Generic check-in', replyRate: 21, win: 8 },
],
lily: [
{ template: 'Recap + next-step proposal', replyRate: 61, win: 28 },
{ template: 'Case-study share', replyRate: 49, win: 22 },
],
};
const teamFollow = {
firstResponseSec: Math.round(meanF(reps.map((r) => r.firstResponseSec))),
followupSec: Math.round(meanF(reps.map((r) => r.followupSec))),
replyRate: Math.round(meanF(reps.map((r) => r.replyRate))),
};
// Pooled org funnel (sum across reps — 100 deals each).
const orgFunnel = STAGE_LABELS.map((label, i) => ({
label,
count: reps.reduce((sum, r) => sum + r.steps[i].count, 0),
}));
const orgWinRate = teamRow.winRate;
// ── stage data model ─────────────────────────────────────────────────────────
interface TalkTrack {
track: string;
rep: string;
uses: number;
toNext: number;
win: number;
}
interface Objection {
objection: string;
bestReply: string;
toNext: number;
/** Real win rate when this objection appears (from the org's objection wiki). */
winPct: number;
/** How many deals this objection has shown up in. */
mentions: number;
talkTracks: TalkTrack[];
}
interface Stage {
n: number;
id: string;
name: string;
goal: string;
objections: Objection[];
}
// Objections are taken from the org's real objection wiki (organisation/wiki-legacy/objections),
// with their actual win rates and mention counts. Talk tracks + best replies are grounded in
// Cedar's value props: "Cursor for GTM" — proactive, full deal context, playbook-controlled,
// reads/writes your existing CRM + Gmail, learns from your top reps.
const stages: Stage[] = [
{
n: 1,
id: 'stage1',
name: 'Discovery',
goal: 'Prospect names one use case + success metric, and sees why Cedar (not a build) is the path.',
objections: [
{
objection: 'Build vs Buy — "we build our GTM tools in-house"',
bestReply:
'Teams that build this underestimate the upkeep. Cedar ships the playbook engine + self-improving data flywheel out of the box and gets smarter from your reps’ own calls. What would your eng team have to deprioritize to build and maintain follow-up + CRM automation?',
toNext: 38,
winPct: 25,
mentions: 4,
talkTracks: [
{ track: 'Reframe to maintenance + opportunity cost', rep: 'Willem', uses: 14, toNext: 42, win: 31 },
{ track: 'Show the self-improving data flywheel', rep: 'Willem', uses: 11, toNext: 38, win: 28 },
{ track: 'Concede + offer API / integration path', rep: 'Lily', uses: 8, toNext: 30, win: 19 },
],
},
{
objection: 'We want to own the data',
bestReply:
'You do — Cedar reads and writes your existing CRM and Gmail; we’re not a separate system of record. Happy to send our infosec + data-handling doc and walk your security lead through it.',
toNext: 58,
winPct: 45,
mentions: 5,
talkTracks: [
{ track: 'Cedar writes to your CRM, not a new silo', rep: 'Willem', uses: 12, toNext: 58, win: 45 },
{ track: 'Share infosec + SOC2 Type II packet', rep: 'Sarah', uses: 9, toNext: 50, win: 38 },
{ track: 'Offer a data-handling review with security', rep: 'Lily', uses: 7, toNext: 44, win: 33 },
],
},
{
objection: 'AI only helps with standardized, templated work',
bestReply:
'True for generic tools — Cedar runs YOUR playbook with full deal context, so it personalizes the non-standard parts. Can I draft a follow-up live on one of your real stalled deals?',
toNext: 66,
winPct: 60,
mentions: 7,
talkTracks: [
{ track: 'Draft live on a real stalled deal', rep: 'Willem', uses: 16, toNext: 66, win: 60 },
{ track: 'Show the 90% no-edit send rate', rep: 'Lily', uses: 10, toNext: 54, win: 47 },
{ track: 'Contrast vs generic ChatGPT output', rep: 'Sarah', uses: 9, toNext: 48, win: 40 },
],
},
{
objection: 'The scaling problem is months away — not urgent now',
bestReply:
'Better to codify your best rep’s playbook before the new hires land — Cedar ramps new AEs to your CRM + follow-up standards without manager time. Want a 3-week pilot timed to the hire?',
toNext: 49,
winPct: 40,
mentions: 5,
talkTracks: [
{ track: 'Tie the pilot to the upcoming hire', rep: 'Willem', uses: 11, toNext: 49, win: 40 },
{ track: 'Ramp-time proof point (Cursor)', rep: 'Lily', uses: 8, toNext: 44, win: 35 },
],
},
{
objection: 'This market is too competitive',
bestReply:
'Crowded on call recording — not on the writing + follow-up layer where deals actually slip. That’s the gap we own, and why Cursor, Warp, and Pylon picked us.',
toNext: 64,
winPct: 60,
mentions: 6,
talkTracks: [
{ track: 'Differentiate on the follow-up layer', rep: 'Willem', uses: 13, toNext: 64, win: 60 },
{ track: 'Name-drop Cursor / Warp / Pylon', rep: 'Sarah', uses: 9, toNext: 52, win: 44 },
],
},
],
},
{
n: 2,
id: 'stage2',
name: 'Demo',
goal: "Live aha on the prospect's own pipeline + a named champion.",
objections: [
{
objection: 'Our team works in the CRM, not Gmail',
bestReply:
'Cedar layers onto both — it reads/writes your CRM and drafts from Gmail, so reps don’t switch context. Want to see it update a HubSpot deal straight from an email thread?',
toNext: 64,
winPct: 60,
mentions: 6,
talkTracks: [
{ track: 'Show CRM write-back from an email', rep: 'Willem', uses: 12, toNext: 64, win: 60 },
{ track: "Layer onto, don't replace, the CRM", rep: 'Lily', uses: 9, toNext: 55, win: 48 },
],
},
{
objection: 'We want to keep Gmail as our default',
bestReply:
'Keep it — Cedar runs inside your Gmail workflow, no migration. You only adopt Send when the drafts are good enough, and they hit a 90% no-edit rate.',
toNext: 90,
winPct: 88,
mentions: 9,
talkTracks: [
{ track: 'No migration — runs inside Gmail', rep: 'Willem', uses: 18, toNext: 90, win: 88 },
{ track: 'Adopt Send gradually as drafts prove out', rep: 'Lily', uses: 11, toNext: 80, win: 75 },
],
},
{
objection: "Product's not ready — don't want to waste time",
bestReply:
'Fair — let’s not demo vapor. I’ll run it on your real pipeline, and if an integration isn’t ready we’ll scope the pilot around what is.',
toNext: 48,
winPct: 40,
mentions: 6,
talkTracks: [
{ track: 'Demo on real pipeline only', rep: 'Willem', uses: 10, toNext: 48, win: 40 },
{ track: 'Scope the pilot to ready integrations', rep: 'Sarah', uses: 8, toNext: 42, win: 34 },
],
},
{
objection: "Differentiation doesn't show up in our sales-tool data",
bestReply:
'Exactly — Gong captures the call, not the writing + follow-up where your top rep wins. Cedar surfaces those tactics so you can clone them across the team.',
toNext: 47,
winPct: 40,
mentions: 5,
talkTracks: [
{ track: "Position vs Gong's blind spot", rep: 'Willem', uses: 9, toNext: 47, win: 40 },
{ track: 'Surface top-rep tactics to clone', rep: 'Lily', uses: 7, toNext: 43, win: 35 },
],
},
{
objection: "Inbox layout doesn't match how we work",
bestReply:
'We mirror your Gmail sections (starred, then unread) — you keep your layout. Let me show the custom inbox so the switch is zero-friction.',
toNext: 78,
winPct: 75,
mentions: 4,
talkTracks: [
{ track: 'Mirror Gmail custom sections', rep: 'Willem', uses: 8, toNext: 78, win: 75 },
{ track: 'Zero-friction switch walkthrough', rep: 'Lily', uses: 6, toNext: 70, win: 66 },
],
},
],
},
{
n: 3,
id: 'stage3',
name: 'Negotiation',
goal: 'Term sheet with a clear concession ladder and the pilot scoped.',
objections: [
{
objection: 'Price is too high (founder doing sales)',
bestReply:
'You’re comparing to a $30/mo tool — Cedar replaces hours of admin and recovers slipped-deal revenue. One saved deal pays for the year. Want the ROI math on your ACV?',
toNext: 49,
winPct: 40,
mentions: 5,
talkTracks: [
{ track: 'Anchor to recovered-deal revenue, not tool price', rep: 'Willem', uses: 11, toNext: 49, win: 40 },
{ track: 'Run the ROI on their ACV', rep: 'Lily', uses: 8, toNext: 44, win: 35 },
{ track: 'Offer a founder / seat-based starter', rep: 'Sarah', uses: 6, toNext: 38, win: 28 },
],
},
{
objection: 'Default cadence is too aggressive for enterprise',
bestReply:
'Fully configurable — we’ll set a long-cycle cadence so nothing feels spammy. Cedar enforces YOUR cadence, not a default.',
toNext: 80,
winPct: 75,
mentions: 5,
talkTracks: [
{ track: 'Configure a long-cycle cadence', rep: 'Willem', uses: 10, toNext: 80, win: 75 },
{ track: 'Show the cadence controls', rep: 'Lily', uses: 7, toNext: 72, win: 66 },
],
},
{
objection: 'Champion lacks authority to approve a pilot',
bestReply:
'Let’s arm you to sell it up — I’ll send a one-page business case and offer a 15-min exec readout so you’re not carrying it alone.',
toNext: 49,
winPct: 40,
mentions: 6,
talkTracks: [
{ track: 'Arm the champion with a 1-pager', rep: 'Willem', uses: 11, toNext: 49, win: 40 },
{ track: 'Offer a 15-min exec readout', rep: 'Lily', uses: 9, toNext: 45, win: 36 },
],
},
{
objection: 'Team has no bandwidth to evaluate',
bestReply:
'We run the pilot for you — setup is hours not weeks, and Cedar works in the background. Pick 5 deals and we’ll do the lift.',
toNext: 30,
winPct: 20,
mentions: 6,
talkTracks: [
{ track: 'We run the pilot (low lift)', rep: 'Willem', uses: 8, toNext: 30, win: 20 },
{ track: 'Background — no workflow change', rep: 'Lily', uses: 6, toNext: 26, win: 16 },
],
},
{
objection: 'Not ready for a pilot — need internal approvals',
bestReply:
'Makes sense — let’s map the approval path now and pre-stage the security packet so the pilot starts the day you’re cleared.',
toNext: 42,
winPct: 33,
mentions: 8,
talkTracks: [
{ track: 'Map the approval path together', rep: 'Willem', uses: 12, toNext: 42, win: 33 },
{ track: 'Pre-stage the security packet', rep: 'Sarah', uses: 9, toNext: 38, win: 28 },
],
},
],
},
{
n: 4,
id: 'stage4',
name: 'Close',
goal: 'Countersigned MSA + order form within 48h of verbal yes.',
objections: [
{
objection: 'SOC 2 required before we can proceed',
bestReply:
'We’re SOC 2 Type II — here’s the report and DPA. Happy to set up a 30-min legal-to-legal to clear it fast.',
toNext: 58,
winPct: 50,
mentions: 5,
talkTracks: [
{ track: 'Send SOC2 Type II report + DPA', rep: 'Willem', uses: 11, toNext: 58, win: 50 },
{ track: 'Legal-to-legal in 30 min', rep: 'Lily', uses: 8, toNext: 52, win: 44 },
],
},
{
objection: 'Integration needs IT-admin approval',
bestReply:
'Only the Gmail / CRM scopes need admin — I’ll send the exact permission list so IT can approve in one pass, and join the call if useful.',
toNext: 72,
winPct: 67,
mentions: 3,
talkTracks: [
{ track: 'Send the exact scope list to IT', rep: 'Willem', uses: 7, toNext: 72, win: 67 },
{ track: 'Offer to join the IT review call', rep: 'Lily', uses: 5, toNext: 64, win: 58 },
],
},
{
objection: 'Salesforce sync lag breaks our source of truth',
bestReply:
'We can move you to near-real-time sync (or a webhook on deal update) so closed/updated deals reflect immediately — no more hourly lag.',
toNext: 58,
winPct: 50,
mentions: 4,
talkTracks: [
{ track: 'Enable near-real-time sync', rep: 'Willem', uses: 9, toNext: 58, win: 50 },
{ track: 'Webhook on deal update', rep: 'Sarah', uses: 6, toNext: 50, win: 42 },
],
},
{
objection: 'An acquisition froze new-tool purchases',
bestReply:
'Understood — let’s keep the pilot warm and re-anchor once the new hierarchy settles. I’ll line up the intro to the new decision-maker.',
toNext: 42,
winPct: 33,
mentions: 4,
talkTracks: [
{ track: 'Keep the pilot warm, re-anchor post-M&A', rep: 'Willem', uses: 9, toNext: 42, win: 33 },
{ track: 'Get an intro to the new decision-maker', rep: 'Lily', uses: 7, toNext: 38, win: 28 },
],
},
{
objection: 'Data-integrity bugs broke pipeline trust',
bestReply:
'We take that seriously — here’s the incident summary and the fix. Let’s re-verify your pipeline together so you can trust the view.',
toNext: 58,
winPct: 50,
mentions: 4,
talkTracks: [
{ track: 'Share the incident summary + fix', rep: 'Willem', uses: 8, toNext: 58, win: 50 },
{ track: 'Re-verify the pipeline live together', rep: 'Lily', uses: 6, toNext: 52, win: 44 },
],
},
],
},
];
const objectionPath = (stageId: string, objection: string) => `${OBJ_BASE}/${stageId}/objections/${slug(objection)}`;
// Competitive landscape (org-wide, shown in every stage's Positioning tab).
// Gong is an integration, not a competitor — hence the high win rate.
const competitors = [
{ competitor: 'Claude / ChatGPT', stance: 'Compete', encounter: 44, theyWin: 'Free-form drafting, cheap', weWin: 'Proactive, full deal context, playbook-controlled', winVs: 62 },
{ competitor: 'Status quo (Gmail + manual CRM)', stance: 'Compete', encounter: 51, theyWin: 'No new tool to adopt', weWin: 'Cut admin 75%, no deals slip', winVs: 60 },
{ competitor: 'Sybill', stance: 'Compete', encounter: 29, theyWin: 'Call notes + CRM autofill', weWin: 'Writing + follow-up + playbook execution', winVs: 55 },
{ competitor: 'Clari', stance: 'Compete', encounter: 24, theyWin: 'Forecast hygiene', weWin: 'Per-deal execution + next steps', winVs: 58 },
{ competitor: 'HubSpot Sequences', stance: 'Compete', encounter: 26, theyWin: 'Bundled with the CRM', weWin: 'Reply quality + deal context', winVs: 49 },
{ competitor: 'Gong', stance: 'Integrate', encounter: 38, theyWin: 'Call recording + coaching', weWin: 'We integrate — surface the writing/follow-up Gong misses', winVs: 78 },
];
// Pain points (org-wide, from "Customer Problems We Solve"). Shown in every stage's tab.
const painPoints = [
{ pain: 'Deals slip through the cracks on timing', feltBy: 'AE', freq: 64, win: 39 },
{ pain: 'Follow-ups are manual and impersonal', feltBy: 'AE', freq: 58, win: 36 },
{ pain: 'CRM busywork — moving info column A → B', feltBy: 'AE', freq: 55, win: 34 },
{ pain: 'Pipeline analysis = copy-pasting scattered context', feltBy: 'Strategic AE', freq: 47, win: 33 },
{ pain: "Can't clone the top rep's playbook", feltBy: 'Manager', freq: 38, win: 37 },
{ pain: 'New reps ramp slowly', feltBy: 'Manager', freq: 34, win: 31 },
{ pain: 'Dormant pipeline goes uncovered', feltBy: 'RevOps', freq: 29, win: 28 },
];
// Ideal customer profile — segment economics (shown in every stage's ICP tab).
const icpSegments = [
{ segment: 'Mid-market SaaS (50–500)', deals: 142, win: 41, avgDeal: 38_000, cycle: 34, signal: 'Champion + follow-up pain' },
{ segment: 'Enterprise (500+)', deals: 63, win: 33, avgDeal: 96_000, cycle: 61, signal: 'Multi-thread, security-ready' },
{ segment: 'Fintech', deals: 44, win: 37, avgDeal: 72_000, cycle: 52, signal: 'Compliance + SOC2 matters' },
{ segment: 'Healthcare', deals: 31, win: 29, avgDeal: 58_000, cycle: 58, signal: 'Slow legal, high ACV' },
{ segment: 'SMB (<50)', deals: 118, win: 22, avgDeal: 11_000, cycle: 18, signal: 'Fast cycle, price-sensitive' },
];
/** A standalone "← back" link to another Cedar Doc, resolved by path at render. */
function backLink(path: string, label: string): string {
return fence({ dataSources: {}, layout: { type: 'docLink', path, label } });
}
// ── main-doc section builders ───────────────────────────────────────────────
// Per-rep quota attainment + closed-won (mock).
const attainment = [
{ rep: 'Willem', attainment: 142, closedWon: 1_620_000 },
{ rep: 'Sarah', attainment: 88, closedWon: 720_000 },
{ rep: 'Marcus', attainment: 61, closedWon: 310_000 },
{ rep: 'Lily', attainment: 79, closedWon: 540_000 },
];
const avgAttainment = mean(attainment.map((a) => a.attainment));
const totalPipeline = 4_250_000;
function orgConversionFence(): string {
return fence({
id: 'org_conversion',
dataSources: {
org: {
mock: [
{
winRate: orgWinRate,
deals: reps.length * 100,
avgAttainment,
pipeline: totalPipeline,
},
],
},
funnel: { mock: orgFunnel },
},
layout: {
type: 'column',
gap: 'lg',
children: [
{
type: 'grid',
columns: 4,
gap: 'lg',
children: [
{ type: 'stat', source: 'org', field: 'winRate', label: 'Org win rate', format: 'percent', style: 'radial' },
{ type: 'stat', source: 'org', field: 'avgAttainment', label: 'Avg quota attainment', format: 'percent' },
{ type: 'stat', source: 'org', field: 'pipeline', label: 'Total pipeline', format: 'currency-compact' },
{ type: 'stat', source: 'org', field: 'deals', label: 'Deals (last 90 days)', format: 'number' },
],
},
{ type: 'funnel', source: 'funnel', size: 'lg' },
],
},
});
}
function perRepFence(): string {
const rows = reps.map((r) => ({
name: r.name,
initials: r.initials,
label: r.label,
steps: r.steps,
attainment: attainment.find((a) => a.rep === r.name)?.attainment ?? 0,
docPath: `${USERS_BASE}/${r.slug}`,
}));
return fence({
id: 'per_rep_conversion',
dataSources: { reps: { mock: rows } },
layout: {
type: 'repeater',
source: 'reps',
as: 'rep',
layout: 'grid',
columns: 4,
gap: 'lg',
template: {
type: 'entityCard',
linkField: 'docPath',
children: [
{ type: 'profile', name: '{{rep.name}}', initials: '{{rep.initials}}', label: '{{rep.label}}' },
{ type: 'stat', field: 'attainment', label: 'Quota attainment', format: 'percent' },
{ type: 'funnel', field: 'steps' },
],
},
},
});
}
function followupFence(): string {
const outcome = [
{ outcome: 'Won deals', firstResponse: hours(1.2), followup: days(1.8), replyRate: 71, deals: 42 },
{ outcome: 'Lost deals', firstResponse: hours(5.4), followup: days(6.2), replyRate: 34, deals: 61 },
{ outcome: 'All deals', firstResponse: hours(3.8), followup: days(4.1), replyRate: 49, deals: 103 },
];
// Follow-up content by type — includes Case study and the pattern-breaker class.
const content = [
{ content: 'Recap + single CTA', replyRate: 71, win: 38 },
{ content: 'Mutual action plan', replyRate: 66, win: 35 },
{ content: 'Case study', replyRate: 59, win: 32 },
{ content: 'Pattern breaker', replyRate: 54, win: 29 },
{ content: 'Generic "just checking in"', replyRate: 18, win: 7 },
];
// Pattern breakers — the actual emails that broke a no-reply streak.
const breakers = [
{
name: 'The permission-to-close',
subject: 'Should I close your file?',
body: "Haven't heard back, which usually means one of three things: it's not a priority right now, the timing is off, or I dropped the ball. Totally fine either way — just tell me which and I'll act accordingly.",
replyRate: 61,
context: 'After 2 unanswered follow-ups',
},
{
name: 'The trigger event',
subject: 'Saw the Series B — congrats',
body: 'Congrats on the raise. New headcount usually means more pipeline than the team can follow up on — which is exactly the gap we close. Worth 15 minutes now that you are scaling the team?',
replyRate: 58,
context: 'Triggered by funding news',
},
{
name: 'The case-study drop',
subject: 'How Ramp cut follow-up time 60%',
body: 'You mentioned reps forgetting to follow up. Ramp had the same problem — attached is the one-pager on how they fixed it in 3 weeks. Happy to walk through it live if useful.',
replyRate: 52,
context: 'Mapped to a stated pain',
},
];
return fence({
id: 'follow_ups',
dataSources: {
headline: { mock: [{ response: hours(3.8), postMeeting: hours(6), followup: days(4.1) }] },
outcome: { mock: outcome },
content: { mock: content },
breakers: { mock: breakers },
},
layout: {
type: 'column',
gap: 'lg',
children: [
{
type: 'grid',
columns: 3,
gap: 'lg',
children: [
{ type: 'stat', source: 'headline', field: 'response', label: 'Avg response time', format: 'duration' },
{ type: 'stat', source: 'headline', field: 'postMeeting', label: 'Avg post-meeting follow-up', format: 'duration' },
{ type: 'stat', source: 'headline', field: 'followup', label: 'Avg follow-up time', format: 'duration' },
],
},
{
type: 'table',
source: 'outcome',
title: 'Response & follow-up by outcome',
columns: [
{ key=[redacted], label: 'Outcome', weight: 2 },
{ key=[redacted], label: 'Avg response time', format: 'duration', align: 'right' },
{ key=[redacted], label: 'Avg follow-up gap', format: 'duration', align: 'right' },
{ key=[redacted], label: 'Reply rate', format: 'percent', align: 'right' },
{ key=[redacted], label: 'Deals', format: 'number', align: 'right' },
],
},
{
type: 'table',
source: 'content',
title: 'Follow-up content — what gets replies',
columns: [
{ key=[redacted], label: 'Content type', weight: 3 },
{ key=[redacted], label: 'Reply rate', format: 'percent', align: 'right' },
{ key: 'win', label: 'Win %', format: 'percent', align: 'right' },
],
},
{
type: 'text',
markdown:
'### Pattern breakers\nThe emails that break a no-reply streak — short, specific, and easy to say yes (or no) to.',
},
{
type: 'repeater',
source: 'breakers',
as: 'breaker',
layout: 'list',
gap: 'md',
template: {
type: 'entityCard',
children: [
{ type: 'text', markdown: '**{{breaker.name}}** · {{breaker.context}} · replies {{breaker.replyRate}}%' },
{ type: 'text', markdown: '**Subject:** {{breaker.subject}}' },
{ type: 'text', markdown: '{{breaker.body}}', className: 'text-muted-foreground italic' },
],
},
},
],
},
});
}
function stageFence(stage: Stage): string {
const objectionRows = stage.objections.map((o) => ({
objection: o.objection,
analysisPath: objectionPath(stage.id, o.objection),
mentions: o.mentions,
bestReply: o.bestReply,
win: o.winPct,
toNext: o.toNext,
}));
// Talk tracks tab — every track used against this stage's objections.
const trackRows = stage.objections.flatMap((o) =>
o.talkTracks.map((t) => ({
track: t.track,
objection: o.objection,
rep: t.rep,
uses: t.uses,
toNext: t.toNext,
win: t.win,
})),
);
// Each tab pairs a detailed table with a win-rate chart.
const tab = (label: string, table: Record<string, unknown>, chart: Record<string, unknown>) => ({
label,
node: { type: 'column', gap: 'lg', children: [table, chart] },
});
return fence({
id: `${stage.id}_playbook`,
dataSources: {
objections: { mock: objectionRows },
positioning: { mock: competitors },
tracks: { mock: trackRows },
pains: { mock: painPoints },
icp: { mock: icpSegments },
},
layout: {
type: 'tabs',
tabs: [
tab(
'Objections',
{
type: 'table',
source: 'objections',
columns: [
{ key=[redacted], label: 'Objection', weight: 3 },
{ key=[redacted], label: 'Analysis', type: 'docLink', linkLabel: 'Analysis', weight: 1 },
{ key=[redacted], label: 'Deals seen', format: 'number', align: 'right' },
{ key=[redacted], label: 'Best-performing response', weight: 4 },
{ key: 'win', label: 'Win %', format: 'percent', align: 'right' },
{ key=[redacted], label: 'Move to next', format: 'percent', align: 'right' },
],
},
{ type: 'chart', source: 'objections', chart: 'bar', orientation: 'horizontal', title: 'Win rate by objection', xAxis: 'objection', series: [{ key: 'win', label: 'Win %' }] },
),
tab(
'Competitive Positioning',
{
type: 'table',
source: 'positioning',
columns: [
{ key=[redacted], label: 'Competitor', weight: 2 },
{ key=[redacted], label: 'Stance', weight: 1 },
{ key=[redacted], label: 'Encounter %', format: 'percent', align: 'right' },
{ key=[redacted], label: 'They win on', weight: 2 },
{ key=[redacted], label: 'We win on', weight: 3 },
{ key=[redacted], label: 'Win rate vs', format: 'percent', align: 'right' },
],
},
{ type: 'chart', source: 'positioning', chart: 'bar', orientation: 'horizontal', title: 'Win rate vs competitor', xAxis: 'competitor', series: [{ key=[redacted], label: 'Win rate vs' }] },
),
tab(
'Talk tracks',
{
type: 'table',
source: 'tracks',
columns: [
{ key=[redacted], label: 'Talk track', weight: 3 },
{ key=[redacted], label: 'Used against', weight: 2 },
{ key: 'rep', label: 'Top rep', type: 'rep', weight: 1 },
{ key=[redacted], label: 'Uses', format: 'number', align: 'right' },
{ key=[redacted], label: 'Move to next', format: 'percent', align: 'right' },
{ key: 'win', label: 'Win %', format: 'percent', align: 'right' },
],
},
{ type: 'chart', source: 'tracks', chart: 'bar', orientation: 'horizontal', title: 'Win rate by talk track', xAxis: 'track', series: [{ key: 'win', label: 'Win %' }] },
),
tab(
'Pain points',
{
type: 'table',
source: 'pains',
columns: [
{ key=[redacted], label: 'Pain point', weight: 3 },
{ key=[redacted], label: 'Felt by', weight: 1 },
{ key=[redacted], label: 'Frequency', format: 'percent', align: 'right' },
{ key: 'win', label: 'Win % when raised', format: 'percent', align: 'right' },
],
},
{ type: 'chart', source: 'pains', chart: 'bar', orientation: 'horizontal', title: 'Win rate by pain point', xAxis: 'pain', series: [{ key: 'win', label: 'Win %' }] },
),
tab(
'ICP',
{
type: 'table',
source: 'icp',
columns: [
{ key=[redacted], label: 'Segment', weight: 3 },
{ key=[redacted], label: 'Deals', format: 'number', align: 'right' },
{ key: 'win', label: 'Win %', format: 'percent', align: 'right' },
{ key=[redacted], label: 'Avg deal', format: 'currency-compact', align: 'right' },
{ key=[redacted], label: 'Cycle (days)', format: 'number', align: 'right' },
{ key=[redacted], label: 'Best-fit signal', weight: 2 },
],
},
{ type: 'chart', source: 'icp', chart: 'bar', orientation: 'horizontal', title: 'Win rate by segment', xAxis: 'segment', series: [{ key: 'win', label: 'Win %' }] },
),
],
},
});
}
// ── subdocument builders ──────────────────────────────────────────────────────
function repSubdoc(rep: Rep): PlaybookDocument {
const call = callStats[rep.slug];
const winFor = (title: string) => rep.stageWin.find((s) => s.stage === title)?.win;
const playbookRows = rep.playbook.map((card) => ({ ...card, winPct: winFor(card.title) }));
const markdown = [
`# ${rep.name} — playbook`,
``,
backLink(MAIN_PATH, '← Back to team playbook'),
``,
`**${rep.label}.** How ${rep.name} converts pipeline versus the team, and the response habits, call behaviour, and stage-by-stage moves behind it.`,
``,
// 1) Conversion vs team — a single comparison (no duplicate funnel).
`## Conversion vs the team`,
``,
fence({
id: `rep_${rep.slug}_compare`,
dataSources: {
rep: { mock: [{ demo: rep.demo, negotiation: rep.negotiation, close: rep.close, winRate: rep.winRate }] },
team: { mock: [teamRow] },
},
layout: {
type: 'comparison',
source: 'rep',
teamSource: 'team',
rowLabel: rep.name,
teamLabel: 'Team avg',
format: 'percent',
fields: [
{ key=[redacted], label: 'Reaches demo' },
{ key=[redacted], label: 'Reaches negotiation' },
{ key=[redacted], label: 'Reaches close' },
{ key=[redacted], label: 'Win rate' },
],
},
}),
``,
// 2) Follow-up & responses.
`## Follow-up & responses`,
``,
fence({
id: `rep_${rep.slug}_followup`,
dataSources: {
headline: {
mock: [{ firstResponse: rep.firstResponseSec, followup: rep.followupSec, replyRate: rep.replyRate }],
},
vsTeam: {
mock: [
{ metric: 'Median first response', you: dur(rep.firstResponseSec), team: dur(teamFollow.firstResponseSec) },
{ metric: 'Median follow-up gap', you: dur(rep.followupSec), team: dur(teamFollow.followupSec) },
{ metric: 'Reply rate', you: `${rep.replyRate}%`, team: `${teamFollow.replyRate}%` },
],
},
content: { mock: followupContent[rep.slug] },
},
layout: {
type: 'column',
gap: 'lg',
children: [
{
type: 'grid',
columns: 3,
gap: 'lg',
children: [
{ type: 'stat', source: 'headline', field: 'firstResponse', label: 'Median first response', format: 'duration' },
{ type: 'stat', source: 'headline', field: 'followup', label: 'Median follow-up gap', format: 'duration' },
{ type: 'stat', source: 'headline', field: 'replyRate', label: 'Reply rate', format: 'percent' },
],
},
{
type: 'table',
source: 'vsTeam',
title: `${rep.name} vs team`,
columns: [
{ key=[redacted], label: 'Metric', weight: 2 },
{ key: 'you', label: rep.name, align: 'right' },
{ key=[redacted], label: 'Team avg', align: 'right' },
],
},
{
type: 'table',
source: 'content',
title: 'Follow-up content that works',
columns: [
{ key=[redacted], label: 'Follow-up content', weight: 3 },
{ key=[redacted], label: 'Reply rate', format: 'percent', align: 'right' },
{ key: 'win', label: 'Win %', format: 'percent', align: 'right' },
],
},
],
},
}),
``,
// 3) Call intelligence.
`## Call intelligence`,
``,
fence({
id: `rep_${rep.slug}_calls`,
dataSources: {
headline: { mock: [{ perWeek: call.perWeek, talkRatio: call.talkRatio, avgCallMin: call.avgCallMin }] },
vsTeam: {
mock: [
{ metric: 'Talk : listen ratio', you: `${call.talkRatio}% / ${100 - call.talkRatio}%`, team: `${teamCall.talkRatio}% / ${100 - teamCall.talkRatio}%` },
{ metric: 'Avg call length', you: `${call.avgCallMin}m`, team: `${teamCall.avgCallMin}m` },
{ metric: 'Discovery questions / call', you: `${call.discoveryQs}`, team: `${teamCall.discoveryQs}` },
{ metric: 'Longest monologue', you: dur(call.monologueSec), team: dur(teamCall.monologueSec) },
{ metric: 'Calls ending with a next step', you: `${call.nextStepRate}%`, team: `${teamCall.nextStepRate}%` },
],
},
},
layout: {
type: 'column',
gap: 'lg',
children: [
{
type: 'grid',
columns: 3,
gap: 'lg',
children: [
{ type: 'stat', source: 'headline', field: 'perWeek', label: 'Calls / week', format: 'number' },
{ type: 'stat', source: 'headline', field: 'talkRatio', label: 'Talk ratio', format: 'percent' },
{ type: 'stat', source: 'headline', field: 'avgCallMin', label: 'Avg call (min)', format: 'number' },
],
},
{
type: 'table',
source: 'vsTeam',
title: `${rep.name} vs team`,
columns: [
{ key=[redacted], label: 'Signal', weight: 2 },
{ key: 'you', label: rep.name, align: 'right' },
{ key=[redacted], label: 'Team avg', align: 'right' },
],
},
],
},
}),
``,
// 4) Stage-by-stage playbook (win rate folded into each card).
`## Playbook by stage`,
``,
fence({
id: `rep_${rep.slug}_playbook`,
dataSources: { playbook: { mock: playbookRows } },
layout: { type: 'playbook', source: 'playbook' },
}),
``,
].join('\n');
return { path: `${USERS_BASE}/${rep.slug}`, title: rep.name, markdown };
}
function objectionSubdoc(stage: Stage, o: Objection): PlaybookDocument {
const path = objectionPath(stage.id, o.objection);
const markdown = [
`# ${o.objection}`,
``,
backLink(MAIN_PATH, '← Back to team playbook'),
``,
`Stage ${stage.n} — ${stage.name}. **Best-performing response:** ${o.bestReply}`,
``,
`Seen in **${o.mentions} deals**. Overall it moves to the next step **${o.toNext}%** of the time and wins **${o.winPct}%**. Talk-track performance below.`,
``,
fence({
id: `${stage.id}_obj_${slug(o.objection)}`,
dataSources: { tracks: { mock: o.talkTracks } },
layout: {
type: 'column',
gap: 'lg',
children: [
{
type: 'table',
source: 'tracks',
title: 'Talk tracks for this objection',
columns: [
{ key=[redacted], label: 'Talk track', weight: 3 },
{ key: 'rep', label: 'Top user', type: 'rep', weight: 2 },
{ key=[redacted], label: 'Uses', format: 'number', align: 'right' },
{ key=[redacted], label: 'Move to next step', format: 'percent', align: 'right' },
{ key: 'win', label: 'Win %', format: 'percent', align: 'right' },
],
},
{
type: 'chart',
source: 'tracks',
chart: 'bar',
orientation: 'horizontal',
title: 'Win rate by talk track',
xAxis: 'track',
series: [{ key: 'win', label: 'Win %' }],
},
],
},
}),
``,
].join('\n');
return { path, title: o.objection, markdown };
}
// ── assembly ────────────────────────────────────────────────────────────────
function mainDoc(): PlaybookDocument {
const parts: string[] = [
`# What is your top rep doing differently?`,
``,
`A living breakdown of how the team converts pipeline — org and per-rep conversion, response habits, and the per-stage patterns, objections, messaging, features, and pain points that move deals. Last 90 days, 100 deals per rep.`,
``,
`## Org-wide conversion rate`,
``,
orgConversionFence(),
``,
`## Per rep conversion rate`,
``,
`Each card is a rep's funnel. Click a rep to open their full playbook — their conversion versus the team average and what they do at each stage.`,
``,
perRepFence(),
``,
`## Playbook`,
``,
`### Follow-up`,
``,
`Speed and cadence of follow-up is the biggest separator between won and lost deals. Times are medians; "first response" is the gap from an inbound message to the first reply, "follow-up gap" is the gap between consecutive outbound emails.`,
``,
followupFence(),
``,
];
for (const stage of stages) {
parts.push(
`### Stage ${stage.n} — ${stage.name}`,
``,
`**Goal:** ${stage.goal}`,
``,
`Each objection links to a breakdown of the talk tracks reps use against it and how each performs.`,
``,
stageFence(stage),
``,
);
}
return { path: MAIN_PATH, title: 'What is your top rep doing differently?', markdown: parts.join('\n') };
}
export const playbookDocuments: PlaybookDocument[] = [
mainDoc(),
...reps.map(repSubdoc),
...stages.flatMap((stage) => stage.objections.map((o) => objectionSubdoc(stage, o))),
];
/** The main document's markdown (used by the standalone demo route). */
export const topRepPlaybookMarkdown = playbookDocuments[0].markdown;