From sales-cadence
Install and configure a personalized sales cadence — the evening recap (interactive review of the day) and the morning outreach (autonomous draft + summary). Walks the user through an interview, then generates a HubSpot CRM skill, a brand-voice skill, a personal-voice skill, two scheduled tasks, and seed state.json — all tailored to them. Trigger when the user says "install sales cadence", "set up the sales routines", "set up morning outreach and evening recap", or asks to configure this plugin's sales workflow.
How this skill is triggered — by the user, by Claude, or both
Slash command
/sales-cadence:sales-cadence-installerThe summary Claude sees in its skill listing — used to decide when to auto-load this skill
This skill walks a new user through a one-time setup, then generates four artifacts tailored to them:
references/alternative-workflow.mdreferences/fallback-google.mdreferences/hubspot-setup.mdreferences/meeting-notes-blocks.mdreferences/smtp-setup.mdtemplates/brand-voice.md.tmpltemplates/crm-hubspot.md.tmpltemplates/evening-recap.md.tmpltemplates/morning-outreach.md.tmpltemplates/personal-voice.md.tmpltemplates/state.json.tmplThis skill walks a new user through a one-time setup, then generates four artifacts tailored to them:
sales-evening-recap and sales-morning-outreach) created via the scheduled-tasks MCPhubspot-crm skill — installed at ~/.claude/skills/hubspot-crm/SKILL.md~/.claude/skills/<brand-skill-name>/SKILL.md~/.claude/skills/<voice-skill-name>/SKILL.mdPlus seed files at ~/.claude/sales-pipeline/:
state.json (pipeline state)smtp.json (Gmail app password — placeholder; user fills in)hubspot.json (HubSpot access token — placeholder; user fills in)All templates are under templates/ next to this file. Read them with the Read tool when you need them. Use Bash with dirname and realpath if you need to resolve the absolute path to this skill folder, or just construct paths starting from $HOME/.claude/plugins/ if installed via plugin (the exact path depends on plugin install location).
Templates available:
templates/evening-recap.md.tmpltemplates/morning-outreach.md.tmpltemplates/crm-hubspot.md.tmpltemplates/brand-voice.md.tmpltemplates/personal-voice.md.tmpltemplates/state.json.tmplReference docs (load only when relevant):
references/hubspot-setup.md — how the user creates a HubSpot private app + tokenreferences/smtp-setup.md — how to generate a Gmail app passwordreferences/fallback-google.md — Google Tasks / Google Keep fallback when Apple Reminders / Granola are unavailablereferences/alternative-workflow.md — alternative workflows if the user doesn't want the standard cadenceBefore the interview, run these checks and tell the user what's missing:
which gws # Google Workspace CLI — required for Gmail / Calendar / Drive
which remindctl # optional — only needed on macOS for Apple Reminders
uname -s # detect macOS vs Linux
If gws is missing, stop and tell the user: "The Google Workspace CLI (gws) isn't installed. The cadence reads Gmail, Calendar, and Drive through it. Install it first, then re-run setup. Want me to point you at install docs?"
If remindctl is missing on macOS, note it — you'll offer the Google Tasks fallback during the interview.
If on Linux/Windows, Apple Reminders isn't available — only offer Google Tasks or "skip reminders" options.
Run this as a back-and-forth conversation. Ask one section at a time. Don't dump all questions at once. Save answers as you go in a working dict.
Privacy note: Never ask the user to paste API tokens or passwords in chat. For HubSpot tokens and Gmail app passwords, point them at the relevant reference docs and tell them where to write the file themselves.
Quick interview — about 10 minutes. I'll ask in chunks.
First, the basics:
1. Your first name (used in greetings and recap dialogue)?
2. Your full name (used in the personal-voice skill)?
3. Your work email?
4. Your phone (for sales email signatures — leave blank to skip)?
Capture: USER_FIRST_NAME, USER_FULL_NAME, USER_EMAIL, USER_PHONE.
About your company:
5. Company name?
6. Primary domain (e.g. acme.com)?
7. One-line description of what your company does (e.g. "B2B AI services" or "marketing automation for dental practices")?
Capture: COMPANY_NAME, COMPANY_DOMAIN, COMPANY_DESCRIPTION.
8. Timezone? (e.g. "US Eastern", "US Pacific", "Europe/London")
9. What time should the morning outreach run? (default 8:30am)
10. What time should the evening recap run? (default 6:00pm)
Capture: TIMEZONE (display name), TIMEZONE_IANA (resolve to IANA tz id, e.g. America/New_York), MORNING_RUN_TIME, EVENING_RUN_TIME.
The cadence syncs to HubSpot. You'll need a HubSpot Private App access token.
11. Do you have a HubSpot Private App created already, or should I walk you through it?
If they need help, read references/hubspot-setup.md and present the steps. Tell them to save the token to ~/.claude/sales-pipeline/hubspot.json in this format:
{"access_token": "their-token-here"}
Don't proceed past this point until they confirm they've created ~/.claude/sales-pipeline/hubspot.json.
python3 -c "import json,os,sys; p=os.path.expanduser('~/.claude/sales-pipeline/hubspot.json'); print('exists' if os.path.exists(p) and json.load(open(p)).get('access_token') else 'missing')"
If missing, tell them and wait.
The morning summary is sent to you via Gmail SMTP.
12. Confirm your Gmail address for sending the summary: {{USER_EMAIL}}?
13. Do you have a Gmail App Password set up?
If no, walk them through references/smtp-setup.md to create one. Tell them to save to ~/.claude/sales-pipeline/smtp.json as:
{"email": "{{USER_EMAIL}}", "gmail_app_password": "abcd efgh ijkl mnop"}
Verify exists:
python3 -c "import json,os; p=os.path.expanduser('~/.claude/sales-pipeline/smtp.json'); d=json.load(open(p)) if os.path.exists(p) else {}; print('ok' if d.get('email') and d.get('gmail_app_password') else 'missing')"
For action items (e.g. "send proposal Friday"), I can use:
A) Apple Reminders — best on Mac, requires `remindctl` CLI
B) Google Tasks — cross-platform, uses your Google Workspace CLI
C) Skip reminders — track action items only in pipeline state
Which would you like? (A / B / C)
Based on choice, set the following variables:
Based on choice, set the following variables. (Newlines shown as \n mean an actual newline character — keep the formatting exact.)
| Choice | REMINDER_TOOL | REMINDER_TOOL_PREFIX | REMINDER_LIST_NAME |
|---|---|---|---|
| A | remindctl | remindctl | Sales |
| B | gws tasks | gws | Sales |
| C | none | none | none |
| Choice | REMINDER_TOOL_BASH_CLAUSE | REMINDER_OPS_USE_LINE |
|---|---|---|
| A | , or remindctl`` | - Use remindctl for all reminder operations.\n |
| B | `` (empty — gws already in list) | - Use gws tasks for all reminder operations.\n |
| C | `` (empty) | `` (empty) |
| Choice | REMINDER_CONFIG_LINE |
|---|---|
| A | - **Reminders**: Apple Reminders ("Sales" list) via remindctl`` |
| B | - **Reminders**: Google Tasks ("Sales" list) via gws tasks`` |
| C | - **Reminders**: disabled — action items tracked in state.json only |
Render REMINDER_LIST_INIT_BLOCK, REMINDER_CHECK_BLOCK, and REMINDER_TOOL_DOCS accordingly:
Choice A (Apple Reminders):
REMINDER_LIST_INIT_BLOCK:
```bash
remindctl list "Sales" --create
This is idempotent — if the list already exists, it's a no-op.
REMINDER_CHECK_BLOCK:
Check today's and overdue sales reminders:
```bash
remindctl show today --list Sales
remindctl show overdue --list Sales
REMINDER_TOOL_DOCS: "the remindctl man page"Choice B (Google Tasks):
REMINDER_LIST_INIT_BLOCK:
```bash
gws tasks tasklists list
If a list named "Sales" doesn't exist, create it:
gws tasks tasklists insert --params '{"requestBody": {"title": "Sales"}}'
Idempotent — if the list already exists, the create call returns the existing list.
REMINDER_CHECK_BLOCK:
Check tasks due today or overdue:
```bash
gws tasks tasks list --params '{"tasklist": "TASKLIST_ID", "showCompleted": false, "dueMax": "TODAY_END_ISO"}'
REMINDER_TOOL_DOCS: "gws tasks reference"Choice C (None):
REMINDER_LIST_INIT_BLOCK: "Reminders disabled — track action items in state.json under each lead's pending_actions array. Skip the rest of this step."REMINDER_CHECK_BLOCK: "Reminders disabled — there are no reminders to check. Skip the rest of this step entirely (the 'For each incomplete/completed reminder' blocks below do not apply). Move directly to the next step. Use the pending_actions array in state.json as the source of truth for outstanding action items."REMINDER_TOOL_DOCS: "(disabled)"For pulling meeting transcripts into the recap:
A) Granola (https://granola.ai) — primary if you use it
B) Google Drive only — find call notes by company name
C) Skip meeting notes
Which? (A / B / C)
Set MEETING_NOTES_BLOCK:
Choice A (Granola): Insert the Granola integration block (uses Granola MCP tools list_meetings, get_meetings).
Choice B (Google Drive only): Insert a block that searches Drive for files with the company name and reads the most recent ones.
Choice C (skip): Insert "Meeting notes disabled. Fall back to asking the user directly during recap."
(Full text for each block is in references/meeting-notes-blocks.md — read it when you need to substitute.)
Tell the user this section drives the brand-voice skill. Run the questions in this order. Capture answers as multi-line strings.
14. Positioning — one sentence describing how you want to be perceived (e.g. "AI-as-a-service for SMBs", "the easiest accounting tool for freelancers"):
15. Primary market segment:
16. Secondary market segment (or "none"):
17. Products — name + one-line description for each, separated by newlines:
18. Taglines — list each tagline and where it gets used (hero, footer, etc.):
19. Three to five "we are" descriptors (e.g. "direct, warm, confident"):
20. Three to five "we are NOT" descriptors (e.g. "corporate, robotic, jargon-heavy"):
21. Cardinal rules — non-negotiable brand rules. List 3-7 (e.g. "Always lowercase 'i' in 'Ai'", "No claims that can't be proven"):
22. Banned words/phrases specific to your brand (e.g. industry buzzwords you refuse to use):
23. One on-brand example (a sentence/paragraph that nails the voice):
24. One off-brand example with explanation of why it fails:
Capture all answers. These become the {{...}} substitutions in brand-voice.md.tmpl.
Pick a brand skill name based on company name: <company-slug>-brand (e.g. acme-brand). Confirm with user.
Now your personal sales voice. I'll learn it from real examples.
25. Paste 3 examples of YOUR sales emails — actual ones you've sent. Cold outreach, follow-ups, intro replies, anything that sounds like "you" at your most authentic. Don't sanitize — I want the real voice.
(Paste them now, separated by --- on its own line.)
Wait for the user to paste. Then analyze the examples and extract:
TARGET_LENGTH (e.g. "60-80")VOCABULARY_USED)VOCABULARY_AVOIDED) — derive from common AI/sales clichés they didn't useSIGNOFF_BLOCK) — copy verbatim from one exampleVOICE_SNAPSHOT)Pick a voice skill name: talk-like-<first-name> (e.g. talk-like-jane). Confirm with user.
Based on those examples, here's what I picked up:
- Greeting: [observation]
- Length: ~[N] words per email
- Tone: [observation]
- Sign-off pattern: [block]
Look right? Anything to add or change?
Iterate until they're satisfied.
26. Internal team email addresses to filter OUT of sales discovery (e.g. "[email protected], [email protected]"):
27. Custom Gmail discovery query (optional — e.g. "from:partner-domain.com" if you have a referral source):
Capture: INTERNAL_TEAM_EMAILS, CUSTOM_DISCOVERY_QUERY (or empty string).
Then derive the conditional rendering variables:
INTERNAL_TEAM_FILTER_CLAUSE:
INTERNAL_TEAM_EMAILS non-empty: (to/from <value>) ← note leading spaceCUSTOM_DISCOVERY_QUERY_LINE:
CUSTOM_DISCOVERY_QUERY non-empty: 3. <value> (one full numbered list line)USER_PHONE_LINE:
USER_PHONE non-empty: - **<USER_FIRST_NAME>'s phone**: <USER_PHONE>\n ← include the trailing newlineSECONDARY_MARKET_LINE:
SECONDARY_MARKET non-empty and not literally none: - **Secondary market:** <value>none: `` (empty)Show a summary of all collected values and ask for final confirmation:
Ready to build. I'll:
- Create skill: hubspot-crm
- Create skill: <brand-skill-name>
- Create skill: <voice-skill-name>
- Create scheduled task: sales-evening-recap (runs daily at <evening-time> {{TIMEZONE}})
- Create scheduled task: sales-morning-outreach (runs daily at <morning-time> {{TIMEZONE}})
- Initialize: ~/.claude/sales-pipeline/state.json
- Verify: ~/.claude/sales-pipeline/smtp.json (already exists)
- Verify: ~/.claude/sales-pipeline/hubspot.json (already exists)
Proceed? (yes/no)
Once they confirm, do all of the following. Each is a separate operation; if one fails, report and continue with the rest where possible.
mkdir -p ~/.claude/sales-pipeline ~/.claude/skills/hubspot-crm
Also create dirs for the brand and voice skills:
mkdir -p ~/.claude/skills/<brand-skill-name> ~/.claude/skills/<voice-skill-name>
templates/crm-hubspot.md.tmpl{{COMPANY_NAME}} and any other placeholders~/.claude/skills/hubspot-crm/SKILL.md (use the Write tool)templates/brand-voice.md.tmpl{{...}} placeholder with collected values~/.claude/skills/<brand-skill-name>/SKILL.mdFor multi-line answers (cardinal rules, banned words, etc.), preserve formatting as bulleted lists. Example for CARDINAL_RULES:
### 1. {{rule_1_title}}
{{rule_1_body}}
### 2. {{rule_2_title}}
{{rule_2_body}}
templates/personal-voice.md.tmpl~/.claude/skills/<voice-skill-name>/SKILL.mdtemplates/state.json.tmpl~/.claude/sales-pipeline/state.json ONLY IF it doesn't already exist (don't overwrite an existing one)python3 -c "import os,sys; p=os.path.expanduser('~/.claude/sales-pipeline/state.json'); print('exists' if os.path.exists(p) else 'missing')"
If exists: ask the user "state.json already exists at ~/.claude/sales-pipeline/state.json — keep it or overwrite?" Default to keep.
templates/evening-recap.md.tmpl and templates/morning-outreach.md.tmplREMINDER_LIST_INIT_BLOCK, REMINDER_CHECK_BLOCK, MEETING_NOTES_BLOCK)Use the scheduled-tasks MCP tool mcp__scheduled-tasks__create_scheduled_task for each:
Evening recap:
{
"name": "sales-evening-recap",
"schedule": "<cron derived from EVENING_RUN_TIME and TIMEZONE_IANA>",
"prompt": "<full rendered evening-recap text>",
"description": "Interactive evening sales recap"
}
Morning outreach:
{
"name": "sales-morning-outreach",
"schedule": "<cron derived from MORNING_RUN_TIME and TIMEZONE_IANA>",
"prompt": "<full rendered morning-outreach text>",
"description": "Autonomous morning sales outreach + summary email"
}
If the scheduled-tasks MCP isn't available in the user's environment, write the rendered routines to ~/.claude/scheduled-tasks/sales-evening-recap/SKILL.md and ~/.claude/scheduled-tasks/sales-morning-outreach/SKILL.md as a fallback, and tell the user how to register them with whatever scheduler they prefer.
Print a confirmation summary:
Setup complete.
Skills installed:
- ~/.claude/skills/hubspot-crm/SKILL.md
- ~/.claude/skills/<brand-skill-name>/SKILL.md
- ~/.claude/skills/<voice-skill-name>/SKILL.md
Scheduled tasks created:
- sales-evening-recap (daily at <evening-time> <timezone>)
- sales-morning-outreach (daily at <morning-time> <timezone>)
Pipeline state:
- ~/.claude/sales-pipeline/state.json (initialized)
- ~/.claude/sales-pipeline/smtp.json (verified)
- ~/.claude/sales-pipeline/hubspot.json (verified)
Want me to run the evening recap once now in dry-run mode (no CRM writes, no reminder writes) so you can see it in action?
If yes, manually run through the evening recap logic — but for every Bash command that would write data (CRM, reminders, state.json), print "[DRY RUN] would run: " instead of executing it. Read-only operations (Gmail search, Calendar list, Drive list) run normally.
When rendering templates, every {{NAME}} token must be replaced. Here's the full list of variables and how to derive them:
| Variable | Source | Example |
|---|---|---|
USER_FIRST_NAME | Section A | Jane |
USER_FIRST_NAME_UPPER | derived | JANE |
USER_FULL_NAME | Section A | Jane Doe |
USER_EMAIL | Section A | [email protected] |
USER_PHONE | Section A | +1 555-555-5555 (or empty) |
COMPANY_NAME | Section B | Acme Corp |
COMPANY_DOMAIN | Section B | acme.com |
COMPANY_DESCRIPTION | Section B | a B2B fintech |
TIMEZONE | Section C | US Eastern |
TIMEZONE_IANA | Section C resolved | America/New_York |
MORNING_RUN_TIME | Section C | 8:30am |
EVENING_RUN_TIME | Section C | 6:00pm |
REMINDER_TOOL | Section F | remindctl / gws tasks / none |
REMINDER_TOOL_PREFIX | Section F | remindctl / gws / none |
REMINDER_LIST_NAME | Section F | Sales |
REMINDER_TOOL_BASH_CLAUSE | Section F derived | , or \remindctl`` / empty / empty |
REMINDER_OPS_USE_LINE | Section F derived | bullet line + newline / empty |
REMINDER_LIST_INIT_BLOCK | Section F derived | (block above) |
REMINDER_CHECK_BLOCK | Section F derived | (block above) |
REMINDER_TOOL_DOCS | Section F | "the remindctl man page" |
REMINDER_CONFIG_LINE | Section F derived | full bullet line, see table above |
INTERNAL_TEAM_FILTER_CLAUSE | Section J derived | (to/from <emails>) or empty |
CUSTOM_DISCOVERY_QUERY_LINE | Section J derived | 3. <query> or empty |
USER_PHONE_LINE | Section A derived | - **<First>'s phone**: <num>\n or empty |
SECONDARY_MARKET_LINE | Section H derived | - **Secondary market:** <val> or empty |
MEETING_NOTES_BLOCK | Section G | (block from references/meeting-notes-blocks.md) |
CRM_NAME | constant for HubSpot | hubspot-crm |
CRM_BASH_PREFIX | constant for HubSpot | curl |
BRAND_SKILL_NAME | Section H | acme-brand |
VOICE_SKILL_NAME | Section I | talk-like-jane |
POSITIONING | Section H | (user input) |
PRIMARY_MARKET | Section H | (user input) |
SECONDARY_MARKET | Section H | (user input or "none") |
PRODUCTS_LIST | Section H | (formatted bulleted list) |
TAGLINES_TABLE | Section H | (formatted markdown table) |
POSITIVE_DESCRIPTORS | Section H | (user input) |
NEGATIVE_DESCRIPTORS | Section H | (user input) |
CARDINAL_RULES | Section H | (formatted numbered list) |
BANNED_WORDS | Section H | (formatted bulleted list) |
VOICE_TONE_RULES | Section H derived | (synthesize from W/W-NOT) |
ON_BRAND_EXAMPLES | Section H | (user input, formatted) |
OFF_BRAND_EXAMPLES | Section H | (user input, formatted) |
VOICE_SNAPSHOT | Section I derived | (1-2 sentence summary of their style) |
GREETING_PATTERN | Section I derived | (extracted from examples) |
SENTENCE_STRUCTURE | Section I derived | (extracted from examples) |
VOCABULARY_USED | Section I derived | (extracted from examples) |
VOCABULARY_AVOIDED | Section I derived | (inferred — common AI/sales words not present) |
SIGNOFF_BLOCK | Section I derived | (extracted verbatim) |
TARGET_LENGTH | Section I derived | 60-80 |
EXAMPLE_1 / _2 / _3 | Section I | (user paste) |
INTERNAL_TEAM_EMAILS | Section J | [email protected], [email protected] |
CUSTOM_DISCOVERY_QUERY | Section J | (user input or empty) |
~/.claude/scheduled-tasks/<name>/SKILL.md and tell the user.~/.claude/plugins/sales-cadence/skills/sales-cadence-installer/templates/. Use ls to find the actual path.state.json if it exists, and it asks before overwriting any other file.references/alternative-workflow.md and discuss with them what they actually want, then decide whether this skill applies or whether to build something custom.Creates, edits, and optimizes skills for Claude Code, including drafting, evaluating with test prompts, iterating on performance, and improving skill descriptions for better triggering accuracy.
npx claudepluginhub samurai-code-ai/sales-cadence-plugin --plugin sales-cadence