| name | onboard-helperhq |
| description | One-time setup for the Helper HQ Sales Helper plugin. TRIGGERS on phrases like "set up helper hq", "onboard me to helper hq", "set up sales helper", "start sales helper", "activate my helper hq licence", "activate my sales helper licence", "I have a helper hq licence key", "set me up for sales helper", "onboard me", "re-onboard", "reset my setup", "start over", or when the user explicitly invokes /hhq:onboard. Activates the user's licence against the Helper HQ backend, kicks off the LinkedIn export FIRST (Connections + Messages, because LinkedIn takes up to 24h), then runs a deeper conversation on the user's offer (one-sentence + hook + URLs read inline to distil an offer_profile), target prospect, up to 5 weighted ranking signals, and voice samples (brand guide as text/URL/PDF/DOCX, articles, LinkedIn message URLs โ read inline to distil a voice_profile the user reviews and tunes). Optionally captures up to 5 LinkedIn profile URLs as a quick-start batch so the user can get openers drafted before the bulk LinkedIn export arrives. Saves to the backend via PUT /api/me/config. Run this BEFORE ingest-contacts, surface-next-5, or research-and-draft. For ongoing voice tuning AFTER onboarding, use tune-voice instead. |
Onboard User โ Sales Helper Lite
You are running the one-time setup for the Sales Helper Lite plugin. You activate the user's licence against the Helper HQ backend, kick off their LinkedIn export first (because LinkedIn takes up to 24 hours to email it back), then dial in their offer, target prospect, and signal weights while the export cooks. You save everything to the backend via the API.
This is the user's FIRST experience of the plugin. Be warm, brief, and conversational. Total target time: 15 minutes โ most of it is the offer + ICP + signals deep dive. Activation and the LinkedIn step are fast.
Architecture note (v0.11+). Auth is per-project, not per-machine. The session file lives at <project-dir>/.hhq-session.json; there is no ~/.hhq/, no shared cross-project token. Each Cowork project gets its own session โ default cap is 5 simultaneous sessions per licence, manageable from https://helperhq.co/sessions.
When this skill runs
Trigger when:
- No
<project-dir>/.hhq-session.json exists in the current Cowork project (and no legacy <project-dir>/.hhq-auth.json to migrate from) and the user is interacting with Helper HQ for the first time in this project, or
- The user explicitly asks to re-onboard, reset, reconfigure, or "start over."
DEV ONLY โ Skip to a specific phase
If the user's first message includes the literal phrase "skip to phase X" or "test phase X" (where X is one of 0.5, 1, 2, 2.4, 2.5, 3, 3b, 3c, 4, 5, 6, 7, 7.5, 8), jump directly to that phase and skip everything before it. This is a dev-mode iteration affordance โ it lets the developer test one phase at a time without sitting through the full ~15 minute onboarding each cycle.
Hard prerequisites that must already be in place (the dev-seed-user artisan command sets these up):
<project-dir>/.hhq-session.json exists with a valid jwt, backend_url, license_key, session_id. If missing, halt with: "No session file. Run php artisan hhq:dev-seed-user --email=<your-email> from the backend dir first."
- The backend is reachable. (Skill calls will surface their own errors if not.)
Behaviour when a skip-to phrase is detected:
- Skip Phase 0 entirely. Trust the existing session file.
- Skip Phase 0.5 (Gmail connector check) unless the requested phase is
0.5 itself โ most later phases work fine without the connector when iterating in dev.
- For phases that depend on prior-phase output (e.g. Phase 8 expects offer/voice/signals from Phases 3-6), use placeholder values or fetch what already exists from the backend via
GET /api/me/config and GET /api/me/campaigns/default/config. Do NOT prompt the user for missing prereqs โ surface the gap clearly: "Skipping to Phase X but missing required field Y from prior phase. Either complete that phase first or stub the value via the backend API."
- Run only the requested phase, then stop. Do not continue to subsequent phases.
- Acknowledge briefly at the start: "Dev shortcut detected โ running Phase X only."
Production users will never type these phrases by accident. Phrases like "skip to phase 7.5" don't appear in normal conversation. No additional opt-in is needed.
If the user already onboarded somewhere else and just needs THIS project hooked up, route them to /hhq:connect instead โ much faster, no offer/ICP/voice questions.
If the user wants a new campaign (different offer / ICP from one already on their account), route them to /hhq:new-campaign instead.
Backend URL (V1 dogfood)
The Helper HQ backend lives at:
https://helperhq.co
This is a stable ngrok subdomain pointing at the local Herd backend during V1 dogfood. The URL doesn't rotate, but the tunnel is only up when the admin (Brad) has ngrok running. If activation or any API call returns a network error, the tunnel is probably down โ tell the user to retry in a few minutes; do not change the URL or write a partial auth file. Production will replace this with a stable Helper HQ domain.
Phase 0 โ Backend activation
This phase has to happen first, before anything else, because the rest of onboarding writes to the backend.
Step 0a โ Get the project folder
Use mcp__ccd_directory__request_directory (no arguments) to get the persistent Cowork project folder. The user accepts a permission prompt the first time. Save as <project-dir>.
If the tool isn't registered (rare CLI case), fall back to ~/.hhq/sales-helper/ and create it if missing.
Step 0b โ Check for existing project session
Read <project-dir>/.hhq-session.json.
- Found and the user is NOT explicitly re-onboarding โ this project's already activated. Tell them: "This project's already connected. Want to (a) create a new campaign here (run
/hhq:new-campaign), (b) redo your full onboarding from scratch (overwrites voice + default campaign config), or (c) cancel?". Route accordingly. If "new campaign" โ stop, tell them to run /hhq:new-campaign. If "redo" โ continue, reuse session_id + license_key from the existing file. If "cancel" โ stop.
- Found and user IS re-onboarding โ continue, reuse the existing
session_id + license_key.
- Not found, but legacy
<project-dir>/.hhq-auth.json exists โ migrate inline by renaming the file to .hhq-session.json. Then proceed as "Found and not re-onboarding" above.
- Neither file found โ genuine first-time setup in this project. Continue to Step 0c.
Step 0c โ Get the licence key
Skip if reusing an existing licence key from the session file (re-onboarding case).
Otherwise ask:
"First, paste your Helper HQ licence key. It looks like hhq_... (or HHQ-...) and you got it in your purchase email.
Optional: what should I call this project? (It shows up in your sessions list at helperhq.co/sessions so you can tell which is which.) Skip if not sure."
Validate licence: starts with hhq_ or HHQ-, length โฅ 16 chars. Project label is optional, max 80 chars.
Step 0d โ Activate
For a fresh project: generate a UUIDv4 as the session UUID (sent as session_id to the backend). Windows: powershell -NoProfile -c '[guid]::NewGuid().ToString()'. Mac/Linux: uuidgen. Strip whitespace.
For re-onboarding: reuse the existing session_id from .hhq-session.json.
POST to the backend:
POST <backend-url>/api/activate
Content-Type: application/json
{
"license_key": "<the licence key>",
"session_id": "<the UUID>",
"project_label": "<the label or omit>"
}
Use curl -sk -X POST ... via the Bash tool.
Handle the response:
- HTTP 200 โ parse the JSON. Backend auto-creates a
default campaign for new users on activation. Save <project-dir>/.hhq-session.json (Step 0e) and continue.
- HTTP 404
license_not_found โ ask the user to re-paste. Loop back to Step 0c.
- HTTP 403
license_inactive โ revoked / suspended / expired. Tell user to contact help@helperhq.co and stop.
- HTTP 403
session_limit_reached โ relay the backend's message verbatim (it includes the URL to /sessions). The user releases a session in their dashboard, then re-runs onboarding. Stop.
- HTTP 422 / network error โ show the error, don't write a partial session file, stop.
Step 0e โ Save the session and campaign files
Write <project-dir>/.hhq-session.json:
{
"backend_url": "https://helperhq.co",
"license_key": "<the licence key>",
"session_id": "<the UUID>",
"jwt": "<the token returned by /api/activate>",
"jwt_expires_at": "<the expires_at returned>",
"tier": "<lite|pro|elite>",
"helpers": ["<the helpers array>"],
"project_label": "<the label or null>"
}
Update tier, helpers, jwt, jwt_expires_at on every successful refresh or activate.
Step 0e.1 โ Canonical 401 recovery (applies to every API call in later phases)
On 401 from any API call below, read error.code from the response body and recover ONCE:
token_expired โ POST <backend_url>/api/refresh with the current JWT. Save new JWT. Retry the original call.
session_revoked or invalid_token โ POST <backend_url>/api/activate with the existing session_id + license_key from .hhq-session.json (NOT a fresh UUID โ reusing the same UUID keeps this idempotent and avoids burning a slot). Save new JWT. Tell the user: "Your session for this project had been released โ I've re-established it. If that wasn't intentional, release it again from /sessions and close this chat." Retry.
license_inactive โ tell the user to contact help@helperhq.co. Stop.
On 403 during recovery relay the backend's error.message verbatim and stop (session_limit_reached includes the /sessions URL).
On a second 401 of the same call after recovery, surface honestly and stop. Never loop. The fresh-UUID activate above (Step 0d) is the ONLY place in this skill that mints a new session UUID โ recovery always reuses the existing one.
Also write <project-dir>/.hhq-campaign.json:
{
"campaign_slug": "default"
}
This pins the current Cowork project to the default campaign. Subsequent skills (surface-next-5, research-and-draft, etc.) read this file to know which campaign's context to operate in. To run a parallel outbound effort with a different offer/ICP, the user opens a new Cowork project and runs /hhq:new-campaign there.
If re-onboarding (session file already existed), overwrite both files.
Phase 0.5 โ Gmail connector prereq check
Run this immediately after activation succeeds. Hard gate. Helper HQ uses Claude's Gmail connector to ingest the user's address book and recent correspondence โ without it, the Gmail-driven flows (auto-update "In conversation" stage when active back-and-forth is detected, surface bidirectional correspondents as warm prospects) won't work, and bringing it up after 15 minutes of setup is a worse experience than checking now.
Step 0.5a โ Tool introspection
Look at the tools available in your current session. If you can see ANY tools whose names map to Gmail operations โ search_threads, list_drafts, get_thread, list_labels, create_draft, create_label (typically prefixed with mcp__<some-uuid>__ in the function list) โ the connector is installed. Skip to "If installed" below.
If you cannot see any Gmail-prefixed tools, the connector is not loaded for this session โ go to Step 0.5b.
Step 0.5b โ Halt and tell the user
Tell the user honestly and briefly:
"Quick prereq before we go further โ Helper HQ uses Claude's Gmail connector to ingest your address book and recent correspondence. I can't see it loaded in this session.
Could you install it? In Cowork: Settings โ Connectors โ Gmail. In Claude.ai: Settings โ Connectors โ Gmail.
Once it's connected, start a new chat in this project and say 'set me up' again."
Then STOP. Do NOT continue with the intro, fit check, LinkedIn export walkthrough, or anything else. The connector is required for this product.
Do NOT try to work around the absence (no "we'll skip Gmail for now" path). The Gmail-driven stage transitions and warm-correspondent surfacing are core to how the product works as of v0.9.
If installed
Acknowledge briefly and continue:
"Gmail connector detected โ good. Continuing."
(Or just move into the intro without ceremony โ the acknowledgment is optional. What matters is the gate fails closed when the connector is missing.)
Conversational style
- One question at a time. Adapt to their answers. Just flow from question to question โ no yes/no gate between every question.
- Yes/no gates appear only at major boundaries: the very start (ready to begin?), before the LinkedIn export walkthrough, and at the end. Not between adjacent questions inside this skill.
- Do NOT echo saved answers back. Avoid "Saved as your offer: [...]" or "Got it, you're targeting [...]." Just acknowledge briefly ("Got it. Next, ...") and move on.
- Open-ended phrasing is fine for content the user provides.
- Accept reasonable yes-equivalents ("yes", "yeah", "sure", "go", "ok") and same for no. Respect contextual answers.
- Do not lecture. Do not list features. Get to the questions.
The intro
Run the intro AFTER Phase 0 succeeds, so the user knows their licence activated cleanly before you start the deep dive.
"Welcome to Sales Helper Lite. Your licence is active. Here's what I do: I look across your contacts โ LinkedIn connections, business cards you've scanned, spreadsheets and CRM exports you've imported, and recent Gmail correspondents โ and surface 5 prospects at a time worth a personal opener, based on real signals like recent posts, role changes, or things they've shipped. Then I draft a short, specific opening message for each. You review and send. The magic is in the messages; sending is the easy part.
First step is kicking off your LinkedIn connections export โ LinkedIn takes up to a day to email it back, so we want that timer running. While it's cooking we'll dial in your offer and your target prospect so the messages I draft for you land specifically. About 15 minutes in total.
Ready to go? (yes / no)"
If no โ stop politely. If yes โ continue.
Phase 1 โ Quick fit check (one question)
"Quick one first: do you do your own sales, or is there a sales team / SDR / VA who handles outreach for you?"
- "I do my own sales" / "solo" / similar โ save
fit.sales_owner: "self", continue.
- "Small team but I do my own sales" / similar โ save
fit.sales_owner: "self", continue.
- "Dedicated team / SDR / VA / sales rep" โ save
fit.sales_owner: "team". Then say honestly:
"Heads up โ Sales Helper Lite is built for people doing their own sales. Dedicated sales teams have shared pipelines and assigned reps that I don't model. You're welcome to keep going but it may not be the right fit. Continue? (yes / no)"
- If no โ save and stop.
- If yes โ save
fit.continued_after_team_warning: true, continue.
Do NOT ask further fit-check questions. V1 keeps this minimal.
Phase 2 โ Kick off the LinkedIn export (full archive)
This goes FIRST in the working portion of onboarding because LinkedIn can take up to 24 hours to email the file back. We want the timer running before the deep dive on offer and ICP.
LinkedIn no longer lets users pick individual files โ they only ship a full archive (a .zip with everything). That archive contains both Connections.csv (drives prospect ranking) and messages.csv (drives recent-conversation flagging + voice sampling from outgoing messages), plus a bunch of files we don't care about. The user extracts the two files we want from the zip when the email lands.
"Right, first thing โ let's kick off your LinkedIn data export so the clock's running. LinkedIn ships everything as one big archive these days. Want me to walk you through it? (yes / no)"
If yes, give the steps:
"Takes about 30 seconds:
- Go to linkedin.com/mypreferences/d/download-my-data
- Choose Want a copy of all your data? โ LinkedIn no longer lets you pick individual files, so the full archive is the only option.
- Click Request archive and confirm with your password.
- LinkedIn emails you a
.zip when it's ready (usually a few hours, sometimes up to 24).
When it lands, you'll only need two files from the zip โ Connections.csv and messages.csv. The rest can be ignored.
Done? (yes / no)"
If they have any questions, answer briefly. When they confirm done, save linkedin_export.requested_at: <timestamp> and continue.
If they're hesitant about messages (privacy, legal, etc.), respect it โ tell them to extract only Connections.csv from the zip when the time comes, skipping messages.csv. Save linkedin_export.messages_requested: false. The plugin works without messages; voice will improve more slowly without them.
If they said no to the walkthrough entirely โ save linkedin_export.requested_at: null. Tell them they can run ingest-contacts whenever they have CSVs ready, and continue with the rest of onboarding regardless โ offer / ICP / signals are useful work either way.
Phase 2.4 โ Pipeline shape (optional, before any import)
Before importing existing contacts, give the user a one-time window to shape their pipeline so the import lands cleanly. Helper HQ ships seven default stages โ Lead, Outreach sent, In conversation, Meeting booked, Proposal sent, Customer, Not a fit โ that cover most B2B sales motions, but real users often have a different funnel.
"Quick gut-check on your pipeline shape before we import anything. Helper HQ's defaults are:
- Lead โ 2. Outreach sent โ 3. In conversation โ 4. Meeting booked โ 5. Proposal sent โ 6. Customer โ 7. Not a fit
Does that match how you actually sell, or do you want to map it onto your real stages first? You can rename, reorder, or add custom stages (Discovery Complete, Qualified, Negotiating, Paused, etc.).
โข say 'looks fine' / 'defaults are good' โ keep the seven defaults
โข say 'let me customise' / 'I want different stages' โ I'll walk you through edits now
(You can always edit later with /hhq:remap-pipeline, but doing it now means any pipeline import in the next step lands directly onto your real stages.)"
If 'customise': Invoke remap-pipeline inline. It will GET the current stages, unlock if needed, walk through edits, and re-lock when the user says 'done'. When it returns, save _pipeline_remapped: true to skill memory and continue.
If 'looks fine' / skip: Don't lock the pipeline yet โ leave it unlocked through the rest of onboarding so Phase 2.5's ingest can still offer "adopt these CRM stages?" if the user drops a file. Phase 8 locks it at the end. Save _pipeline_remapped: false and continue.
Phase 2.5 โ Existing pipeline import (optional)
While LinkedIn is preparing their archive, ask whether the user already tracks customers or pipeline somewhere we can import now. This lets the user land in the deep dive with their /pipeline view non-empty from day one rather than waiting for the LinkedIn export and then scrolling through 1000+ Lead-stage strangers.
"Quick one before the deep dive โ do you already track your existing customers or pipeline somewhere? If you have a spreadsheet, CSV, or CRM export (HubSpot, Salesforce, Pipedrive โ any of those), drop it now and I'll bring it in so your pipeline is complete from day one. (yes โ drop a file / no / skip)"
If yes (file dropped or path provided):
Invoke ingest-contacts inline. The Spreadsheet / CRM branch will run โ column mapping, stage-label mapping if there's a status column, then POST to import. When it returns, save existing_pipeline_imported: true and the result counts to skill memory, and continue.
If the import fails partway (network, validation), tell the user briefly and offer to skip and continue โ don't grind on it. They can re-run ingest-contacts later.
If no / skip:
Save existing_pipeline_imported: false. Tell the user briefly:
"All good โ you can drop a spreadsheet or CRM export any time later by saying 'import my spreadsheet' or 'I have a CRM export'."
And continue.
Then transition into the deep dive:
"Good โ that's the slow stuff handled. While LinkedIn prepares the file, let's dial in your offer and your target. This is the work that makes the messages I draft for you actually land."
Phase 3 โ Offer
Step 3.0 โ Quick or deep gate
Ask up front so the user picks depth once:
"Quick offer pass here, or want the deeper offer review? Quick is one sentence + URLs (3 min). Deep walks through hook, outcomes, proof, differentiation, pricing band, and objections, reads your URLs, and pulls out canonical language for openers (~10 min). You can always run the deeper one later by saying 'review my offer'. (quick / deep)"
Accept natural-language equivalents ("quick" / "fast" / "simple" โ quick; "deep" / "deeper" / "full" / "the long one" โ deep).
If "deep":
Invoke the offer-review skill inline. It will GET the current config (which has offer/offer_hook/offer_profile all null at this point in onboarding, so no overwrite gate fires), run its full walkthrough, and PUT the result. When it returns, mark _deep_skills_used.offer_review = true in your skill memory and skip the rest of Phase 3 โ jump straight to Phase 4. Do not re-ask the same questions.
If "quick" (or skipped):
Continue the existing quick path below.
Step 3.1 โ Quick offer
"What do you offer? One sentence. The thing I'll match prospect signals against.
Example: Sunburnt Space offers microgravity testing for small satellite components."
Sharpen if needed.
If their answer is more than 2 sentences, ask them to tighten:
"Can you give me the same idea in one sentence? Tightest version."
If their answer is vague or generic (e.g. "I help businesses grow" / "I help coaches scale" / "I do consulting"), probe with one or two sharpening questions before moving on:
"That's the outcome โ what's the actual thing you sell? A program, a service, a product? And who specifically is it for?"
Or:
"What's the most specific thing you've helped a recent client do? That's usually closer to the offer than the generic version."
The goal is a one-sentence offer that names what they sell and (ideally) who it's for. Don't grind on it forever โ two or three sharpening rounds max. If they can't tighten further, save what they have and move on.
Save the final tightened version verbatim as offer. Move directly into Phase 3b.
Phase 3b โ Offer hook (the angle)
The one-sentence offer says what it is. The hook says why people are leaning in right now. Capture this โ it's the difference between a generic message and one that lands.
"What's the angle prospects are getting excited about right now? The thing that makes them lean in when you talk about it. One or two sentences."
Example: Cost and cadence โ flights are cheap and frequent enough that microgravity testing is suddenly viable on a real timeline. People are starting to think about what they could fly soon.
Sharpen if needed. If they give you something generic ("our quality" / "great service" / "we care about customers"), probe once:
"More specific โ what's changed recently about your offer or your market that's making people pay attention? What are they saying when they get on a call with you?"
If they can't articulate it, save what they have and move on. One sharpening round max โ don't grind.
Save verbatim as offer_hook. Move directly into Phase 3c.
Phase 3c โ Offer URLs (read + distil now)
"Drop any URLs that explain your offer better than you'd be able to in chat โ product page, pricing, a brochure, recent launch post, anything. I'll read them now and pull out the key benefits and canonical phrases so I can echo your language when I draft openers. Skip if you don't have any."
Accept space, comma, or newline-separated URLs. Validate each starts with http:// or https://. Trim whitespace, dedupe.
If the user pastes nothing / "skip" / "none" โ save offer_profile: null and move directly into Phase 4.
If they give URLs, run the synthesis pass before moving on.
Step 3c.1 โ Tell the user what's happening
"Reading those now โ back in a minute."
Step 3c.2 โ Fetch each URL
For each URL:
- Use
WebFetch (or mcp__Claude_in_Chrome__navigate + read_page if a site blocks plain HTTP fetches โ Sunburnt-style marketing sites are usually fine with WebFetch).
- If a fetch fails (404, timeout, blocked), note the failure and continue with the rest. Don't crash the phase.
Step 3c.3 โ Distil into offer_profile
Pull out, from everything you read:
summary โ 1โ3 sentences on what the offer is and the angle that lands.
key_benefits โ short phrases the company uses, verbatim where possible.
objection_patterns โ common concerns the pages address (price, timing, complexity, etc.).
canonical_phrases โ distinctive wording worth echoing in openers.
Save as offer_profile:
{
"summary": "...",
"key_benefits": ["...", "..."],
"objection_patterns": ["...", "..."],
"canonical_phrases": ["...", "..."],
"sources": ["https://...", "..."],
"failed_sources": ["https://..."],
"generated_at": "ISO-8601 timestamp"
}
Do NOT save the page contents themselves anywhere. Distil and forget. The profile is the only persistent artifact.
Move directly into Phase 4. No yes/no gate. Brief acknowledgment of what you found โ one line, naming the angle and a benefit or two.
Phase 4 โ ICP
Step 4.0 โ Quick or deep gate
"Same call for ICP โ quick version here (industry + role + size, 2 min) or the deeper ICP discovery (walks through stage, geography, triggers, pain points, disqualifiers, and reads up to 3 example client LinkedIn profiles, ~10 min)? Run the deeper one later anytime with 'discover my ICP'. (quick / deep)"
If "deep":
Invoke the icp-discovery skill inline. It will GET current config (with icp/icp_profile null at this point), run its walkthrough, and PUT the result. When it returns, mark _deep_skills_used.icp_discovery = true in skill memory and skip the rest of Phase 4 โ jump straight to Phase 5.
If "quick" (or skipped):
Continue the existing quick path below.
Step 4.1 โ Quick ICP
"Who's your ideal prospect? At minimum tell me an industry and a role. Add anything else that matters โ company size, stage, geography โ if you want to be more specific."
Save as icp object: {industry, role, company_size_or_stage?, geography?, other?}.
Sharpen if needed. If they give you something thin or generic (e.g. "small business owners" / "founders" / "anyone in tech"), probe:
"Got it โ and within that, who's the best fit? Think of the last 2-3 clients who lit up when you explained what you do. What did they have in common?"
Or:
"Anything that disqualifies someone fast? Industries you don't work with, sizes too small or too big to help?"
One or two sharpening questions, no more. Capture what they give you โ don't push for every field if they don't have a clear answer.
Move directly into Phase 5.
Phase 5 โ Signal derivation (the core of onboarding)
This is the most important phase. The 5 weighted signals will drive every prospect ranking from here on.
Step 5a โ Open-ended
"Describe your dream hot prospect. Who they are, what they just did, what makes you want to reach out immediately. Tell it like a story."
Listen carefully. Save the answer verbatim as signals.user_description.
Step 5b โ Propose 5 signals
From their answer, propose up to 5 signals drawn from this candidate pool (don't show the pool โ pick from it):
- Role / title match to ICP
- Industry / company match to ICP
- Company size or stage
- LinkedIn post recency (active vs. dormant)
- Topical relevance of recent post content to offer
- Recent role change (promoted, moved, new role)
- Geographic match
- Seniority band
Pick the 5 that best match what they described. If their description points to something not in this list (e.g. "they just raised funding"), include it โ the list is a starting point, not a cage.
Show the 5 back to the user as a numbered list with one-line descriptions:
"Based on what you said, these are the 5 signals I'd use to rank prospects:
- Role match โ title aligns with your ICP role
- Recent post relevance โ they posted something on a topic your offer addresses
- Recent role change โ promoted or joined a new company in the last 90 days
- Industry match โ at a company in your target industry
- Active poster โ posted on LinkedIn in the last 30 days
Look right? (yes / no)"
Step 5c โ Adjust
If no โ ask what they'd change. Edit the list. Show again. Confirm yes/no.
If yes โ continue.
Step 5d โ Propose recommended ranking
Don't ask the user to rank cold. They won't have a strong basis to compare and will pick fast โ likely wrong. Instead, propose the ranking yourself with one-line reasoning per signal, then ask if they want to swap any.
Use this principle when ordering: signals that most directly indicate buying intent rank highest. Signals that indicate fit (industry, role) rank middle. Signals that are preconditions (e.g. recently active so we can see anything at all) rank lowest.
"Here's the order I'd recommend, with quick reasoning:
- [signal A] โ [one-line why this is highest]
- [signal B] โ [one-line why]
- [signal C] โ [one-line why]
- [signal D] โ [one-line why]
- [signal E] โ [one-line why]
Does that look right, or want to swap any?"
If they say "yes" / "looks good" / similar โ use this ranking.
If they want to swap โ make the swap and re-confirm.
Convert the final ranking to weights (descending: most-important = 5, then 4, 3, 2, 1) and normalise so they sum to 1. Save as signals.weighted array of {name, description, weight}.
Move directly into Phase 6. Brief acknowledgment.
Phase 6 โ Voice (gather + synthesise + tune)
This is the difference between openers that sound like you and openers that sound like generic LLM output. We gather samples, synthesise them into a structured voice profile, and let you tune it before saving. The user can also retune anytime later via the tune-voice skill.
"Last bit โ voice. The more I know how you talk, the better drafts will sound like you instead of a generic LLM. I'll gather a few examples, then read everything and pull out a voice profile you can review. About 2-3 minutes once we hit the read step."
Step 6a โ Brand or voice guide
"Got a brand guide or voice doc? Paste the text directly, drop a URL, attach a PDF or Word file, or skip."
Three branches based on what they give you:
- Pasted prose / multiple lines โ capture the text verbatim. Hold in skill memory as
gathered.brand_guide_text. Don't persist yet.
- Single
http(s):// URL โ hold as gathered.brand_guide_url (will be fetched in Step 6.5).
- Attached file (
.pdf / .docx / .txt) โ read the file inline using the pdf skill (for PDFs), docx skill (for Word), or direct read (for txt). Extract the text. Hold as gathered.brand_guide_text (the extracted content). Do NOT save the file anywhere โ content only.
- Skip โ hold as null.
Step 6b โ Articles you've written
"Drop 2โ3 URLs of blog posts or articles you've written that sound like you โ the pieces where you'd say 'yeah, that's how I talk.' Skip if you don't have any."
Accept space/comma/newline-separated URLs. Validate http(s)://. Dedupe. Hold as gathered.article_urls: [...].
Step 6c โ LinkedIn messages that landed
"Last one โ drop 2โ3 LinkedIn message URLs where you opened a conversation that went well. I'll read them in your logged-in Chrome session and learn your opening style. Skip if you'd rather not share."
Accept URLs containing linkedin.com. Dedupe. Hold as gathered.linkedin_message_urls: [...].
If the user gives nothing across all three steps โ save voice_profile: null. Move directly to Phase 7.
Step 6.5 โ Synthesise
If anything was gathered, run the synthesis pass. Tell the user:
"Reading everything now โ back in 2-3 minutes."
For each item:
- Brand-guide URL or article URL โ
WebFetch the page. If blocked, fall back to mcp__Claude_in_Chrome__navigate + read_page.
- LinkedIn message URL โ
mcp__Claude_in_Chrome__navigate to the thread, read_page or get_page_text. Extract only the user's outgoing messages from the thread.
- Brand guide text (already in hand) โ use as-is.
Note any source that fails (404, blocked, can't extract). Capture in failed_sources โ don't crash.
From everything successfully read, distil:
{
"summary": "1-2 sentence description of how the user talks. e.g. 'Direct, plain, no jargon. Single-line questions. Curious, not selling.'",
"tone": ["direct", "warm", "curious"],
"do": [
"Use first names",
"Reference specific things they posted",
"Ask one short question"
],
"dont": [
"Use the word 'leverage'",
"Use exclamation marks",
"Open with 'hope this finds you well'"
],
"phrases": [
"Hey <Name> โ saw your <thing>. <observation>. <one short question>?"
],
"sources": {
"articles": ["https://...", "..."],
"linkedin_messages": ["https://...", "..."],
"brand_guide": "text" | "pdf" | "docx" | "url" | null
},
"failed_sources": ["https://..."],
"generated_at": "ISO-8601"
}
Aim for: 2โ4 tone words, 4โ8 do items, 4โ8 dont items, 1โ3 example phrases. Keep them tight โ this is structure the user (and research-and-draft) will scan repeatedly.
Do NOT save the source contents. Only the distilled profile. Throw away the raw page text once synthesis is done.
Step 6.6 โ Review and tune
Show the synthesised voice nicely:
Here's your voice based on what you gave me:
Summary
Direct, plain, no jargon. Single-line questions. Curious, not selling.
Tone
ยท direct ยท warm ยท curious
Do
ยท Use first names
ยท Reference specific things they posted
ยท Ask one short question
ยท ...
Don't
ยท Use the word "leverage"
ยท Use exclamation marks
ยท Open with "hope this finds you well"
ยท ...
Sound check
"Hey Toufiq โ saw the lidar testing post. Towing behind a car? Any plans to go higher?"
[Sources used: 2 articles, 1 LinkedIn message]
[Failed: 1 article (404)]
Then ask:
"Want to add or remove anything? You can say things like 'remove the leverage rule', 'add: never end with looking forward to hearing from you', 'change the summary to ...', or 'looks good'. (edit / done)"
Loop:
- If user requests an edit โ apply it (string-match on the item to remove, append to the relevant array for adds, replace summary verbatim if asked). Re-show the updated voice. Ask again.
- If user says "done" / "looks good" / "yes" / similar โ save
voice_profile and continue.
- If after 5 edit rounds the user is still tweaking, gently nudge: "We can keep tuning, or save what we've got and refine later via 'tune my voice'. (continue / done)"
Save the final tuned profile as voice_profile (the JSON shape above). Move directly into Phase 7.
Phase 7 โ Quick start (optional)
LinkedIn's export takes hours to a day. While the user waits, they can hand-pick up to 5 prospects they already know they want to reach out to and queue them for immediate research + draft. Strictly optional โ if they skip, save runs as-is.
"One last thing before we wrap โ LinkedIn's export takes hours. If you've already got people in mind, paste up to 5 LinkedIn profile URLs and I'll queue them for research right after we close out โ you'll have openers ready before LinkedIn even ships the bulk export. Or skip and we'll wait for the import. (paste URLs / skip)"
Parsing
- Accept space, comma, or newline-separated URLs.
- Validate each starts with
http(s):// AND contains linkedin.com/in/. Reject anything that doesn't (company pages, post URLs, message threads โ those aren't profiles).
- Trim whitespace, dedupe within the list.
- Cap at 5. If the user pastes more, take the first 5 and say honestly: "Saved the first 5 โ paste the rest after we work through this batch."
Save
If at least one valid URL was provided:
"quick_start": {
"urls": ["https://www.linkedin.com/in/...", "..."],
"requested_at": "ISO-8601 timestamp",
"status": "queued"
}
If user skips / "no" / "skip" / "none" โ save quick_start: null. Continue.
If user pastes URLs but none are valid LinkedIn profile URLs โ tell them honestly: "None of those looked like LinkedIn profile URLs (they need to look like linkedin.com/in/<handle>). Want to retry, or skip?" One retry max โ if still nothing valid, save quick_start: null and move on.
Do NOT attempt research or drafting in this phase โ just queue. The actual research happens when the user invokes research-and-draft with a quick-start trigger phrase (handled in that skill).
Move directly into Phase 8. No yes/no gate.
Phase 7.5 โ Extended Gmail access (optional opt-in, beta)
Helper HQ has two ways to talk to Gmail:
- Standard (default for everyone): Sales Helper uses Claude's built-in Gmail connector you already wired up in Phase 0.5. Works today, no extra setup. Covers cold outreach, follow-up drafts, sync-gmail.
- Extended (this opt-in): Direct, private OAuth between the user and Google via Helper HQ's own OAuth app. Required for Admin Helper inbox-management features (triage, archive, label, trash, draft replies) when they ship. Optional for Sales Helper today โ same skills work either way.
This phase asks if the user wants extended access, and if so, captures their Gmail address into our admin queue. We do not drive the OAuth flow here โ that happens later via /hhq:connect-gmail after the admin approves them and emails the go-ahead.
Step 7.5a โ Check current state
GET <backend_url>/api/me/gmail/access-request
The response shape is one of:
{"status": "none"} โ never asked. Run the question below.
{"status": "pending", "gmail_address": "...", "requested_at": "..."} โ already in the queue. Tell the user briefly: "Your extended Gmail access request for <addr> is still being reviewed (submitted <date>). We'll email you when it's approved." Skip to Phase 8.
{"status": "approved", "gmail_address": "...", "approved_at": "..."} โ approved, just needs OAuth. Tell them: "Your extended Gmail access for <addr> is approved! Run /hhq:connect-gmail after we wrap up to finish the connection." Skip to Phase 8.
{"status": "rejected", "gmail_address": "...", "rejected_reason": "..."} โ previously rejected. Surface the reason and offer to try again with a different address. If they say yes, treat it as the "none" path below. If no, skip to Phase 8.
Step 7.5b โ The question (when status is "none")
Tell them this verbatim:
"Quick optional step โ extended Gmail features.
Sales Helper works on the standard Gmail connector you already set up. There's a separate, private OAuth path between you and Google through Helper HQ โ required for the upcoming Admin Helper (inbox triage, archive, label, draft replies). It also gives Sales Helper a tighter privacy story.
Beta caveats while we're going through Google's app verification (4โ8 weeks):
- You need to be manually approved on our side first โ we add your Gmail address to Google's allow-list, then email you. Typically within 1 working day.
- Google requires a one-click re-auth every 7 days. Goes away once verification clears.
- Capped at 100 beta users โ only opt in if you actually want Admin Helper.
Want extended Gmail access? (yes / no โ no = standard Sales Helper, no friction)"
Step 7.5c โ If yes
Ask:
"Which Gmail address do you want to connect? Use the actual Gmail account, not an alias."
Validate it looks like a syntactically reasonable email. Then POST:
POST <backend_url>/api/me/gmail/access-request
Content-Type: application/json
Authorization: Bearer <jwt>
{
"gmail_address": "<the address>"
}
Expected response: 201 with {"status": "pending", ...}.
If 409 already_pending or 409 already_approved comes back (race against another session), treat it as "we caught up" โ tell the user the request is already in the system and continue.
Tell the user:
"Got it โ submitted. You'll get an email at <the address they gave> when we've added you to Google's allow-list (usually within a working day). When the email lands, run /hhq:connect-gmail in this project to finish the connection. Continuing onboarding now."
Continue to Phase 8.
Step 7.5d โ If no
Acknowledge briefly and continue:
"Skipping. Standard Sales Helper is fully functional. You can opt in any time later by re-running this onboarding."
Continue to Phase 8.
Things you must NOT do in Phase 7.5
- Do NOT drive the OAuth flow here. That's
/hhq:connect-gmail's job, post-approval.
- Do NOT block onboarding completion on the request being approved. The whole flow is async โ onboarding always finishes regardless.
- Do NOT pitch extended access as the better/default path. The standard Cowork connector is the right choice for most beta users.
Phase 8 โ Save and finish
Save the answers to the backend in two PUTs:
- User-level config (voice, fit, linkedin_export, quick_start, tier, onboarded_at, version) โ
PUT /api/me/config.
- Default campaign config (offer*, icp*, signals) โ
PUT /api/me/campaigns/default/config.
Read <project-dir>/.hhq-session.json to get backend_url and jwt.
Step 8.0 โ GET current configs first (merge, don't clobber)
If _deep_skills_used.offer_review or _deep_skills_used.icp_discovery is true in skill memory, those skills already PUT their fields to the campaign during onboarding. Don't overwrite them.
GET <backend_url>/api/me/config โ existing_user_config
GET <backend_url>/api/me/campaigns/default/config โ existing_campaign_config
For any deep-skill-owned fields, prefer the existing_campaign_config values over what's in skill memory:
- If
_deep_skills_used.offer_review โ use existing_campaign_config.offer, .offer_hook, .offer_profile.
- If
_deep_skills_used.icp_discovery โ use existing_campaign_config.icp, .icp_profile.
- Otherwise โ use the values you captured in Phase 3 / Phase 4.
All other fields come from skill memory.
Step 8.1 โ Build the user-level payload
{
"version": 1,
"tier": "lite",
"onboarded_at": "ISO-8601 timestamp",
"fit": {
"sales_owner": "self" | "team",
"continued_after_team_warning": true
},
"voice_profile": {
"summary": "1-2 sentence description of how the user talks",
"tone": ["direct", "warm", "curious"],
"do": ["..."],
"dont": ["..."],
"phrases": ["..."],
"sources": {
"articles": ["https://..."],
"linkedin_messages": ["https://..."],
"brand_guide": "text | pdf | docx | url | null"
},
"failed_sources": ["https://..."],
"generated_at": "ISO-8601 timestamp"
},
"linkedin_export": {
"requested_at": "ISO-8601 timestamp or null",
"messages_requested": true
},
"quick_start": {
"urls": ["https://www.linkedin.com/in/...", "..."],
"requested_at": "ISO-8601 timestamp",
"status": "queued"
}
}
PUT <backend_url>/api/me/config with {"config": <payload>}. Expect HTTP 200.
Step 8.2 โ Build the default-campaign payload
{
"offer": "verbatim one-sentence offer",
"offer_hook": "verbatim 1-2 sentence hook from Phase 3b",
"offer_profile": {
"summary": "...",
"key_benefits": ["...", "..."],
"objection_patterns": ["...", "..."],
"canonical_phrases": ["...", "..."],
"sources": ["https://...", "..."],
"failed_sources": ["https://..."],
"generated_at": "ISO-8601 timestamp"
},
"icp": {
"industry": "string",
"role": "string",
"company_size_or_stage": "string (optional)",
"geography": "string (optional)",
"other": "string (optional)"
},
"signals": {
"user_description": "verbatim user answer to step 5a",
"weighted": [
{ "name": "role-match", "description": "...", "weight": 0.33 },
{ "name": "post-relevance", "description": "...", "weight": 0.27 },
{ "name": "role-change", "description": "...", "weight": 0.20 },
{ "name": "industry-match", "description": "...", "weight": 0.13 },
{ "name": "active-poster", "description": "...", "weight": 0.07 }
]
}
}
PUT <backend_url>/api/me/campaigns/default/config with {"config": <payload>}. Expect HTTP 200.
If either call fails:
- HTTP 401 โ JWT is bad. Tell the user their session expired, ask them to re-run onboarding. Stop.
- HTTP 5xx or network error โ tell the user the backend's not responding, suggest they retry in a moment. Keep the answers in conversation context so a retry can re-PUT.
The tier field is set to "lite" for V1. Pro and Elite tiers will check this field at runtime to gate features when they ship.
Step 8.3 โ Lock the pipeline shape
Once both PUTs succeed, lock the pipeline so the seven defaults (plus any custom stages the user added in Phase 2.4 or via an ingest "adopt-stages" branch in Phase 2.5) stay structurally fixed from now on. Renames remain allowed any time; structural changes require an explicit /hhq:remap-pipeline.
POST <backend_url>/api/me/pipeline-stages/lock
Authorization: Bearer <jwt>
Expected response: {"pipeline_locked": true, "pipeline_locked_at": "<iso>"}. If the call fails (5xx / network), don't block onboarding โ log to skill memory as _pipeline_lock_failed: true and continue to the warm close. The user can re-run /hhq:remap-pipeline later, which will lock on completion.
If _pipeline_remapped is true in skill memory, append a one-line confirmation to the warm close: "Pipeline locked at the shape you mapped โ <N> stages."
Do NOT write any other local files. The only local files are <project-dir>/.hhq-session.json (per-project session auth) and <project-dir>/.hhq-campaign.json (per-project campaign pin). Everything else lives backend-side.
Then close warmly. Two close variants depending on whether Phase 7 captured quick-start URLs:
Variant A โ quick-start URLs were saved (config.quick_start.urls.length > 0):
"All set โ everything's saved. Your project session is at .hhq-session.json and pinned to your default campaign via .hhq-campaign.json. Both live in this project folder. Manage all your sessions across projects at https://helperhq.co/sessions.
Your LinkedIn export is in flight (usually a few hours, sometimes up to 24). And โ your <N> quick-start prospects are queued. Whenever you're ready, say 'let's go on quick start' and I'll research each, draft an opener, and have them ready to send in a few minutes.
When the LinkedIn email lands, open a fresh chat in this same project, drop the CSV in, and say 'I've got my LinkedIn export'.
Nice work on the offer and targeting โ that's the work that makes the messages I draft next actually land."
Variant B โ no quick-start URLs (config.quick_start is null):
"All set โ everything's saved. Your project session is at .hhq-session.json and pinned to your default campaign via .hhq-campaign.json. Both live in this project folder. Manage all your sessions across projects at https://helperhq.co/sessions.
Your LinkedIn export is in flight. When the email lands (usually a few hours, sometimes up to 24), open a fresh chat in this same project, drop the CSV in, and say 'I've got my LinkedIn export'. I'll take it from there.
Nice work on the offer and targeting โ that's the work that makes the messages I draft next actually land."
Things you must NOT do
- Do NOT write
config.json, contacts-master.csv, or any user-data file to local disk. All user state lives in the backend now. The only local files are <project-dir>/.hhq-session.json (per-project session auth) and <project-dir>/.hhq-campaign.json (per-project campaign pin).
- Do NOT save raw page contents, PDFs, DOCX text, or LinkedIn message bodies anywhere. Synthesis is the only persistent artifact โ distil and forget the source content.
- Do NOT save uploaded files to disk or to the backend. PDF/DOCX inputs in Phase 6a are read inline; the file itself is the user's responsibility to keep.
- Do NOT pressure for voice samples. Phase 6 is gravy โ if the user skips, save
voice_profile: null; they can build it later with tune-voice.
- Do NOT loop on Phase 6.6 review forever. After 5 edit rounds, gently nudge them to "save and refine later" rather than grinding.
- Do NOT ask network/source questions beyond LinkedIn โ V1 is LinkedIn-CSV only.
- Do NOT ask about paid sales tools, existing CRMs, or pipeline workflow.
- Do NOT ingest contacts. That's the
ingest-contacts skill.
- Do NOT promise weekly cadence โ V1 is on-demand only.
- Do NOT promise Pro or Elite features. The
tier field exists for forward compatibility, but V1 is Lite-only.
- Do NOT log the licence key, JWT, or any contents of
<project-dir>/.hhq-session.json in chat output. Those are secrets.