@elitedcs/ghl-mcp 3.53.2 → 3.55.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (27) hide show
  1. package/CHANGELOG.md +62 -32
  2. package/README.md +9 -7
  3. package/dist/index.js +1922 -684
  4. package/package.json +4 -3
  5. package/skills/blueprint/README.md +27 -0
  6. package/skills/blueprint/SKILL.md +149 -0
  7. package/skills/blueprint/examples/medspa-approval-view.md +92 -0
  8. package/skills/blueprint/examples/medspa-brief.json +52 -0
  9. package/skills/blueprint/examples/medspa-build-plan.json +265 -0
  10. package/skills/blueprint/examples/medspa-dry-run-report.md +67 -0
  11. package/skills/blueprint/examples/sample-approval-view.md +82 -0
  12. package/skills/blueprint/examples/sample-brief.json +13 -0
  13. package/skills/blueprint/examples/sample-build-plan.json +227 -0
  14. package/skills/blueprint/examples/validate-plan.cjs +127 -0
  15. package/skills/blueprint/presets/clinic-launch-a2p.md +39 -0
  16. package/skills/blueprint/presets/clinic-launch-a2p.preset.json +337 -0
  17. package/skills/blueprint/presets/generic-client.md +36 -0
  18. package/skills/blueprint/presets/generic-client.preset.json +255 -0
  19. package/skills/blueprint/presets/med-spa.md +59 -0
  20. package/skills/blueprint/presets/med-spa.preset.json +264 -0
  21. package/skills/blueprint/references/agency-os-detection.md +80 -0
  22. package/skills/blueprint/references/approval-view.md +83 -0
  23. package/skills/blueprint/references/brief-schema.md +45 -0
  24. package/skills/blueprint/references/build-plan-schema.md +52 -0
  25. package/skills/blueprint/references/external-funnel.md +310 -0
  26. package/skills/blueprint/references/intake-question-set.md +141 -0
  27. package/skills/blueprint/references/preset-format.md +130 -0
@@ -0,0 +1,255 @@
1
+ {
2
+ "presetId": "generic",
3
+ "presetVersion": "1.0.0",
4
+ "schemaVersion": "0.1",
5
+ "title": "Generic Client",
6
+ "description": "The neutral lead-to-customer fallback build: pipeline, core fields, tag system, booking calendar, intake form + landing page, a full email/SMS follow-up system (5 workflows), and the honest operator-handoff list. Industry-neutral skeleton; the brief supplies the names, copy, and toggles. Modeled on the showcase-video Modules 0-7 build. (Not the library default — med_spa is; this is the catch-all for clinic/coach/ecom/local-service/agency and any unmatched type.)",
7
+ "default": false,
8
+ "selectors": {
9
+ "businessTypes": ["Clinic / practice", "Coach / consultant", "Ecommerce", "Local service", "Agency", "Other"],
10
+ "aliases": ["generic", "clinic", "coach", "ecom", "agency", "local_service"]
11
+ },
12
+ "source": "ghl-command showcase-video Modules 0-7 + the proven Bliss Med Spa build",
13
+ "skeleton": {
14
+ "pipelines": [
15
+ {
16
+ "ref": "pipeline.main",
17
+ "name": "{{business.name}} Pipeline || Sales Pipeline",
18
+ "stages": {
19
+ "fillFrom": "goal.salesStages",
20
+ "transform": "stageList",
21
+ "default": [
22
+ { "ref": "stage.new_lead", "name": "New Lead", "position": 0 },
23
+ { "ref": "stage.contacted", "name": "Contacted", "position": 1 },
24
+ { "ref": "stage.qualified", "name": "Qualified", "position": 2 },
25
+ { "ref": "stage.appointment", "name": "Appointment Booked", "position": 3, "conditionalOn": "goal.bookingNeeded == true" },
26
+ { "ref": "stage.won", "name": "Won", "position": 4 },
27
+ { "ref": "stage.repeat", "name": "Repeat / Retention", "position": 5 },
28
+ { "ref": "stage.lost", "name": "Lost", "position": 6 }
29
+ ]
30
+ }
31
+ }
32
+ ],
33
+ "customFields": [
34
+ { "ref": "field.lead_source", "name": "Lead Source", "dataType": "SINGLE_OPTIONS", "model": "contact" },
35
+ { "ref": "field.interest", "name": "Service / Product Interest", "dataType": "MULTIPLE_OPTIONS", "model": "contact" },
36
+ { "ref": "field.appt_date", "name": "Appointment Date", "dataType": "DATE", "model": "contact", "conditionalOn": "goal.bookingNeeded == true" },
37
+ { "ref": "field.deposit_paid", "name": "Deposit Paid", "dataType": "CHECKBOX", "model": "contact", "conditionalOn": "goal.bookingNeeded == true" },
38
+ { "ref": "field.deal_sold", "name": "Deal Sold", "dataType": "TEXT", "model": "contact" },
39
+ { "ref": "field.ltv", "name": "Estimated Lifetime Value", "dataType": "MONETORY", "model": "contact" }
40
+ ],
41
+ "tags": [
42
+ { "ref": "tag.source_website", "name": "source-website" },
43
+ { "ref": "tag.source_instagram", "name": "source-instagram" },
44
+ { "ref": "tag.source_facebook", "name": "source-facebook" },
45
+ { "ref": "tag.source_google", "name": "source-google" },
46
+ { "ref": "tag.source_referral", "name": "source-referral" },
47
+ { "ref": "tag.source_other", "name": "source-other" },
48
+ { "ref": "tag.lc_new_lead", "name": "lifecycle-new-lead" },
49
+ { "ref": "tag.lc_contacted", "name": "lifecycle-contacted" },
50
+ { "ref": "tag.lc_qualified", "name": "lifecycle-qualified" },
51
+ { "ref": "tag.lc_appointment", "name": "lifecycle-booked", "conditionalOn": "goal.bookingNeeded == true" },
52
+ { "ref": "tag.lc_won", "name": "lifecycle-won" },
53
+ { "ref": "tag.lc_repeat", "name": "lifecycle-repeat" },
54
+ { "ref": "tag.lc_lapsed", "name": "lifecycle-lapsed" },
55
+ { "ref": "tag.lc_lost", "name": "lifecycle-lost" }
56
+ ],
57
+ "_tagNote": "The skill ALSO derives interest-<x> tags from the offer/business type at fill time (e.g. interest-injectables for a med spa) and adds them to this list, mirroring the field.interest options. Declared here so they are reviewable; generated because they are business-specific.",
58
+ "customValues": [
59
+ { "ref": "cv.booking_link", "name": "Booking Link", "value": "", "filledBy": "handoff.calendar_oauth", "conditionalOn": "goal.bookingNeeded == true" }
60
+ ],
61
+ "calendars": [
62
+ {
63
+ "ref": "calendar.consult",
64
+ "conditionalOn": "goal.bookingNeeded == true",
65
+ "name": "{{business.name}} Consultation || Consultation",
66
+ "calendarType": "round_robin",
67
+ "openHours": [{ "daysOfTheWeek": [1, 2, 3, 4, 5], "hours": [{ "openHour": 9, "openMinute": 0, "closeHour": 17, "closeMinute": 0 }] }],
68
+ "availabilityType": 0,
69
+ "slotDuration": 30,
70
+ "slotBuffer": 10,
71
+ "requiresStaff": true,
72
+ "notes": "Default Mon-Fri 9-5; operator edits hours after build. round_robin needs at least one staff user (handoff.add_staff)."
73
+ }
74
+ ],
75
+ "forms": [
76
+ {
77
+ "ref": "form.intake",
78
+ "name": "{{business.name}} Inquiry || New Inquiry",
79
+ "fields": [
80
+ { "type": "standard", "key": "first_name", "required": true },
81
+ { "type": "standard", "key": "last_name", "required": false },
82
+ { "type": "standard", "key": "email", "required": true },
83
+ { "type": "standard", "key": "phone", "required": false },
84
+ { "type": "custom", "fieldRef": "field.interest", "required": false }
85
+ ]
86
+ }
87
+ ],
88
+ "funnels": [
89
+ {
90
+ "ref": "funnel.main",
91
+ "name": "{{business.name}} Funnel || Lead Funnel",
92
+ "pages": [
93
+ { "ref": "page.landing", "name": "Landing", "role": "optin", "outline": "Hero with the core offer in the customer's language; 3 proof/benefit points; single CTA to form.intake. Tone matched to {{business.type}}.", "formRef": "form.intake" },
94
+ { "ref": "page.thankyou", "name": "Thank You", "role": "confirmation", "outline": "Confirm we received them; set the next-step expectation; booking CTA.", "calendarRef": "calendar.consult" }
95
+ ]
96
+ }
97
+ ],
98
+ "emails": [
99
+ { "ref": "email.welcome_1", "name": "Welcome / First Touch", "subject": "Thanks for reaching out to {{business.name}}", "copyDirection": "Speed-to-lead first touch. Thank them; restate the core offer in their language; one clear next step (book / reply); warm, concrete, no hype.", "mergeTags": ["contact.first_name"] },
100
+ { "ref": "email.nurture_1", "name": "Nurture 1 — Value", "subject": "The thing most people get wrong about {{audience.ideal}}", "copyDirection": "Lead with one of audience.painPoints; teach one useful idea; soft CTA. Establish authority, do not pitch hard.", "mergeTags": ["contact.first_name"] },
101
+ { "ref": "email.nurture_2", "name": "Nurture 2 — Proof", "subject": "How this actually works", "copyDirection": "Walk through the offer/process; address the top objection from audience.objections; CTA to the next step.", "mergeTags": ["contact.first_name"] },
102
+ { "ref": "email.nurture_3", "name": "Nurture 3 — Last Call", "subject": "Did this slip?", "copyDirection": "Short re-engagement; restate the single benefit; final CTA; sign off and let them go.", "mergeTags": ["contact.first_name"] },
103
+ { "ref": "email.no_show_1", "name": "No-Show Re-offer", "subject": "Missed you — let's get you rebooked", "conditionalOn": "goal.bookingNeeded == true", "copyDirection": "Warm, no guilt; offer to rebook; booking CTA via cv.booking_link.", "mergeTags": ["contact.first_name"] },
104
+ { "ref": "email.onboarding_1", "name": "Onboarding / Pre-visit", "subject": "You're booked with {{business.name}}", "conditionalOn": "goal.bookingNeeded == true", "copyDirection": "Confirm the appointment; what to expect / how to prepare; logistics; reassurance.", "mergeTags": ["contact.first_name"] },
105
+ { "ref": "email.winback_1", "name": "Win-back", "subject": "It's been a while", "copyDirection": "Reconnect with lapsed contacts; lead with a reason to return tied to the offer; CTA.", "mergeTags": ["contact.first_name"] }
106
+ ],
107
+ "sms": [
108
+ { "ref": "sms.speed_to_lead_1", "name": "Speed-to-lead text", "conditionalOn": "channels.sms == true", "copyDirection": "Instant first text after a new lead. Name + business; one line; one CTA; include opt-out (Reply STOP).", "mergeTags": ["contact.first_name"] },
109
+ { "ref": "sms.nurture_1", "name": "Nurture text 1", "conditionalOn": "channels.sms == true", "copyDirection": "Short nudge a couple days in; one benefit; CTA; opt-out line.", "mergeTags": ["contact.first_name"] },
110
+ { "ref": "sms.no_show_1", "name": "No-show text", "conditionalOn": "channels.sms == true && goal.bookingNeeded == true", "copyDirection": "Quick rebook nudge after a no-show; booking link; opt-out line.", "mergeTags": ["contact.first_name"] },
111
+ { "ref": "sms.winback_1", "name": "Win-back text", "conditionalOn": "channels.sms == true", "copyDirection": "Friendly reconnect text for lapsed contacts; one reason to return; opt-out line.", "mergeTags": ["contact.first_name"] }
112
+ ],
113
+ "workflows": [
114
+ {
115
+ "ref": "workflow.speed_to_lead",
116
+ "name": "Speed to Lead",
117
+ "trigger": { "type": "form_submission", "formRef": "form.intake" },
118
+ "stopOnResponse": true,
119
+ "actions": [
120
+ { "type": "add_contact_tag", "tagRef": "tag.lc_new_lead" },
121
+ { "type": "create_opportunity", "pipelineRef": "pipeline.main", "stageRef": "stage.new_lead", "name": "{{contact.name}} - New Lead" },
122
+ { "type": "send_email", "emailRef": "email.welcome_1" },
123
+ { "type": "internal_notification", "to": "{{operator.userId}}", "title": "New lead", "body": "New inquiry from {{contact.first_name}}" },
124
+ { "type": "wait", "value": 5, "unit": "minutes" },
125
+ { "type": "send_sms", "smsRef": "sms.speed_to_lead_1", "conditionalOn": "channels.sms == true" }
126
+ ]
127
+ },
128
+ {
129
+ "ref": "workflow.lead_nurture",
130
+ "name": "Lead Nurture",
131
+ "trigger": { "type": "contact_tag", "tagRef": "tag.lc_new_lead" },
132
+ "stopOnResponse": true,
133
+ "actions": [
134
+ { "type": "wait", "value": 1, "unit": "days" },
135
+ { "type": "send_email", "emailRef": "email.nurture_1" },
136
+ { "type": "wait", "value": 2, "unit": "days" },
137
+ { "type": "send_sms", "smsRef": "sms.nurture_1", "conditionalOn": "channels.sms == true" },
138
+ { "type": "wait", "value": 2, "unit": "days" },
139
+ { "type": "send_email", "emailRef": "email.nurture_2" },
140
+ { "type": "wait", "value": 3, "unit": "days" },
141
+ { "type": "send_email", "emailRef": "email.nurture_3" },
142
+ { "type": "add_contact_tag", "tagRef": "tag.lc_lapsed" }
143
+ ]
144
+ },
145
+ {
146
+ "ref": "workflow.no_show",
147
+ "name": "No-Show / Reschedule",
148
+ "conditionalOn": "goal.bookingNeeded == true",
149
+ "trigger": { "type": "appointment", "appointmentStatus": "noshow", "calendarRef": "calendar.consult" },
150
+ "stopOnResponse": true,
151
+ "actions": [
152
+ { "type": "add_contact_tag", "tagRef": "tag.lc_contacted" },
153
+ { "type": "internal_notification", "to": "{{operator.userId}}", "title": "No-show", "body": "{{contact.first_name}} missed their appointment" },
154
+ { "type": "send_email", "emailRef": "email.no_show_1" },
155
+ { "type": "send_sms", "smsRef": "sms.no_show_1", "conditionalOn": "channels.sms == true" }
156
+ ]
157
+ },
158
+ {
159
+ "ref": "workflow.onboarding",
160
+ "name": "New Patient / Client Onboarding",
161
+ "conditionalOn": "goal.bookingNeeded == true",
162
+ "trigger": { "type": "appointment", "appointmentStatus": "confirmed", "calendarRef": "calendar.consult" },
163
+ "stopOnResponse": false,
164
+ "actions": [
165
+ { "type": "remove_from_workflow", "workflowRef": "workflow.lead_nurture" },
166
+ { "type": "add_contact_tag", "tagRef": "tag.lc_appointment" },
167
+ { "type": "send_email", "emailRef": "email.onboarding_1" }
168
+ ]
169
+ },
170
+ {
171
+ "ref": "workflow.win_back",
172
+ "name": "Win-back",
173
+ "trigger": { "type": "contact_tag", "tagRef": "tag.lc_lapsed" },
174
+ "stopOnResponse": true,
175
+ "actions": [
176
+ { "type": "send_email", "emailRef": "email.winback_1" },
177
+ { "type": "wait", "value": 3, "unit": "days" },
178
+ { "type": "send_sms", "smsRef": "sms.winback_1", "conditionalOn": "channels.sms == true" },
179
+ { "type": "wait", "value": 7, "unit": "days" },
180
+ { "type": "add_contact_tag", "tagRef": "tag.lc_lost" }
181
+ ]
182
+ },
183
+ {
184
+ "ref": "workflow.reply_route",
185
+ "name": "Replied — Stop & Route",
186
+ "trigger": { "type": "customer_reply" },
187
+ "stopOnResponse": false,
188
+ "actions": [
189
+ { "type": "remove_from_workflow", "workflowRef": "workflow.lead_nurture" },
190
+ { "type": "remove_from_workflow", "workflowRef": "workflow.win_back" },
191
+ { "type": "add_contact_tag", "tagRef": "tag.lc_contacted" },
192
+ { "type": "internal_notification", "to": "{{operator.userId}}", "title": "Lead replied", "body": "{{contact.first_name}} replied — follow up" }
193
+ ]
194
+ }
195
+ ],
196
+ "handoffs": [
197
+ {
198
+ "ref": "handoff.add_staff",
199
+ "conditionalOn": "goal.bookingNeeded == true",
200
+ "owner": "OPERATOR-UI",
201
+ "title": "Add yourself (or a provider) as a team member",
202
+ "trigger": "goal.bookingNeeded == true",
203
+ "instruction": "In the sub-account, go to Settings > My Staff and add at least one user. The booking calendar needs someone to assign appointments to, and GHL Command cannot add users.",
204
+ "produces": null,
205
+ "successCheck": "At least one staff user exists in the account",
206
+ "blocks": ["calendar.consult"]
207
+ },
208
+ {
209
+ "ref": "handoff.calendar_oauth",
210
+ "conditionalOn": "flags includes calendar_oauth_needed",
211
+ "owner": "OPERATOR-UI",
212
+ "title": "Connect your calendar (Google / Outlook)",
213
+ "trigger": "goal.bookingNeeded == true && channels.calendarConnected == false",
214
+ "instruction": "In the sub-account, open the consultation calendar > Connections and authorize your Google or Outlook account. This is a login only you can approve. Then copy the booking link.",
215
+ "produces": "cv.booking_link",
216
+ "successCheck": "Calendar shows a connected external account; booking link resolves",
217
+ "blocks": ["cv.booking_link"]
218
+ },
219
+ {
220
+ "ref": "handoff.a2p",
221
+ "conditionalOn": "flags includes needs_a2p",
222
+ "owner": "OPERATOR-EXT",
223
+ "title": "Register A2P (so texts can send)",
224
+ "trigger": "channels.sms == true",
225
+ "instruction": "Submit your A2P brand and campaign with the carrier (via the GHL Trust Center). SMS steps are built but will NOT send until this is approved. Save your submission package before submitting.",
226
+ "produces": null,
227
+ "successCheck": "A2P campaign status shows submitted/approved",
228
+ "blocks": ["workflow.speed_to_lead", "workflow.lead_nurture", "workflow.no_show", "workflow.win_back", "sms.speed_to_lead_1", "sms.nurture_1", "sms.no_show_1", "sms.winback_1"],
229
+ "_blocksNote": "Lists the SMS-bearing WORKFLOW refs (so they stay DRAFT) plus the sms.* asset refs — only an explicit workflow.* ref keeps a workflow DRAFT. After conditional pruning, drop any entry whose workflow/asset was removed."
230
+ },
231
+ {
232
+ "ref": "handoff.stripe",
233
+ "conditionalOn": "flags includes stripe_not_connected",
234
+ "owner": "OPERATOR-EXT",
235
+ "title": "Connect Stripe (to take payment)",
236
+ "trigger": "channels.payment != 'Stripe connected'",
237
+ "instruction": "Connect Stripe in Payments > Integrations and create your products/prices. The landing page is built; the money connection is yours to authorize.",
238
+ "produces": null,
239
+ "successCheck": "Stripe shows connected; products exist",
240
+ "blocks": []
241
+ },
242
+ {
243
+ "ref": "handoff.email_domain",
244
+ "conditionalOn": "flags includes email_domain_needed",
245
+ "owner": "OPERATOR-UI",
246
+ "title": "Set up your sending email",
247
+ "trigger": "channels.email == false",
248
+ "instruction": "Connect a sending domain / mailbox in Settings > Email Services so the welcome and nurture emails send from your address instead of a default.",
249
+ "produces": null,
250
+ "successCheck": "A verified sending domain / from-address exists",
251
+ "blocks": ["email.welcome_1", "email.nurture_1", "email.nurture_2", "email.nurture_3", "email.onboarding_1", "email.winback_1"]
252
+ }
253
+ ]
254
+ }
255
+ }
@@ -0,0 +1,59 @@
1
+ # Preset: Med Spa (default)
2
+
3
+ **File:** `med-spa.preset.json` · **id:** `med_spa` · **version:** 1.0.0 · **default:** yes
4
+ **Source:** ghl-command showcase Modules 0-7 + the proven Bliss Med Spa / Lux Bio Therapy builds.
5
+ **Status:** Live-validated 2026-06-25 — `validate_build_plan` valid (48 refs, 0 dead) and
6
+ `apply_build_plan` dry_run clean (40 objects, 28 actions auto-built, 0 need-content,
7
+ 0 manual action steps, every trigger auto-built). See `examples/medspa-dry-run-report.md`.
8
+
9
+ ## When it is selected
10
+ The default reference preset, and the routing target for med-spa / aesthetics business
11
+ types (`selectors.businessTypes`) and the `med_spa` / `medspa` / `aesthetics` aliases. It
12
+ is also the library default: a brief whose `business_type` matches no preset resolves here.
13
+ (The med-spa business type and `med_spa` alias moved here from the generic preset, which is
14
+ now the neutral, non-default fallback for clinics/coaches/ecom/etc.)
15
+
16
+ ## What it builds (skeleton)
17
+ - **1 pipeline** — "Patient Journey", default 7 stages (New Lead → Contacted → Consult Booked* → Showed* → Treated → Member / Rebooked → Lost). Overridden by `goal.salesStages` when supplied.
18
+ - **5 custom fields** — Lead Source (dropdown), Service Interest (multi-select, options mirrored by the interest-* tags), Appointment Date*, Consult Deposit Paid* (checkbox), Estimated Lifetime Value (`MONETORY`).
19
+ - **13 tags + derived interest tags** — 5 lead-source + 8 lifecycle; the skill also derives `interest-<x>` tags (injectables / filler / facials / membership) from the offer and mirrors them onto the Service Interest field options.
20
+ - **1 custom value** — Booking Link (filled by the calendar-connect handoff).*
21
+ - **1 calendar** — round-robin Consultation, Mon-Fri 9-5 default. Auto-assigned when the operator is the sole user; otherwise the add-staff handoff applies.*
22
+ - **1 intake form** — first/last/email/phone (phone required) + Service Interest (mapped to the custom field).
23
+ - **1 funnel** — Free Assessment opt-in (→ form, with the A2P-compliant consent block) + Thank You (booking CTA).* Page content filled from `funnel-page-content-template.md`.
24
+ - **5 emails + 3 SMS** — welcome, education nurture, consult reminder*, no-show*, win-back; speed-to-lead / consult-reminder / no-show texts (SMS only when `channels.sms`).
25
+ - **5 workflows** — Speed-to-Lead (form_submission), Lead Nurture (contact_tag), Replied → Stop & Route (customer_reply), New Patient Onboarding* (appointment confirmed; appointment-relative reminder via `wait_appointment`), No-Show Win-back* (appointment noshow; `find_opportunity` branch → found: move opp back + text; notFound: win-back email).
26
+ - **Handoffs (conditional)** — add-staff* (team only), calendar-connect, A2P, Stripe, email-domain.
27
+
28
+ \* conditional on `goal.bookingNeeded == true`.
29
+
30
+ ## What this preset gets right (the contract corrections)
31
+ - **Canonical trigger tokens only.** Every trigger uses a type the executor builds natively
32
+ (`form_submission`, `contact_tag`, `appointment` + `appointmentStatus`, `customer_reply`),
33
+ so triggers auto-build instead of falling through to a manual GHL-UI step. (The older
34
+ generic preset used `tag_added` / `appointment_status` / `appointment_booked`, which are
35
+ NOT recognized and surface as manual steps.)
36
+ - **A2P gates the workflows, not just the assets.** `handoff.a2p.blocks` lists each
37
+ SMS-bearing **workflow** ref (Speed-to-Lead, Onboarding, No-Show) so they stay DRAFT until
38
+ A2P is approved — a bare `sms.*` wildcard gates only the asset surface, not the workflow's
39
+ publish state.
40
+ - **Publishable internal notifications.** `internal_notification.to` uses the
41
+ `{{operator.userId}}` token, which the skill resolves to a real GHL user id from
42
+ `get_users` at fill time (a non-user value builds a notify-all that GHL will not publish).
43
+ - **Customer-safe handoff owners.** `OPERATOR-UI` / `OPERATOR-EXT` / `TEAM` — never internal
44
+ names.
45
+
46
+ ## How the brief shapes it
47
+ | Brief signal | Effect |
48
+ |---|---|
49
+ | `goal.salesStages` present | replaces the default pipeline stages |
50
+ | `goal.bookingNeeded == false` | drops the calendar, appointment field/tag/stage, onboarding + no-show workflows, and the add-staff/connect handoffs |
51
+ | `channels.sms == true` | keeps SMS assets + the send_sms actions + the A2P handoff/gating; otherwise they drop |
52
+ | `flags: needs_a2p` | adds the A2P handoff and holds the SMS-bearing workflows DRAFT |
53
+ | `flags: stripe_not_connected` / `calendar_oauth_needed` / `email_domain_needed` | adds the matching handoff |
54
+ | `business.teamSize != "Just me"` | adds the add-staff handoff (a team needs an assigned provider) |
55
+ | `business.name`, `offer.*`, `audience.*`, `extended.*` | fill object names + ground all copy |
56
+
57
+ ## Worked example
58
+ `examples/medspa-brief.json` → `examples/medspa-build-plan.json` →
59
+ `examples/medspa-approval-view.md`, proven by `examples/medspa-dry-run-report.md`.
@@ -0,0 +1,264 @@
1
+ {
2
+ "presetId": "med_spa",
3
+ "presetVersion": "1.0.0",
4
+ "schemaVersion": "0.1",
5
+ "title": "Med Spa",
6
+ "description": "The default GHL Command Blueprint reference preset. A med-spa lead-to-member system: a Patient Journey pipeline, lead-source/service-interest/appointment fields, source + lifecycle + interest tags, a round-robin consult calendar, an A2P-compliant intake form + funnel, a full email/SMS follow-up library, and five workflows (speed-to-lead, nurture, reply-route, confirmed-appointment onboarding with an appointment-relative reminder, and a no-show win-back that branches on whether an opportunity exists). SMS-bearing workflows stay DRAFT behind the A2P handoff. Industry skeleton is fixed; the brief supplies names, copy, stages, and toggles. Live-validated 2026-06-25 (validate_build_plan valid + apply_build_plan dry_run clean).",
7
+ "default": true,
8
+ "selectors": {
9
+ "businessTypes": ["Med spa", "Medical spa", "Medical aesthetics", "Aesthetics", "Med Spa / Aesthetics"],
10
+ "aliases": ["med_spa", "medspa", "medical_spa", "aesthetics", "injectables"]
11
+ },
12
+ "source": "ghl-command showcase Modules 0-7 + the proven Bliss Med Spa / Lux Bio Therapy builds; canonical trigger + handoff-owner + A2P-gating contract live-validated 2026-06-25 on MCP Testing.",
13
+ "conventions": {
14
+ "triggerTokens": "Use ONLY the executor's native trigger types so triggers auto-build (never surface as a manual step): contact_tag (needs tagRef), form_submission (needs formRef), appointment (needs appointmentStatus: confirmed|noshow|...; calendarRef optional), customer_reply, pipeline_stage_updated (needs pipelineRef+stageRef), inbound_webhook, payment_received. Do NOT use tag_added / form_submitted / appointment_status / appointment_booked / contact_replied — form_submitted and contact_replied are accepted aliases but the canonical tokens are preferred; the others are NOT recognized and fall through to a manual step.",
15
+ "publishDefault": "Workflows build DRAFT. The skill PROMPTS the operator ('publish now or leave DRAFT for review?') and only passes publishWorkflows:true on an explicit yes. Workflows gated by an unmet handoff never auto-publish even when opted in.",
16
+ "internalNotification": "internal_notification.to must be a REAL GHL user id (a non-user value builds selectedUser:'' = notify-all, which GHL will not publish). The token {{operator.userId}} is resolved by the skill from get_users on the current location at fill time. If no single user can be resolved, the skill leaves it for the operator (handoff) rather than shipping an unpublishable notify-all.",
17
+ "a2pGating": "handoff.a2p.blocks MUST list each SMS-bearing WORKFLOW ref (not just sms.* asset refs) — only an explicit workflow.* (or workflow.* wildcard) ref keeps a workflow DRAFT; a bare sms.* wildcard gates the asset surface only, not the workflow's publish state. After conditional pruning, prune any blocks entry whose workflow/asset was dropped.",
18
+ "copyExpansion": "emails/sms carry copyDirection for review. Before apply_build_plan execute, the skill expands each SENT email/sms to a full `body` (a send_email/send_sms action whose asset has only an outline reports needsContent and will not build). See the worked example for the expanded form."
19
+ },
20
+ "skeleton": {
21
+ "pipelines": [
22
+ {
23
+ "ref": "pipeline.patient_journey",
24
+ "name": "{{business.name}} Patient Journey || Patient Journey",
25
+ "stages": {
26
+ "fillFrom": "goal.salesStages",
27
+ "transform": "stageList",
28
+ "default": [
29
+ { "ref": "stage.new_lead", "name": "New Lead", "position": 0 },
30
+ { "ref": "stage.contacted", "name": "Contacted", "position": 1 },
31
+ { "ref": "stage.consult_booked", "name": "Consult Booked", "position": 2, "conditionalOn": "goal.bookingNeeded == true" },
32
+ { "ref": "stage.showed", "name": "Showed", "position": 3, "conditionalOn": "goal.bookingNeeded == true" },
33
+ { "ref": "stage.treated", "name": "Treated", "position": 4 },
34
+ { "ref": "stage.member", "name": "Member / Rebooked", "position": 5 },
35
+ { "ref": "stage.lost", "name": "Lost", "position": 6 }
36
+ ]
37
+ }
38
+ }
39
+ ],
40
+ "customFields": [
41
+ { "ref": "field.lead_source", "name": "Lead Source", "dataType": "SINGLE_OPTIONS", "model": "contact", "options": ["Website", "Instagram", "Facebook", "Google", "Referral", "Walk-in", "Other"] },
42
+ { "ref": "field.service_interest", "name": "Service Interest", "dataType": "MULTIPLE_OPTIONS", "model": "contact", "options": ["Injectables / Tox", "Dermal Filler", "Facials / Skin", "Membership", "Weight / Body"], "_optionsNote": "The skill derives these from offer.summary/business; mirror them with the interest-* tags below." },
43
+ { "ref": "field.appointment_date", "name": "Appointment Date", "dataType": "DATE", "model": "contact", "conditionalOn": "goal.bookingNeeded == true" },
44
+ { "ref": "field.consult_deposit_paid", "name": "Consult Deposit Paid", "dataType": "CHECKBOX", "model": "contact", "options": ["Deposit paid"], "conditionalOn": "goal.bookingNeeded == true" },
45
+ { "ref": "field.lifetime_value", "name": "Estimated Lifetime Value", "dataType": "MONETORY", "model": "contact" }
46
+ ],
47
+ "tags": [
48
+ { "ref": "tag.source_website", "name": "source-website" },
49
+ { "ref": "tag.source_instagram", "name": "source-instagram" },
50
+ { "ref": "tag.source_facebook", "name": "source-facebook" },
51
+ { "ref": "tag.source_google", "name": "source-google" },
52
+ { "ref": "tag.source_referral", "name": "source-referral" },
53
+ { "ref": "tag.lc_new_lead", "name": "lifecycle-new-lead" },
54
+ { "ref": "tag.lc_contacted", "name": "lifecycle-contacted" },
55
+ { "ref": "tag.lc_consult_booked", "name": "lifecycle-consult-booked", "conditionalOn": "goal.bookingNeeded == true" },
56
+ { "ref": "tag.lc_showed", "name": "lifecycle-showed", "conditionalOn": "goal.bookingNeeded == true" },
57
+ { "ref": "tag.lc_treated", "name": "lifecycle-treated" },
58
+ { "ref": "tag.lc_member", "name": "lifecycle-member" },
59
+ { "ref": "tag.lc_lapsed", "name": "lifecycle-lapsed" },
60
+ { "ref": "tag.lc_lost", "name": "lifecycle-lost" }
61
+ ],
62
+ "_tagNote": "The skill ALSO derives interest-<x> tags from offer/business type at fill time (e.g. interest-injectables, interest-filler, interest-facials, interest-membership for a med spa) and adds them here, mirroring the field.service_interest options. Declared by the derive step because they are business-specific.",
63
+ "customValues": [
64
+ { "ref": "cv.booking_link", "name": "Booking Link", "value": "", "filledBy": "handoff.calendar_oauth", "conditionalOn": "goal.bookingNeeded == true" }
65
+ ],
66
+ "calendars": [
67
+ {
68
+ "ref": "calendar.consult",
69
+ "conditionalOn": "goal.bookingNeeded == true",
70
+ "name": "{{business.name}} Consultation || Consultation",
71
+ "calendarType": "round_robin",
72
+ "openHours": [{ "daysOfTheWeek": [1, 2, 3, 4, 5], "hours": [{ "openHour": 9, "openMinute": 0, "closeHour": 17, "closeMinute": 0 }] }],
73
+ "availabilityType": 0,
74
+ "requiresStaff": true,
75
+ "notes": "Default Mon-Fri 9-5 from business.hours; operator edits after build. round_robin auto-builds only when the account has exactly one user (solo operator auto-assigned); with 2+ users it surfaces as a manual step and handoff.add_staff applies."
76
+ }
77
+ ],
78
+ "forms": [
79
+ {
80
+ "ref": "form.intake",
81
+ "name": "{{business.name}} New Client Inquiry || New Client Inquiry",
82
+ "fields": [
83
+ { "type": "standard", "key": "first_name", "required": true },
84
+ { "type": "standard", "key": "last_name", "required": false },
85
+ { "type": "standard", "key": "email", "required": true },
86
+ { "type": "standard", "key": "phone", "required": true },
87
+ { "type": "custom", "fieldRef": "field.service_interest", "required": false }
88
+ ]
89
+ }
90
+ ],
91
+ "funnels": [
92
+ {
93
+ "ref": "funnel.main",
94
+ "name": "{{business.name}} Lead Funnel || Lead Funnel",
95
+ "target": "ghl",
96
+ "pages": [
97
+ {
98
+ "ref": "page.optin",
99
+ "name": "{{offer.leadMagnet}} || Free Assessment",
100
+ "role": "optin",
101
+ "formRef": "form.intake",
102
+ "copyDirection": "OPTIN page per templates/funnel-page-content-template.md: Hero (one H1 = the offer promise) + GEO quotable summary (who/what/where/next step) + 3-5 outcome-led benefits grounded in offer + audience + (extended ICA voice when present) + social proof only if real + the A2P-compliant capture form (form.intake) with a NOT-pre-checked SMS consent checkbox carrying the four disclosures, visible business legal name + mailing address, and Privacy/Terms links at the point of capture + a 4-8 Q FAQ with FAQPage JSON-LD + NAP/legal footer. Head: title<60, meta<155, canonical, LocalBusiness + Service + FAQPage JSON-LD. A2P block is MANDATORY."
103
+ },
104
+ {
105
+ "ref": "page.thankyou",
106
+ "name": "Thank You",
107
+ "role": "confirmation",
108
+ "calendarRef": "calendar.consult",
109
+ "conditionalOn": "goal.bookingNeeded == true",
110
+ "copyDirection": "CONFIRMATION page: confirm receipt, set the next-step expectation, embed the booking widget via cv.booking_link / calendar.consult so they self-schedule now. Keep NAP + Privacy/Terms in the footer. No new opt-in."
111
+ }
112
+ ]
113
+ }
114
+ ],
115
+ "emails": [
116
+ { "ref": "email.welcome_1", "name": "Welcome / First Touch", "subject": "Your {{offer.leadMagnet || free assessment}} with {{business.name}}", "copyDirection": "Speed-to-lead first touch. Thank them; restate the core offer in their language (offer.summary); one clear next step (book via cv.booking_link / reply); warm, concrete, no hype, no em-dashes.", "mergeTags": ["contact.first_name"] },
117
+ { "ref": "email.nurture_1", "name": "Nurture / Education", "subject": "The difference between refreshed and overdone", "copyDirection": "Teach one useful idea tied to audience.painPoints (e.g. natural results are technique + restraint); establish authority; soft CTA to the assessment. Do not pitch hard.", "mergeTags": ["contact.first_name"] },
118
+ { "ref": "email.consult_reminder", "name": "Pre-Consult Reminder", "subject": "Your {{business.name}} consult is coming up", "conditionalOn": "goal.bookingNeeded == true", "copyDirection": "Confirm the upcoming consult; how to prepare; logistics; reassurance; reschedule via cv.booking_link.", "mergeTags": ["contact.first_name"] },
119
+ { "ref": "email.no_show", "name": "No-Show Re-offer", "subject": "We missed you, let us get you rebooked", "conditionalOn": "goal.bookingNeeded == true", "copyDirection": "Warm, no guilt; the assessment is still open; rebook CTA via cv.booking_link.", "mergeTags": ["contact.first_name"] },
120
+ { "ref": "email.winback", "name": "Win-back", "subject": "It has been a while", "copyDirection": "Reconnect with lapsed contacts; one reason to return tied to the offer/season; CTA.", "mergeTags": ["contact.first_name"] }
121
+ ],
122
+ "sms": [
123
+ { "ref": "sms.speed_to_lead", "name": "Speed-to-lead text", "conditionalOn": "channels.sms == true", "copyDirection": "Instant first text after a new lead. Name + business; one line; one CTA to hold a consult time; include opt-out (Reply STOP).", "mergeTags": ["contact.first_name"] },
124
+ { "ref": "sms.consult_reminder", "name": "Consult reminder text", "conditionalOn": "channels.sms == true && goal.bookingNeeded == true", "copyDirection": "Reminder ~1 day before the consult; offer to reschedule; opt-out line.", "mergeTags": ["contact.first_name"] },
125
+ { "ref": "sms.no_show", "name": "No-show rebook text", "conditionalOn": "channels.sms == true && goal.bookingNeeded == true", "copyDirection": "Quick rebook nudge after a no-show; the assessment is still open; opt-out line.", "mergeTags": ["contact.first_name"] }
126
+ ],
127
+ "workflows": [
128
+ {
129
+ "ref": "workflow.speed_to_lead",
130
+ "name": "Speed to Lead",
131
+ "trigger": { "type": "form_submission", "formRef": "form.intake" },
132
+ "stopOnResponse": true,
133
+ "actions": [
134
+ { "type": "add_contact_tag", "tagRef": "tag.lc_new_lead" },
135
+ { "type": "create_opportunity", "pipelineRef": "pipeline.patient_journey", "stageRef": "stage.new_lead", "name": "{{contact.name}} - New Lead" },
136
+ { "type": "send_email", "emailRef": "email.welcome_1" },
137
+ { "type": "internal_notification", "to": "{{operator.userId}}", "title": "New lead", "body": "New inquiry from {{contact.first_name}} {{contact.last_name}}" },
138
+ { "type": "wait", "value": 5, "unit": "minutes" },
139
+ { "type": "send_sms", "smsRef": "sms.speed_to_lead", "conditionalOn": "channels.sms == true" }
140
+ ]
141
+ },
142
+ {
143
+ "ref": "workflow.lead_nurture",
144
+ "name": "Lead Nurture",
145
+ "trigger": { "type": "contact_tag", "tagRef": "tag.lc_new_lead" },
146
+ "stopOnResponse": true,
147
+ "actions": [
148
+ { "type": "wait", "value": 1, "unit": "days" },
149
+ { "type": "send_email", "emailRef": "email.nurture_1" },
150
+ { "type": "wait", "value": 3, "unit": "days" },
151
+ { "type": "add_contact_tag", "tagRef": "tag.lc_lapsed" }
152
+ ]
153
+ },
154
+ {
155
+ "ref": "workflow.reply_route",
156
+ "name": "Replied - Stop and Route",
157
+ "trigger": { "type": "customer_reply" },
158
+ "stopOnResponse": false,
159
+ "actions": [
160
+ { "type": "remove_from_workflow", "workflowRef": "workflow.lead_nurture" },
161
+ { "type": "add_contact_tag", "tagRef": "tag.lc_contacted" },
162
+ { "type": "internal_notification", "to": "{{operator.userId}}", "title": "Lead replied", "body": "{{contact.first_name}} replied, follow up" }
163
+ ]
164
+ },
165
+ {
166
+ "ref": "workflow.onboarding",
167
+ "name": "New Patient Onboarding",
168
+ "conditionalOn": "goal.bookingNeeded == true",
169
+ "trigger": { "type": "appointment", "appointmentStatus": "confirmed", "calendarRef": "calendar.consult" },
170
+ "stopOnResponse": false,
171
+ "actions": [
172
+ { "type": "remove_from_workflow", "workflowRef": "workflow.lead_nurture" },
173
+ { "type": "add_contact_tag", "tagRef": "tag.lc_consult_booked" },
174
+ { "type": "update_opportunity", "pipelineRef": "pipeline.patient_journey", "stageRef": "stage.consult_booked" },
175
+ { "type": "send_email", "emailRef": "email.consult_reminder" },
176
+ { "type": "wait_appointment", "value": 1, "unit": "days" },
177
+ { "type": "send_sms", "smsRef": "sms.consult_reminder", "conditionalOn": "channels.sms == true" }
178
+ ]
179
+ },
180
+ {
181
+ "ref": "workflow.no_show",
182
+ "name": "No-Show Win-back",
183
+ "conditionalOn": "goal.bookingNeeded == true",
184
+ "trigger": { "type": "appointment", "appointmentStatus": "noshow", "calendarRef": "calendar.consult" },
185
+ "stopOnResponse": true,
186
+ "actions": [
187
+ { "type": "add_contact_tag", "tagRef": "tag.lc_lapsed" },
188
+ { "type": "internal_notification", "to": "{{operator.userId}}", "title": "No-show", "body": "{{contact.first_name}} missed their consult" },
189
+ { "type": "send_email", "emailRef": "email.no_show" },
190
+ {
191
+ "type": "find_opportunity",
192
+ "pipelineRef": "pipeline.patient_journey",
193
+ "_note": "find_opportunity MUST be the last action (its Found/Not-Found branches do not rejoin a linear tail).",
194
+ "found": [
195
+ { "type": "update_opportunity", "pipelineRef": "pipeline.patient_journey", "stageRef": "stage.contacted" },
196
+ { "type": "send_sms", "smsRef": "sms.no_show", "conditionalOn": "channels.sms == true" }
197
+ ],
198
+ "notFound": [
199
+ { "type": "send_email", "emailRef": "email.winback" }
200
+ ]
201
+ }
202
+ ]
203
+ }
204
+ ],
205
+ "handoffs": [
206
+ {
207
+ "ref": "handoff.add_staff",
208
+ "conditionalOn": "goal.bookingNeeded == true && business.teamSize != 'Just me'",
209
+ "owner": "OPERATOR-UI",
210
+ "title": "Assign booking staff to the consult calendar",
211
+ "trigger": "goal.bookingNeeded == true && a staff user must be chosen",
212
+ "instruction": "In the sub-account, open the Consultation calendar and assign the provider(s) who take consults (Settings > My Staff if no user exists yet). Blueprint auto-assigns the calendar only when you are the sole user; with a team it will not guess who books.",
213
+ "produces": null,
214
+ "successCheck": "The consultation calendar has at least one assigned staff member",
215
+ "blocks": ["calendar.consult"]
216
+ },
217
+ {
218
+ "ref": "handoff.calendar_oauth",
219
+ "conditionalOn": "flags includes calendar_oauth_needed",
220
+ "owner": "OPERATOR-UI",
221
+ "title": "Connect your calendar (Google / Outlook)",
222
+ "trigger": "goal.bookingNeeded == true && channels.calendarConnected == false",
223
+ "instruction": "Open the consultation calendar > Connections and authorize your Google or Outlook account. This login can only be approved by you. Then copy the booking link into the Booking Link custom value.",
224
+ "produces": "cv.booking_link",
225
+ "successCheck": "Calendar shows a connected external account and the booking link resolves",
226
+ "blocks": ["cv.booking_link"]
227
+ },
228
+ {
229
+ "ref": "handoff.a2p",
230
+ "conditionalOn": "flags includes needs_a2p",
231
+ "owner": "OPERATOR-EXT",
232
+ "title": "Register A2P 10DLC (so texts can send)",
233
+ "trigger": "channels.sms == true",
234
+ "instruction": "Submit your A2P brand and campaign through the GHL Trust Center / your carrier. The SMS steps are built but stay DRAFT and will not send until this is approved. Save your full submission package before submitting.",
235
+ "produces": null,
236
+ "successCheck": "A2P campaign status shows submitted or approved",
237
+ "blocks": ["workflow.speed_to_lead", "workflow.onboarding", "workflow.no_show", "sms.speed_to_lead", "sms.consult_reminder", "sms.no_show"],
238
+ "_blocksNote": "Lists the SMS-bearing WORKFLOW refs (so they stay DRAFT) plus the sms.* asset refs. After conditional pruning, drop any entry whose workflow/asset was removed (e.g. onboarding/no_show when goal.bookingNeeded == false)."
239
+ },
240
+ {
241
+ "ref": "handoff.stripe",
242
+ "conditionalOn": "flags includes stripe_not_connected",
243
+ "owner": "OPERATOR-EXT",
244
+ "title": "Connect Stripe (to take payment)",
245
+ "trigger": "channels.payment != 'Stripe connected'",
246
+ "instruction": "Connect Stripe under Payments > Integrations and create your consult-deposit and membership products. The funnel is built; authorizing the money connection is yours to do.",
247
+ "produces": null,
248
+ "successCheck": "Stripe shows connected and products exist",
249
+ "blocks": []
250
+ },
251
+ {
252
+ "ref": "handoff.email_domain",
253
+ "conditionalOn": "flags includes email_domain_needed",
254
+ "owner": "OPERATOR-UI",
255
+ "title": "Set up your sending email",
256
+ "trigger": "channels.email == false",
257
+ "instruction": "Connect a sending domain / mailbox in Settings > Email Services so the welcome and nurture emails send from your address instead of a default.",
258
+ "produces": null,
259
+ "successCheck": "A verified sending domain / from-address exists",
260
+ "blocks": ["email.welcome_1", "email.nurture_1", "email.consult_reminder", "email.no_show", "email.winback"]
261
+ }
262
+ ]
263
+ }
264
+ }