@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,67 @@
1
+ # Worked example proof — med_spa brief → plan → validate → dry_run
2
+
3
+ This is the live proof that the med_spa preset produces a plan that passes the MCP's
4
+ authoritative validator and dry-runs clean. Captured 2026-06-25 against the **MCP Testing**
5
+ sandbox (`JrV2p35O3hY2wqhr2c0T`, a throwaway 1-user account). dry_run writes nothing.
6
+
7
+ Source artifacts: [`medspa-brief.json`](medspa-brief.json) → [`medspa-build-plan.json`](medspa-build-plan.json).
8
+
9
+ ## 1. `validate_build_plan` (authoritative Zod schema + ref-integrity)
10
+
11
+ ```json
12
+ { "valid": true, "errors": [], "warnings": [], "referencesScanned": 48 }
13
+ ```
14
+
15
+ Every one of the 48 symbolic ref-pointers resolves to a defined object of the right
16
+ namespace. Zero dead refs, zero warnings. The structural guarantee holds: no action can
17
+ point at an ID that does not exist.
18
+
19
+ ## 2. `apply_build_plan` mode:"dry_run" — summary
20
+
21
+ ```json
22
+ {
23
+ "wouldCreate": 40, "existing": 0,
24
+ "workflowsTotal": 5, "workflowsGated": 3,
25
+ "actionsExpanded": 28, "actionsManual": 0, "actionsNeedContent": 0
26
+ }
27
+ ```
28
+
29
+ Definition of done met: **0 actionsNeedContent, 0 actionsManual**, and every workflow
30
+ trigger auto-built (`triggerAutoBuilt: true` on all 5). `calendarsManual: []` — the
31
+ round_robin consult calendar auto-builds because the account has exactly one user (the
32
+ solo operator is auto-assigned as staff).
33
+
34
+ ### Per-workflow expansion
35
+
36
+ | Workflow | Trigger (auto-built) | Actions | Gated DRAFT by |
37
+ |---|---|---|---|
38
+ | Speed to Lead | form_submission | 6 | handoff.a2p (sends SMS) |
39
+ | Lead Nurture | contact_tag | 4 | — (email-only, ungated) |
40
+ | Replied - Stop and Route | customer_reply | 3 | — |
41
+ | New Patient Onboarding | appointment (confirmed) | 6 | handoff.a2p (wait_appointment + SMS) |
42
+ | No-Show Win-back | appointment (noshow) | 9 | handoff.a2p (find_opportunity branch → SMS) |
43
+
44
+ The No-Show Win-back exercises `find_opportunity` as the last action: 9 native nodes
45
+ (head + 2 transitions + found[update_opportunity, send_sms] + notFound[send_email]).
46
+
47
+ ### Part 2 — "you must do these by hand" (expected, not failures)
48
+
49
+ - Design + populate the GHL funnel pages (Blueprint builds funnel + named steps;
50
+ page content is the operator's, filled from `funnel-page-content-template.md`).
51
+ - [OPERATOR-UI] Connect your calendar (produces the booking link).
52
+ - [OPERATOR-EXT] Register A2P 10DLC (holds the 3 SMS-bearing workflows DRAFT until met).
53
+ - [OPERATOR-EXT] Connect Stripe.
54
+
55
+ These are the honest, designed handoffs — the second half of the approval view, not
56
+ unexpected manual steps.
57
+
58
+ ## Notes for re-running
59
+
60
+ - The committed `medspa-build-plan.json` carries `internal_notification.to:
61
+ "REPLACEwithGetUsersId"` (a placeholder so the shipped example contains no real account
62
+ id). The live dry_run above substituted the demo account's real operator user id
63
+ (resolved via `get_users`), which is the one field that legitimately holds a real GHL id
64
+ (Blueprint never creates users). The skill resolves the subscriber's own id at plan-gen
65
+ time. Both forms dry_run identically clean.
66
+ - `execute` mode was intentionally not run: the DoD for the plan is validate + dry_run
67
+ clean before the approval gate. Live execution is the operator's post-approval step.
@@ -0,0 +1,82 @@
1
+ # Build Plan — Glow Aesthetics
2
+
3
+ A full lead-to-rebook account for your med spa: a pipeline that follows your own sales stages, the fields and tags to track every lead, a booking calendar, an inquiry form and landing page, and a complete email + text follow-up system that stops the moment a client replies. Below is what GHL Command will build for you automatically, and the short list of things only you can do, in order.
4
+
5
+ Generated from the **Generic Client** preset (v1.0.0) · brief source: agency_os · schema v0.1.
6
+ This is a plan for your review. Nothing is built yet. Edit anything below, then approve.
7
+
8
+ ---
9
+
10
+ ## 1. GHL Command will build this automatically (once you approve)
11
+
12
+ **Pipeline**
13
+ - Build pipeline "Glow Aesthetics Pipeline" with 6 stages: New Lead → Contacted → Consult Booked → Showed → Treated → Rebooked.
14
+
15
+ **Custom fields (6)**
16
+ - Lead Source (dropdown)
17
+ - Service / Product Interest (multi-select: Injectables, Filler, Facials, Laser, Skin Assessment)
18
+ - Appointment Date (date)
19
+ - Deposit Paid (checkbox)
20
+ - Deal Sold (text)
21
+ - Estimated Lifetime Value (monetary)
22
+
23
+ **Tags (18)**
24
+ - Source: source-website, source-instagram, source-facebook, source-google, source-referral, source-other
25
+ - Lifecycle: lifecycle-new-lead, contacted, qualified, booked, won, repeat, lapsed, lost
26
+ - Interest: interest-injectables, interest-filler, interest-facials, interest-laser
27
+
28
+ **Custom values (1)**
29
+ - Booking Link (filled after you connect your calendar — step 2 below)
30
+
31
+ **Calendar**
32
+ - "Glow Aesthetics Consultation" (round-robin, 30-min slots, 10-min buffer, Mon-Fri 9-5 default — edit hours after build). Needs a staff member assigned — step 1 below.
33
+
34
+ **Form**
35
+ - "Glow Aesthetics Inquiry": First name, Last name, Email, Phone, plus Service Interest mapped to the custom field.
36
+
37
+ **Funnel**
38
+ - "Glow Aesthetics Funnel": a Landing page (opt-in → the inquiry form) and a Thank You page (booking CTA).
39
+
40
+ **Email + SMS assets**
41
+ - 7 emails: Welcome, Nurture 1-3, No-Show Re-offer, Onboarding/Pre-visit, Win-back.
42
+ - 4 SMS: Speed-to-lead, Nurture, No-show, Win-back. _(Texts will not send until A2P is approved — step 3 below.)_
43
+
44
+ **Workflows (6)**
45
+ - "Speed to Lead" — on inquiry → tag, create opportunity at New Lead, send welcome email, alert you, wait 5 min, send a text. _(stops on reply)_
46
+ - "Lead Nurture" — over ~8 days for leads who don't book: email, text, email, email, then mark lapsed. _(stops on reply)_
47
+ - "No-Show / Reschedule" — on a missed appointment → tag, alert you, re-offer by email + text. _(stops on reply)_
48
+ - "New Patient / Client Onboarding" — on a booked appointment → stop the nurture, tag booked, send the pre-visit email.
49
+ - "Win-back" — for lapsed clients: email, text, then mark lost. _(stops on reply)_
50
+ - "Replied — Stop & Route" — the moment a lead replies → pull them out of nurture/win-back, tag contacted, alert you.
51
+
52
+ _Total: 58 objects across 11 types — a full pipeline, 6 fields, 18 tags, a calendar, a form, a funnel, 7 emails, 4 texts, and 6 workflows._
53
+
54
+ ---
55
+
56
+ ## 2. You must do these yourself, in this order
57
+
58
+ 1. **Add yourself (or a provider) as a team member** — **YOU-UI**
59
+ - What: In Settings > My Staff, add at least one user.
60
+ - Why now: the booking calendar can't be assigned to anyone until a staff user exists, and GHL Command can't add users.
61
+ - Done when: at least one staff user exists in the account.
62
+
63
+ 2. **Connect your calendar (Google / Outlook)** — **YOU-UI**
64
+ - What: open the consultation calendar > Connections and authorize Google or Outlook, then copy the booking link.
65
+ - Why now: this is a login only you can approve, and it produces the Booking Link the emails and texts use.
66
+ - Done when: the calendar shows a connected account and the booking link resolves.
67
+
68
+ 3. **Register A2P (so texts can send)** — **YOU-EXT**
69
+ - What: submit your A2P brand and campaign in the GHL Trust Center. Save your submission package first.
70
+ - Why now: every SMS step is built but stays dark until A2P is approved (7-14 day review).
71
+ - Done when: the A2P campaign shows submitted/approved.
72
+
73
+ 4. **Connect Stripe (to take payment)** — **YOU-EXT**
74
+ - What: connect Stripe in Payments > Integrations and create your products/prices.
75
+ - Why now: the landing page is built, but the money connection is yours to authorize.
76
+ - Done when: Stripe shows connected and your products exist.
77
+
78
+ _Not included (not needed for this build): an email-domain step — your email sending is already set up._
79
+
80
+ ---
81
+
82
+ _v1 stops here. When you approve, the automatic list above is ready for one-shot staging (phase 2). Each manual step is verified before the steps that depend on it run, so nothing is ever wired to something that doesn't exist yet._
@@ -0,0 +1,13 @@
1
+ {
2
+ "schemaVersion": "0.1",
3
+ "briefId": "sub_abc123",
4
+ "preset": "med_spa",
5
+ "briefSource": "agency_os",
6
+ "business": { "name": "Glow Aesthetics", "type": "med spa", "website": "glowaesthetics.com", "location": "Scottsdale, AZ", "timezone": "America/Phoenix" },
7
+ "offer": { "summary": "Injectables and facials for first-time and returning clients", "pricePoints": [{ "name": "New client consult", "price": "19" }, { "name": "Tox session", "price": "350" }], "leadMagnet": "Free skin assessment", "avgDealValue": "350" },
8
+ "audience": { "ideal": "Women 30-55 in N. Scottsdale", "painPoints": ["aging skin", "trust / who to go to"], "objections": ["price", "fear of looking overdone"] },
9
+ "goal": { "primary": "book appointments", "salesStages": ["new lead", "contacted", "consult booked", "showed", "treated", "rebooked"], "bookingNeeded": true, "followUpStyle": "high-touch" },
10
+ "channels": { "email": true, "sms": true, "a2pStatus": "not started", "payment": "Stripe not connected", "calendarConnected": false, "social": ["instagram", "facebook"] },
11
+ "assets": { "existingPipeline": "no", "existingWorkflows": "", "brand": "logo + navy/gold, domain ready", "notes": "" },
12
+ "flags": ["needs_a2p", "stripe_not_connected", "calendar_oauth_needed"]
13
+ }
@@ -0,0 +1,227 @@
1
+ {
2
+ "schemaVersion": "0.1",
3
+ "planId": "plan_abc123",
4
+ "briefId": "sub_abc123",
5
+ "preset": "med_spa",
6
+ "summary": "Build plan for Glow Aesthetics (med spa, Scottsdale AZ). Generated from the generic preset v1.0.0, brief source agency_os. Full lead-to-rebook account: a 6-stage pipeline from the client's own sales stages, 6 custom fields, an 18-tag system (source + lifecycle + service interest), a Booking Link custom value, a round-robin consultation calendar, an inquiry form + lead funnel, 7 emails + 4 SMS, and 6 workflows (speed-to-lead, nurture, no-show, onboarding, win-back, replied-stop-route) all stopping on reply. Operator handoffs: add staff, connect calendar (Google OAuth), register A2P (texts are dark until then), connect Stripe. No email-domain handoff needed (email sending is already set up).",
7
+ "pipelines": [
8
+ {
9
+ "ref": "pipeline.main",
10
+ "name": "Glow Aesthetics Pipeline",
11
+ "stages": [
12
+ { "ref": "stage.new_lead", "name": "New Lead", "position": 0 },
13
+ { "ref": "stage.contacted", "name": "Contacted", "position": 1 },
14
+ { "ref": "stage.consult_booked", "name": "Consult Booked", "position": 2 },
15
+ { "ref": "stage.showed", "name": "Showed", "position": 3 },
16
+ { "ref": "stage.treated", "name": "Treated", "position": 4 },
17
+ { "ref": "stage.rebooked", "name": "Rebooked", "position": 5 }
18
+ ]
19
+ }
20
+ ],
21
+ "customFields": [
22
+ { "ref": "field.lead_source", "name": "Lead Source", "dataType": "SINGLE_OPTIONS", "model": "contact" },
23
+ { "ref": "field.interest", "name": "Service / Product Interest", "dataType": "MULTIPLE_OPTIONS", "model": "contact", "options": ["Injectables", "Filler", "Facials", "Laser", "Skin Assessment"] },
24
+ { "ref": "field.appt_date", "name": "Appointment Date", "dataType": "DATE", "model": "contact" },
25
+ { "ref": "field.deposit_paid", "name": "Deposit Paid", "dataType": "CHECKBOX", "model": "contact" },
26
+ { "ref": "field.deal_sold", "name": "Deal Sold", "dataType": "TEXT", "model": "contact" },
27
+ { "ref": "field.ltv", "name": "Estimated Lifetime Value", "dataType": "MONETORY", "model": "contact" }
28
+ ],
29
+ "tags": [
30
+ { "ref": "tag.source_website", "name": "source-website" },
31
+ { "ref": "tag.source_instagram", "name": "source-instagram" },
32
+ { "ref": "tag.source_facebook", "name": "source-facebook" },
33
+ { "ref": "tag.source_google", "name": "source-google" },
34
+ { "ref": "tag.source_referral", "name": "source-referral" },
35
+ { "ref": "tag.source_other", "name": "source-other" },
36
+ { "ref": "tag.lc_new_lead", "name": "lifecycle-new-lead" },
37
+ { "ref": "tag.lc_contacted", "name": "lifecycle-contacted" },
38
+ { "ref": "tag.lc_qualified", "name": "lifecycle-qualified" },
39
+ { "ref": "tag.lc_appointment", "name": "lifecycle-booked" },
40
+ { "ref": "tag.lc_won", "name": "lifecycle-won" },
41
+ { "ref": "tag.lc_repeat", "name": "lifecycle-repeat" },
42
+ { "ref": "tag.lc_lapsed", "name": "lifecycle-lapsed" },
43
+ { "ref": "tag.lc_lost", "name": "lifecycle-lost" },
44
+ { "ref": "tag.interest_injectables", "name": "interest-injectables" },
45
+ { "ref": "tag.interest_filler", "name": "interest-filler" },
46
+ { "ref": "tag.interest_facials", "name": "interest-facials" },
47
+ { "ref": "tag.interest_laser", "name": "interest-laser" }
48
+ ],
49
+ "customValues": [
50
+ { "ref": "cv.booking_link", "name": "Booking Link", "value": "", "filledBy": "handoff.calendar_oauth" }
51
+ ],
52
+ "calendars": [
53
+ {
54
+ "ref": "calendar.consult",
55
+ "name": "Glow Aesthetics Consultation",
56
+ "calendarType": "round_robin",
57
+ "openHours": [{ "daysOfTheWeek": [1, 2, 3, 4, 5], "hours": [{ "openHour": 9, "openMinute": 0, "closeHour": 17, "closeMinute": 0 }] }],
58
+ "availabilityType": 0,
59
+ "slotDuration": 30,
60
+ "slotBuffer": 10,
61
+ "requiresStaff": true
62
+ }
63
+ ],
64
+ "forms": [
65
+ {
66
+ "ref": "form.intake",
67
+ "name": "Glow Aesthetics Inquiry",
68
+ "fields": [
69
+ { "type": "standard", "key": "first_name", "required": true },
70
+ { "type": "standard", "key": "last_name", "required": false },
71
+ { "type": "standard", "key": "email", "required": true },
72
+ { "type": "standard", "key": "phone", "required": false },
73
+ { "type": "custom", "fieldRef": "field.interest", "required": false }
74
+ ]
75
+ }
76
+ ],
77
+ "funnels": [
78
+ {
79
+ "ref": "funnel.main",
80
+ "name": "Glow Aesthetics Funnel",
81
+ "pages": [
82
+ { "ref": "page.landing", "name": "Landing", "role": "optin", "outline": "Hero: clearer, natural-looking results for women 30-55 who want to look like themselves. Three benefit points (expertise, natural results, first-visit comfort). Single CTA to the inquiry form. Navy/gold to match brand.", "formRef": "form.intake" },
83
+ { "ref": "page.thankyou", "name": "Thank You", "role": "confirmation", "outline": "Confirm we got their info; set expectation that we will reach out fast; book-a-consult CTA.", "calendarRef": "calendar.consult" }
84
+ ]
85
+ }
86
+ ],
87
+ "emails": [
88
+ { "ref": "email.welcome_1", "name": "Welcome / First Touch", "subject": "Thanks for reaching out to Glow Aesthetics", "bodyOutline": "Thank them by name; restate that we help them get natural-looking results from injectables and facials; one clear next step (book a complimentary consult); warm, no hype.", "mergeTags": ["contact.first_name"] },
89
+ { "ref": "email.nurture_1", "name": "Nurture 1 — Value", "subject": "The thing most people get wrong about aging skin", "bodyOutline": "Lead with the 'aging skin' pain in their language; teach one useful idea (results come from a plan, not a single syringe); soft CTA to consult.", "mergeTags": ["contact.first_name"] },
90
+ { "ref": "email.nurture_2", "name": "Nurture 2 — Proof", "subject": "How a first visit actually works", "bodyOutline": "Walk through the consult + treatment process; address the 'fear of looking overdone' objection directly (conservative, natural philosophy); CTA to book.", "mergeTags": ["contact.first_name"] },
91
+ { "ref": "email.nurture_3", "name": "Nurture 3 — Last Call", "subject": "Did this slip?", "bodyOutline": "Short re-engagement; restate the single benefit (look like yourself, refreshed); final CTA; sign off graciously.", "mergeTags": ["contact.first_name"] },
92
+ { "ref": "email.no_show_1", "name": "No-Show Re-offer", "subject": "Missed you — let's get you rebooked", "bodyOutline": "Warm, no guilt; offer to rebook the consult; booking CTA via the Booking Link.", "mergeTags": ["contact.first_name"] },
93
+ { "ref": "email.onboarding_1", "name": "Onboarding / Pre-visit", "subject": "You're booked with Glow Aesthetics", "bodyOutline": "Confirm date/time; what to expect at the consult; light pre-care (avoid blood thinners if relevant) marked for provider review; reassurance.", "mergeTags": ["contact.first_name"] },
94
+ { "ref": "email.winback_1", "name": "Win-back", "subject": "It's been a while", "bodyOutline": "Reconnect with lapsed clients; reason to return tied to maintaining their results; CTA to rebook.", "mergeTags": ["contact.first_name"] }
95
+ ],
96
+ "sms": [
97
+ { "ref": "sms.speed_to_lead_1", "name": "Speed-to-lead text", "bodyOutline": "Instant text after a new inquiry: 'Hi {{first_name}}, it's Glow Aesthetics — thanks for reaching out! Want me to grab you a complimentary consult time? Reply STOP to opt out.'", "mergeTags": ["contact.first_name"] },
98
+ { "ref": "sms.nurture_1", "name": "Nurture text 1", "bodyOutline": "A couple days in: one-line nudge with the consult offer + booking link; opt-out line.", "mergeTags": ["contact.first_name"] },
99
+ { "ref": "sms.no_show_1", "name": "No-show text", "bodyOutline": "Quick rebook nudge after a no-show; booking link; opt-out line.", "mergeTags": ["contact.first_name"] },
100
+ { "ref": "sms.winback_1", "name": "Win-back text", "bodyOutline": "Friendly reconnect for lapsed clients; one reason to return; opt-out line.", "mergeTags": ["contact.first_name"] }
101
+ ],
102
+ "workflows": [
103
+ {
104
+ "ref": "workflow.speed_to_lead",
105
+ "name": "Speed to Lead",
106
+ "trigger": { "type": "form_submission", "formRef": "form.intake" },
107
+ "stopOnResponse": true,
108
+ "actions": [
109
+ { "type": "add_contact_tag", "tagRef": "tag.lc_new_lead" },
110
+ { "type": "create_opportunity", "pipelineRef": "pipeline.main", "stageRef": "stage.new_lead", "name": "{{contact.name}} - New Lead" },
111
+ { "type": "send_email", "emailRef": "email.welcome_1" },
112
+ { "type": "internal_notification", "to": "REPLACEwithGetUsersId", "title": "New lead", "body": "New inquiry from {{contact.first_name}}" },
113
+ { "type": "wait", "value": 5, "unit": "minutes" },
114
+ { "type": "send_sms", "smsRef": "sms.speed_to_lead_1" }
115
+ ]
116
+ },
117
+ {
118
+ "ref": "workflow.lead_nurture",
119
+ "name": "Lead Nurture",
120
+ "trigger": { "type": "contact_tag", "tagRef": "tag.lc_new_lead" },
121
+ "stopOnResponse": true,
122
+ "actions": [
123
+ { "type": "wait", "value": 1, "unit": "days" },
124
+ { "type": "send_email", "emailRef": "email.nurture_1" },
125
+ { "type": "wait", "value": 2, "unit": "days" },
126
+ { "type": "send_sms", "smsRef": "sms.nurture_1" },
127
+ { "type": "wait", "value": 2, "unit": "days" },
128
+ { "type": "send_email", "emailRef": "email.nurture_2" },
129
+ { "type": "wait", "value": 3, "unit": "days" },
130
+ { "type": "send_email", "emailRef": "email.nurture_3" },
131
+ { "type": "add_contact_tag", "tagRef": "tag.lc_lapsed" }
132
+ ]
133
+ },
134
+ {
135
+ "ref": "workflow.no_show",
136
+ "name": "No-Show / Reschedule",
137
+ "trigger": { "type": "appointment", "appointmentStatus": "noshow", "calendarRef": "calendar.consult" },
138
+ "stopOnResponse": true,
139
+ "actions": [
140
+ { "type": "add_contact_tag", "tagRef": "tag.lc_contacted" },
141
+ { "type": "internal_notification", "to": "REPLACEwithGetUsersId", "title": "No-show", "body": "{{contact.first_name}} missed their appointment" },
142
+ { "type": "send_email", "emailRef": "email.no_show_1" },
143
+ { "type": "send_sms", "smsRef": "sms.no_show_1" }
144
+ ]
145
+ },
146
+ {
147
+ "ref": "workflow.onboarding",
148
+ "name": "New Patient / Client Onboarding",
149
+ "trigger": { "type": "appointment", "appointmentStatus": "confirmed", "calendarRef": "calendar.consult" },
150
+ "stopOnResponse": false,
151
+ "actions": [
152
+ { "type": "remove_from_workflow", "workflowRef": "workflow.lead_nurture" },
153
+ { "type": "add_contact_tag", "tagRef": "tag.lc_appointment" },
154
+ { "type": "send_email", "emailRef": "email.onboarding_1" }
155
+ ]
156
+ },
157
+ {
158
+ "ref": "workflow.win_back",
159
+ "name": "Win-back",
160
+ "trigger": { "type": "contact_tag", "tagRef": "tag.lc_lapsed" },
161
+ "stopOnResponse": true,
162
+ "actions": [
163
+ { "type": "send_email", "emailRef": "email.winback_1" },
164
+ { "type": "wait", "value": 3, "unit": "days" },
165
+ { "type": "send_sms", "smsRef": "sms.winback_1" },
166
+ { "type": "wait", "value": 7, "unit": "days" },
167
+ { "type": "add_contact_tag", "tagRef": "tag.lc_lost" }
168
+ ]
169
+ },
170
+ {
171
+ "ref": "workflow.reply_route",
172
+ "name": "Replied — Stop & Route",
173
+ "trigger": { "type": "customer_reply" },
174
+ "stopOnResponse": false,
175
+ "actions": [
176
+ { "type": "remove_from_workflow", "workflowRef": "workflow.lead_nurture" },
177
+ { "type": "remove_from_workflow", "workflowRef": "workflow.win_back" },
178
+ { "type": "add_contact_tag", "tagRef": "tag.lc_contacted" },
179
+ { "type": "internal_notification", "to": "REPLACEwithGetUsersId", "title": "Lead replied", "body": "{{contact.first_name}} replied — follow up" }
180
+ ]
181
+ }
182
+ ],
183
+ "handoffs": [
184
+ {
185
+ "ref": "handoff.add_staff",
186
+ "owner": "OPERATOR-UI",
187
+ "title": "Add yourself (or a provider) as a team member",
188
+ "trigger": "goal.bookingNeeded == true",
189
+ "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.",
190
+ "produces": null,
191
+ "successCheck": "At least one staff user exists in the account",
192
+ "blocks": ["calendar.consult"]
193
+ },
194
+ {
195
+ "ref": "handoff.calendar_oauth",
196
+ "owner": "OPERATOR-UI",
197
+ "title": "Connect your calendar (Google / Outlook)",
198
+ "trigger": "goal.bookingNeeded == true && channels.calendarConnected == false",
199
+ "instruction": "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.",
200
+ "produces": "cv.booking_link",
201
+ "successCheck": "Calendar shows a connected external account; booking link resolves",
202
+ "blocks": ["cv.booking_link"]
203
+ },
204
+ {
205
+ "ref": "handoff.a2p",
206
+ "owner": "OPERATOR-EXT",
207
+ "title": "Register A2P (so texts can send)",
208
+ "trigger": "channels.sms == true",
209
+ "instruction": "Submit your A2P brand and campaign with the carrier (via the GHL Trust Center). The SMS steps are built but will NOT send until this is approved. Save your submission package before submitting.",
210
+ "produces": null,
211
+ "successCheck": "A2P campaign status shows submitted/approved",
212
+ "blocks": ["sms.speed_to_lead_1", "sms.nurture_1", "sms.no_show_1", "sms.winback_1"]
213
+ },
214
+ {
215
+ "ref": "handoff.stripe",
216
+ "owner": "OPERATOR-EXT",
217
+ "title": "Connect Stripe (to take payment)",
218
+ "trigger": "channels.payment != 'Stripe connected'",
219
+ "instruction": "Connect Stripe in Payments > Integrations and create your products/prices. The landing page is built; the money connection is yours to authorize.",
220
+ "produces": null,
221
+ "successCheck": "Stripe shows connected; products exist",
222
+ "blocks": []
223
+ }
224
+ ],
225
+ "buildOrder": ["tag.*", "field.*", "cv.*", "pipeline.*", "calendar.*", "form.*", "funnel.*", "email.*", "sms.*", "workflow.*"],
226
+ "idMap": {}
227
+ }
@@ -0,0 +1,127 @@
1
+ #!/usr/bin/env node
2
+ // Validates an Intake-to-Build §5 build plan against the contract rules.
3
+ // Not the MCP's authoritative Zod validator (that lives in the mcp repo) — this is
4
+ // atlas's self-check that the skill's output is internally consistent + schema-shaped:
5
+ // every ref resolves, no dangling pointers, GHL-correct enums, workflow sanity.
6
+ // Usage: node validate-plan.cjs [plan.json]
7
+
8
+ const fs = require('fs');
9
+ const path = require('path');
10
+
11
+ const planPath = process.argv[2] || path.join(__dirname, 'sample-build-plan.json');
12
+ const plan = JSON.parse(fs.readFileSync(planPath, 'utf8'));
13
+
14
+ const errors = [];
15
+ const warns = [];
16
+ const E = (m) => errors.push(m);
17
+ const W = (m) => warns.push(m);
18
+
19
+ const DATA_TYPES = new Set(['TEXT', 'LARGE_TEXT', 'NUMERICAL', 'PHONE', 'MONETORY', 'CHECKBOX', 'SINGLE_OPTIONS', 'MULTIPLE_OPTIONS', 'FLOAT', 'DATE', 'TEXTBOX_LIST', 'FILE_UPLOAD', 'SIGNATURE']);
20
+ const CAL_TYPES = new Set(['round_robin', 'event', 'class_booking', 'collective', 'service_booking']);
21
+ const OWNERS = new Set(['OPERATOR-UI', 'OPERATOR-EXT', 'TEAM', 'JERRY-UI', 'JERRY-EXT']); // canonical (v3.44+) + legacy accepted
22
+ const MODELS = new Set(['contact', 'opportunity']);
23
+
24
+ // ---- collect all defined refs ----
25
+ const refs = new Set();
26
+ const addRef = (r) => { if (refs.has(r)) E(`duplicate ref: ${r}`); refs.add(r); };
27
+
28
+ (plan.pipelines || []).forEach(p => { addRef(p.ref); (p.stages || []).forEach(s => addRef(s.ref)); });
29
+ (plan.customFields || []).forEach(f => addRef(f.ref));
30
+ (plan.tags || []).forEach(t => addRef(t.ref));
31
+ (plan.customValues || []).forEach(c => addRef(c.ref));
32
+ (plan.calendars || []).forEach(c => addRef(c.ref));
33
+ (plan.forms || []).forEach(f => addRef(f.ref));
34
+ (plan.funnels || []).forEach(f => { addRef(f.ref); (f.pages || []).forEach(pg => addRef(pg.ref)); });
35
+ (plan.emails || []).forEach(e => addRef(e.ref));
36
+ (plan.sms || []).forEach(s => addRef(s.ref));
37
+ (plan.workflows || []).forEach(w => addRef(w.ref));
38
+ (plan.handoffs || []).forEach(h => addRef(h.ref));
39
+
40
+ const need = (r, where) => { if (r == null) return; if (!refs.has(r)) E(`dangling ref ${r} (in ${where})`); };
41
+
42
+ // ---- top level ----
43
+ if (plan.schemaVersion !== '0.1') W(`schemaVersion is ${plan.schemaVersion}, expected 0.1`);
44
+ ['planId', 'briefId', 'preset', 'summary'].forEach(k => { if (!plan[k]) E(`missing top-level ${k}`); });
45
+ if (plan.idMap == null || Object.keys(plan.idMap).length) W('idMap should be present and empty at authoring time');
46
+
47
+ // ---- custom fields ----
48
+ (plan.customFields || []).forEach(f => {
49
+ if (!DATA_TYPES.has(f.dataType)) E(`customField ${f.ref}: bad dataType "${f.dataType}"`);
50
+ if (f.model && !MODELS.has(f.model)) E(`customField ${f.ref}: bad model "${f.model}"`);
51
+ });
52
+
53
+ // ---- calendars ----
54
+ (plan.calendars || []).forEach(c => {
55
+ if (!CAL_TYPES.has(c.calendarType)) E(`calendar ${c.ref}: bad calendarType "${c.calendarType}"`);
56
+ });
57
+
58
+ // ---- custom values ----
59
+ (plan.customValues || []).forEach(c => { need(c.filledBy, `cv ${c.ref}.filledBy`); });
60
+
61
+ // ---- forms ----
62
+ (plan.forms || []).forEach(f => {
63
+ (f.fields || []).forEach(fl => {
64
+ if (fl.type === 'custom') need(fl.fieldRef, `form ${f.ref} field`);
65
+ else if (fl.type === 'standard') { if (!fl.key) E(`form ${f.ref}: standard field missing key`); }
66
+ else E(`form ${f.ref}: bad field type "${fl.type}"`);
67
+ });
68
+ });
69
+
70
+ // ---- funnels ----
71
+ (plan.funnels || []).forEach(f => {
72
+ (f.pages || []).forEach(pg => { need(pg.formRef, `page ${pg.ref}.formRef`); need(pg.calendarRef, `page ${pg.ref}.calendarRef`); });
73
+ });
74
+
75
+ // ---- workflows ----
76
+ const stageRefs = new Set();
77
+ (plan.pipelines || []).forEach(p => (p.stages || []).forEach(s => stageRefs.add(s.ref)));
78
+ (plan.workflows || []).forEach(w => {
79
+ if (!w.trigger || !w.trigger.type) E(`workflow ${w.ref}: missing trigger`);
80
+ if (w.trigger) { need(w.trigger.formRef, `wf ${w.ref} trigger.formRef`); need(w.trigger.tagRef, `wf ${w.ref} trigger.tagRef`); need(w.trigger.calendarRef, `wf ${w.ref} trigger.calendarRef`); }
81
+ if (!Array.isArray(w.actions) || w.actions.length === 0) E(`workflow ${w.ref}: no actions`);
82
+ if ((w.actions || []).length > 40) E(`workflow ${w.ref}: >40 actions (split required)`);
83
+ (w.actions || []).forEach((a, i) => {
84
+ const at = `wf ${w.ref} action[${i}] ${a.type}`;
85
+ need(a.tagRef, `${at}.tagRef`); need(a.emailRef, `${at}.emailRef`); need(a.smsRef, `${at}.smsRef`);
86
+ need(a.pipelineRef, `${at}.pipelineRef`); need(a.workflowRef, `${at}.workflowRef`);
87
+ if (a.stageRef && !stageRefs.has(a.stageRef)) E(`${at}: stageRef ${a.stageRef} not a defined stage`);
88
+ });
89
+ // nurture exit hygiene: any wf with send_* + waits should stop on response OR be a one-shot route
90
+ const hasSends = (w.actions || []).some(a => a.type === 'send_email' || a.type === 'send_sms');
91
+ const hasWaits = (w.actions || []).some(a => a.type === 'wait');
92
+ if (hasSends && hasWaits && w.stopOnResponse !== true) W(`workflow ${w.ref}: multi-step send sequence not stopOnResponse`);
93
+ });
94
+
95
+ // ---- handoffs ----
96
+ (plan.handoffs || []).forEach(h => {
97
+ if (!OWNERS.has(h.owner)) E(`handoff ${h.ref}: bad owner "${h.owner}"`);
98
+ if (!h.successCheck) E(`handoff ${h.ref}: missing successCheck`);
99
+ (h.blocks || []).forEach(b => {
100
+ // blocks may use a wildcard like sms.* — accept wildcard if any ref in that namespace exists
101
+ if (b.endsWith('.*')) { const ns = b.slice(0, -2); if (![...refs].some(r => r.startsWith(ns + '.'))) W(`handoff ${h.ref}: blocks "${b}" matches nothing`); }
102
+ else need(b, `handoff ${h.ref}.blocks`);
103
+ });
104
+ if (h.produces) need(h.produces, `handoff ${h.ref}.produces`);
105
+ });
106
+
107
+ // ---- report ----
108
+ const counts = {
109
+ pipelines: (plan.pipelines || []).length,
110
+ stages: stageRefs.size,
111
+ customFields: (plan.customFields || []).length,
112
+ tags: (plan.tags || []).length,
113
+ customValues: (plan.customValues || []).length,
114
+ calendars: (plan.calendars || []).length,
115
+ forms: (plan.forms || []).length,
116
+ funnels: (plan.funnels || []).length,
117
+ emails: (plan.emails || []).length,
118
+ sms: (plan.sms || []).length,
119
+ workflows: (plan.workflows || []).length,
120
+ handoffs: (plan.handoffs || []).length,
121
+ };
122
+
123
+ console.log(`Plan: ${path.basename(planPath)} (preset ${plan.preset}, ${refs.size} refs)`);
124
+ console.log('Counts:', JSON.stringify(counts));
125
+ if (warns.length) { console.log(`\nWARN (${warns.length}):`); warns.forEach(w => console.log(' - ' + w)); }
126
+ if (errors.length) { console.log(`\nERRORS (${errors.length}):`); errors.forEach(e => console.log(' ✗ ' + e)); process.exit(1); }
127
+ console.log('\n✓ VALID — every ref resolves, enums correct, workflows sane. Schema-valid §5 plan.');
@@ -0,0 +1,39 @@
1
+ # Preset: Clinic Launch Event + A2P (not default)
2
+
3
+ **File:** `clinic-launch-a2p.preset.json` · **id:** `clinic_launch_a2p` · **version:** 1.0.0 · **default:** no
4
+ **Source:** `client-provisioning-spec.md` + CLL `CLIENT_BUILD_AUTOMATION_BLUEPRINT.md` + `A2P_Client_Playbook.md` + QA v3 snapshot.
5
+
6
+ ## When it is selected
7
+ Explicitly, or when the brief describes a **time-boxed on-site event + database-reactivation** model: event start/end dates, a deposit + balance, package tiers, and SMS reactivation of an existing opted-in list. It is **not** auto-selected by `business_type` — a med spa running a steady-state practice gets the `generic` preset, not this one.
8
+
9
+ ## Why it exists
10
+ It proves the preset format scales to the hardest case: a build that is mostly **operator handoffs and compliance gates**, not auto-created objects. It also makes Jerry's most-proven asset (the CLL launch system) reusable as one preset without making the whole feature CLL-specific.
11
+
12
+ ## What it builds (skeleton)
13
+ - **15-stage DBR pipeline** (DBR Lead → … → Package Sold → … → Lost) — the proven launch funnel.
14
+ - **9 representative intake/consent + sale fields** (the full launch intake is ~49 fields from the CLL onboarding/consent set; the skill creates the subset the workflows use). Package Recommended is genericized to Tier 1-4 with prices in custom values, not option labels.
15
+ - **17 tags** — DBR lines (1-4), DBR events, event status, routing.
16
+ - **25 custom values** — the launch data map (business/clinic identity, event window + capacity, deposit/trial/balance, package tier names + prices, and the link values filled by handoffs).
17
+ - **1 event calendar** — event-type, scoped to the event window, with a deposit.
18
+ - **1 patient intake & consent form** (phone not required — A2P opt-in rule).
19
+ - **No GHL funnel** — the opt-in presence is the dedicated **A2P compliance site** (6-page template), because A2P approval requires a real hosted site with exact consent HTML, not a GHL preview.
20
+ - **10 workflows** — DBR SMS line 1, auto-tag reply, send booking link (keyword BOOK), deposit-paid confirm, no-show, balance collection, package sold, post-event non-buyer (72hr), sold-out waitlist, STOP→DND. (The full system is ~27 WFs incl. lines 2-4; line 1 + the core set are encoded; additional lines clone the line-1 shape.)
21
+
22
+ > **Known manual step — SMS keyword triggers (decided 2026-06-27).** The keyword-routed workflows (BOOK / STOP / WAIT) use an `inbound_keyword` trigger that the executor does NOT auto-build yet. They build with all their steps, but the trigger is surfaced as a "set up this keyword trigger by hand in the GHL UI" step rather than auto-wiring. ghl-command-mcp decided to keep it manual for the v3.48.0 release (a new trigger type needs its own build-and-prove cycle) and queued a native build for when clinic keyword routing is prioritized. This is CLL-only and does not affect the med_spa/generic presets.
23
+
24
+ ## The handoff chain (the heart of this preset)
25
+ This preset is handoff-dominated. In dependency order:
26
+ 1. **add-staff** (blocks the calendar)
27
+ 2. **stripe-products** → fills the deposit/balance/tier link custom values
28
+ 3. **calendar-config** (dates, hours, deposit, connect) → fills the booking link
29
+ 4. **phone-numbers** (1 per ~500, max 4) → blocks the DBR lines
30
+ 5. **a2p-site** (deploy the compliance site, exact consent HTML) → blocks A2P submission
31
+ 6. **a2p-brand-campaign** (submit, avoid forbidden words, 7-14 day review) → blocks all SMS sends
32
+ 7. **conv-ai** (Chloe normal + sold-out prompts)
33
+ 8. **sms-warming** (4-week ramp, parallel with A2P)
34
+ 9. **list-import** (clean + split per line, test batch of 5) → blocks the DBR lines
35
+
36
+ The §5A approval view renders these as the "you must do manually, in this order" list, with each success check. That honesty — "here is everything GHL Command cannot do for you, sequenced" — is the point of the launch preset.
37
+
38
+ ## A2P safety carried into the copy
39
+ SMS `copyDirection` enforces A2P-safe wording (clinic name not legal entity, opt-out lines, and explicit avoidance of the forbidden list: weight-loss / health-claim / income / 'operated by' / reactivation-lead-gen language). The compliance-site handoff carries the exact consent-HTML requirements that passed 8/8 in the playbook.