@elitedcs/ghl-mcp 3.88.3 → 3.89.0-beta.10

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.
package/guide/guide.html CHANGED
@@ -4,7 +4,7 @@
4
4
  <meta charset="utf-8">
5
5
  <meta name="viewport" content="width=device-width, initial-scale=1">
6
6
  <title>GHL Command — User Guide</title>
7
- <!-- guides-hash: 15d2093538a1928d -->
7
+ <!-- guides-hash: f5a3b2744fb86bef -->
8
8
  <style>
9
9
  /* Deliberately light in every environment, including a dark-mode OS. This is a
10
10
  reference document people read at length, print, and save to PDF, and a page
@@ -320,7 +320,7 @@ Do not change anything. This is a read.</p></div>
320
320
  <li>The comparison is the useful one. Line a new client's account up against your template account and the setup list writes itself.</li>
321
321
  <li>User roles matter for workflow notifications. A notification pointed at somebody who has left the business fails silently, and this is where you spot them.</li>
322
322
  <li>Snapshots are how you carry a whole account setup to a new client. Claude can list what you have and create a share link for one.</li>
323
- <li>Chat creation is off until you enable “Let GHL Command create sub-accounts from chat” in Your agency. This switch does not enable deletion. GHL Command creates sub-accounts on GoHighLevel's Agency Pro plan; on other plans you create it in GoHighLevel and register it in GHL Command with its key. Creation also needs an agency key with permission to create locations. Deletion requires separate confirmation and GoHighLevel authorization; its current plan gate has not been verified. Removing a saved key only disconnects an account.</li>
323
+ <li>Chat creation is off until you enable “Let GHL Command create sub-accounts from chat” in Your agency. This switch does not enable deletion. GHL Command creates sub-accounts on GoHighLevel's Agency Pro plan; on other plans you create it in GoHighLevel and register it in GHL Command with its key. To create it: in GoHighLevel's agency view, click Sub-Accounts in the left menu, then Create Sub-Account. Fill in the business details and save. Creation also needs an agency key with permission to create locations. Deletion requires separate confirmation and GoHighLevel authorization; its current plan gate has not been verified. Removing a saved key only disconnects an account.</li>
324
324
  <li>Needs the full license. Reading users and numbers is free.</li>
325
325
  <li><b>Destructive actions ask before they act.</b> Twelve of them did not until 31 August, including deleting a custom field, which erases that field on every contact, and deleting or voiding an invoice.</li>
326
326
  <li><b>Changing a sub-account's business details is refused if Claude cannot read the account first.</b> Those are the details your text-message registration is filed against, and a check that could not run is not a check that passed. Timezone and other settings are unaffected.</li>
@@ -571,7 +571,7 @@ Before you build: confirm the [Consult] calendar exists and scope the trigger to
571
571
  <li>Claude confirms the calendar and ties the trigger to it.</li>
572
572
  <li>It uses appointment-relative waits, so the reminders track the booking rather than the enrolment.</li>
573
573
  <li>It writes the messages with the real booking fields in them and shows you first.</li>
574
- <li>It builds as a draft; you publish it.</li>
574
+ <li>It builds as a draft; you publish it. Claude runs the pre-flight check on that exact version first, and publishing is refused until it comes back with no errors.</li>
575
575
  </ol>
576
576
  </div>
577
577
  <div class="gsec"><h2>Good to know</h2>
@@ -697,7 +697,7 @@ Build nothing until I say approved.</p></div>
697
697
  <li>If a workflow step moves a deal to a new stage, the plan makes it find the deal first. That pairing is checked before the build runs, because a stage move on its own saves cleanly and then does nothing at all.</li>
698
698
  <li>The manual list is short but real: connecting a payment provider, phone and A2P registration, connecting social accounts, and domain settings. Those need a human with account access, and no tool can honestly do them for you.</li>
699
699
  <li><b>If you are told there are no submissions, the answers are not lost.</b> GoHighLevel does not report submissions for forms created outside its own editor, which is every intake form this installs, so the tool that reads submissions will say there are none even after your client has filled it in. The answers are on the contact tagged <code>blueprint-intake</code>. Say &quot;read the intake from the contact&quot; and Claude will pull it from there with the ordinary contact tools and carry on into the plan.</li>
700
- <li>Blueprint builds assets inside a registered sub-account. GHL Command creates sub-accounts on GoHighLevel's Agency Pro plan; on other plans you create it in GoHighLevel and register it in GHL Command with its key. Confirm that account before building. On an account that has been built before, it refuses to quietly compose a second, different plan. That refusal is deliberate: it is what stops you ending up with duplicate pipelines and duplicate workflows.</li>
700
+ <li>Blueprint builds assets inside a registered sub-account. GHL Command creates sub-accounts on GoHighLevel's Agency Pro plan; on other plans you create it in GoHighLevel and register it in GHL Command with its key. To create it: in GoHighLevel's agency view, click Sub-Accounts in the left menu, then Create Sub-Account. Fill in the business details and save. Confirm that account before building. On an account that has been built before, it refuses to quietly compose a second, different plan. That refusal is deliberate: it is what stops you ending up with duplicate pipelines and duplicate workflows.</li>
701
701
  <li>The build plan is worth keeping. It is the document you walk your client through on the kickoff call.</li>
702
702
  <li>Budget an hour end to end, most of it your review time. The building itself is minutes.</li>
703
703
  <li>Needs the full license.</li>
@@ -1169,7 +1169,7 @@ Before you build: look up the real calendar, the real workflows you are removing
1169
1169
  <li>Claude looks up every workflow you are removing them from by name, in the account, and tells you if one does not exist.</li>
1170
1170
  <li>It pairs the stage move with a find step, so the move has something to act on.</li>
1171
1171
  <li>It shows you the plan and the confirmation messages before building.</li>
1172
- <li>It builds as a draft; you publish it.</li>
1172
+ <li>It builds as a draft; you publish it. Claude runs the pre-flight check on that exact version first, and publishing is refused until it comes back with no errors.</li>
1173
1173
  </ol>
1174
1174
  </div>
1175
1175
  <div class="gsec"><h2>Good to know</h2>
@@ -1344,8 +1344,44 @@ Before you build: look up the real calendar, the real workflows you are removing
1344
1344
  <p class="gpara">Remove the saved connection or take back the provider's permission.</p>
1345
1345
  <p class="gpara">Disconnect Google Drive in your Claude's connector settings.</p>
1346
1346
  </div>
1347
+ <div class="gsec"><h2>Zoom · not yet checked</h2>
1348
+ <p class="gpara">A Server-to-Server OAuth app is a small private app you make in your own Zoom account; its Account ID, Client ID and Client Secret let this machine read your cloud recordings without your Zoom password.</p>
1349
+ <p class="gpara">not yet checked</p>
1350
+ <h3>What it gives you</h3>
1351
+ <p class="gpara">Adds the meetings you recorded to the Zoom cloud in the last 7 days to your brief: how long each ran, whether a transcript or audio and video was saved, and a link to open it. A meeting recorded yesterday is flagged so you review it and send follow-ups.</p>
1352
+ <h3>What you need</h3>
1353
+ <p class="gpara">Have these ready before you connect.</p>
1354
+ <ul>
1355
+ <li>A Zoom account where you are the owner or an admin, with cloud recording turned on. Zoom's paid plans include it.</li>
1356
+ <li>Permission to build apps on marketplace.zoom.us. An account owner or admin normally has it.</li>
1357
+ <li>About ten minutes to make the app and copy three values: the Account ID, the Client ID and the Client Secret.</li>
1358
+ </ul>
1359
+ <h3>How to connect</h3>
1360
+ <p class="gpara">Follow these steps to give this machine access.</p>
1361
+ <h4>8 steps</h4>
1362
+ <p class="gpara">These steps have not been checked on a real screen yet.</p>
1363
+ <ol class="steps">
1364
+ <li>Open marketplace.zoom.us and sign in with your Zoom account.</li>
1365
+ <li>Choose Develop at the top right, then Build App.</li>
1366
+ <li>Pick Server-to-Server OAuth App and choose Create. Give it a name you will recognise, such as Morning brief reader.</li>
1367
+ <li>On App Credentials, copy the Account ID, the Client ID and the Client Secret. Keep them somewhere private until you paste them here.</li>
1368
+ <li>Fill in the Information page with your name and email; Zoom asks for them before it lets the app run.</li>
1369
+ <li>Open Scopes and choose Add Scopes. Add the user read scope, which Zoom may list as View a user (user:read:user:admin), and the cloud recording read scope, which Zoom may list as View all user recordings (cloud&#95;recording:read:list&#95;user&#95;recordings:admin). Add nothing else.</li>
1370
+ <li>Open Activation and choose Activate your app.</li>
1371
+ <li>On the Zoom card on the Connections screen, paste the Account ID, the Client ID and the Client Secret, and choose Connect.</li>
1372
+ </ol>
1373
+ <h3>What to say</h3>
1374
+ <p class="gpara">In a Claude chat you can say one of these to connect or use it.</p>
1375
+ <ul>
1376
+ <li>“Connect my Zoom.”</li>
1377
+ <li>“Disconnect my Zoom.”</li>
1378
+ </ul>
1379
+ <h3>How to take access back</h3>
1380
+ <p class="gpara">Remove the saved connection or take back the provider's permission.</p>
1381
+ <p class="gpara">Choose Disconnect on this system's Connections card to remove the app's credentials from this machine. To take access back everywhere, open marketplace.zoom.us, then Manage, open the app and deactivate or delete it.</p>
1382
+ </div>
1347
1383
  <div class="gsec"><h2>Not built yet</h2>
1348
- <p class="gpara">Not built yet: Zoom, Plaud.</p>
1384
+ <p class="gpara">Not built yet: Plaud.</p>
1349
1385
  </div>
1350
1386
  <p class="gfoot">Works in Claude Desktop and Claude Code. Stuck? Email support@ghlcommand.com and a human answers.</p>
1351
1387
  </main></div>
@@ -1654,9 +1690,9 @@ Show me the first ten rows in the chat so I can sanity check them before you wri
1654
1690
  <a class="back" href="#top">&#8592; All guides</a>
1655
1691
  <div class="gsec"><h2>Before you start: what you need</h2>
1656
1692
  <ul>
1657
- <li><b>A GoHighLevel plan that includes API access.</b> That is GoHighLevel Unlimited ($297 a month) or Agency Pro ($497). The $97 Starter plan has no API, so nothing can connect to it, GHL Command included. Check under Settings, then Billing, in GoHighLevel before you install.</li>
1693
+ <li><b>A GoHighLevel plan that includes API access.</b> That is GoHighLevel Unlimited ($297 a month) or Agency Pro ($497). The $97 Starter plan has no API, so nothing can connect to it, GHL Command included. Check under Settings, then Billing, in GoHighLevel before you install. On Unlimited ($297), GoHighLevel does not let GHL Command create sub-accounts or add staff users. Both are Agency Pro only, so on $297 you do those two jobs in GoHighLevel yourself.</li>
1658
1694
  <li><b>Claude on this computer.</b> GHL Command runs inside the Claude Desktop app or inside Claude Code in a terminal. It cannot run in a claude.ai tab in your browser. Not sure which you have? The Members Hub page Which Claude do I use? settles it in two minutes, and with it every later question about restarts.</li>
1659
- <li><b>A Private Integration token</b> from the sub-account you want to work in, and that sub-account's location ID (the part of its GoHighLevel web address after /location/). Claude asks for both during setup, not the installer. GoHighLevel shows the token under Settings, then Private Integrations.</li>
1695
+ <li><b>A Private Integration token</b> from the sub-account you want to work in, and that sub-account's location ID. Claude asks for both during setup, not the installer. Inside the sub-account, create the token under Settings, then Private Integrations (under Other settings), then Create New Integration. The location ID is under Settings, then Business Profile, at the top of the General Information card, with a copy button. It is also the part of the sub-account's web address after /location/.</li>
1660
1696
  </ul>
1661
1697
  </div>
1662
1698
  <div class="gsec"><h2>What you'll get</h2>
@@ -1668,9 +1704,10 @@ Show me the first ten rows in the chat so I can sanity check them before you wri
1668
1704
  </div>
1669
1705
  <div class="gsec"><h2>The two steps</h2>
1670
1706
  <ol class="steps">
1671
- <li><b>Paste one command, once.</b> Open a terminal. On a Mac press Cmd+Space, type Terminal and press Return. On Windows press the Windows key, type cmd and press Enter. Paste <code>npx -y @elitedcs/ghl-mcp@latest cli install</code> and press Enter. That one command writes GHL Command into the Claude Desktop app's settings for you, so there is no JSON to hand-edit and anything else already in there is left alone. Your account and your license are connected in the next breath, inside Claude, not in the terminal. <b>Using Claude Code instead?</b> Paste this line instead of the installer: <code>claude mcp add --scope user -t stdio ghl -- npx -y @elitedcs/ghl-mcp@latest</code>. Then quit Claude fully and open it again (Desktop app: Cmd+Q on a Mac, not just closing the window; on Windows close it from the system tray too. Claude Code: start a new session). The new tools only load on a fresh start. Now say to Claude: &quot;Set up GHL Command with my license key.&quot; Claude asks for your email, your license key and that sub-account's Private Integration token and location ID, then answers: &quot;Step 1 of 2 done.&quot;</li>
1707
+ <li><b>Paste one command, once.</b> Open a terminal. On a Mac press Cmd+Space, type Terminal and press Return. On Windows press the Windows key, type cmd and press Enter. Paste <code>npx -y @elitedcs/ghl-mcp@latest cli install</code> and press Enter. That one command writes &quot;GHL Command&quot; into the Claude Desktop app's settings for you, so there is no JSON to hand-edit and anything else already in there is left alone. Your account and your license are connected in the next breath, inside Claude, not in the terminal. <b>Building client accounts from the cockpit?</b> The installer writes GHL Command into the Claude Desktop app only, and the cockpit runs every client-build stage through Claude Code. So also paste <code>npx -y @elitedcs/ghl-mcp@latest cli install --claude-code</code> once. It adds GHL Command to Claude Code and changes nothing else. The cockpit's Check readiness then confirms it. <b>Using Claude Code instead?</b> Paste this line instead of the installer: <code>claude mcp add --scope user -t stdio ghl -- npx -y @elitedcs/ghl-mcp@latest</code>. Then quit Claude fully and open it again (Desktop app: Cmd+Q on a Mac, not just closing the window; on Windows close it from the system tray too. Claude Code: start a new session). The new tools only load on a fresh start. Now say to Claude: &quot;Set up GHL Command with my license key.&quot; Claude asks for your email, your license key and that sub-account's Private Integration token and location ID, then answers: &quot;Step 1 of 2 done.&quot;</li>
1672
1708
  <li><b>One 60-second browser login.</b> Say to Claude: &quot;Unlock the Workflow Builder.&quot; A Chrome window opens and you log into GoHighLevel like you always do. This is how GHL itself trusts the workflow tools. One last step: quit Claude fully and open it once more so the workflow tools load. That is two restarts in all, thirty seconds each, and the install is finished.</li>
1673
1709
  </ol>
1710
+ <p class="gpara">Older installs show it as ghl; that is the same thing.</p>
1674
1711
  </div>
1675
1712
  <div class="gsec"><h2>Say this</h2>
1676
1713
  <div class="prompt"><q>Set up GHL Command with my license key.</q><button class="copy" type="button">Copy</button></div>
@@ -1810,7 +1847,7 @@ Before you build: confirm the account has a phone number that can send texts, an
1810
1847
  <li>Claude checks the account can actually send a text before it builds anything that sends one.</li>
1811
1848
  <li>It writes both messages for the caller you described and shows you them first.</li>
1812
1849
  <li>It builds the workflow as a draft and hands you the link to read it in GoHighLevel.</li>
1813
- <li>You publish it.</li>
1850
+ <li>You publish it. Claude runs the pre-flight check on that exact version first, and publishing is refused until it comes back with no errors.</li>
1814
1851
  </ol>
1815
1852
  </div>
1816
1853
  <div class="gsec"><h2>Good to know</h2>
@@ -1947,7 +1984,7 @@ Before you build: confirm the [Consult] calendar exists in this account and scop
1947
1984
  <li>Claude confirms the calendar exists and ties the trigger to it, rather than to every appointment in the account.</li>
1948
1985
  <li>It writes the messages and shows you them before building.</li>
1949
1986
  <li>It builds as a draft and hands you the link.</li>
1950
- <li>You publish it, and from then on the empty chair has a process attached to it.</li>
1987
+ <li>You publish it, and from then on the empty chair has a process attached to it. Claude runs the pre-flight check on that exact version first, and publishing is refused until it comes back with no errors.</li>
1951
1988
  </ol>
1952
1989
  </div>
1953
1990
  <div class="gsec"><h2>Good to know</h2>
@@ -2500,7 +2537,7 @@ Before you build: look up the real form, the real pipeline and stage, and the re
2500
2537
  <li>Claude looks up the real form, pipeline, stage and user in the account. Not names you typed, actual records.</li>
2501
2538
  <li>It writes the messages for the audience you described and shows you every one of them before anything is built.</li>
2502
2539
  <li>It builds the workflow as a draft, checks each piece exists after it creates it, and hands you the link.</li>
2503
- <li>You read the messages, then publish it yourself.</li>
2540
+ <li>You read the messages, then publish it yourself. Claude runs the pre-flight check on that exact version first, and publishing is refused until it comes back with no errors.</li>
2504
2541
  </ol>
2505
2542
  </div>
2506
2543
  <div class="gsec"><h2>Good to know</h2>
@@ -2989,7 +3026,7 @@ Do not add anyone to the workflow yet. Build it, show me the messages, and stop.
2989
3026
  <li>Claude builds the segment first and tells you the count, so you approve a number before you approve a campaign.</li>
2990
3027
  <li>It writes all five messages and shows you every one.</li>
2991
3028
  <li>It builds the workflow as a draft. Nobody is enrolled.</li>
2992
- <li>You read it, publish it, then add the tag to the segment in batches.</li>
3029
+ <li>You read it, publish it, then add the tag to the segment in batches. Claude runs the pre-flight check on that exact version before it publishes, and publishing is refused until it comes back with no errors.</li>
2993
3030
  </ol>
2994
3031
  </div>
2995
3032
  <div class="gsec"><h2>Good to know</h2>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@elitedcs/ghl-mcp",
3
- "version": "3.88.3",
3
+ "version": "3.89.0-beta.10",
4
4
  "mcpName": "io.github.drjerryrelth/ghl-command",
5
5
  "description": "GoHighLevel MCP Server for Claude. 250 tools — full CRM, automation, marketing control, account-wide workflow audit, live funnel-capture verification, and the only programmatic GHL workflow builder, now multi-tenant across client accounts.",
6
6
  "main": "dist/index.js",
@@ -55,7 +55,7 @@ Load the chosen preset's `skeleton`.
55
55
 
56
56
  Apply, in order:
57
57
  1. **Evaluate `conditionalOn`** on every item (and every workflow action). Drop where false. Prune refs that now dangle, and record each prune (you will note these in the summary, not silently drop).
58
- 2. **Resolve `fillFrom`** lists. The main case is pipeline stages: if `goal.salesStages` is non-empty, build `stage.<slug>` refs from it in order; else use the preset `default`. If a workflow references a `stage.*` not in the chosen set, map it to the nearest equivalent (first stage for "new lead" creation) and note it — never ship a dead ref.
58
+ 2. **Resolve `fillFrom`** lists. The main case is pipeline stages: if `goal.salesStages` is non-empty, build `stage.<slug>` refs from it in order; else use the preset `default`. If a workflow references a `stage.*` not in the chosen set, map it to the nearest equivalent through the preset stage's `aliases` (first stage for "new lead" creation; a workflow's `stagePreference` wins for that workflow) and note it — never ship a dead ref, and never add a template stage after the client's Lost.
59
59
  3. **Substitute `{{tokens}}`** (with `||` fallback). Where a token resolves empty and has no fallback, omit the field and note the default you used.
60
60
  4. **Derive interest tags + field options** from the offer/business type (e.g. `interest-injectables` for a med spa) and add them to `tags` + the `field.interest` options, mirroring each other.
61
61
 
@@ -435,7 +435,7 @@
435
435
  {
436
436
  "ref": "sms_template.n_sms_1",
437
437
  "name": "Nurture Text 1 - One Word",
438
- "body": "{{contact.first_name}}, quick one from {{custom_values.owner_first_name}} at {{custom_values.business_name}}: is it lines, volume, or skin texture you'd most like to fix first? One word is enough. Reply STOP to opt out."
438
+ "body": "{{contact.first_name}}, quick one from {{custom_values.owner_first_name}} at {{custom_values.business_name}}: is it lines, volume, or skin texture you'd most like to fix first? One word is enough.\nReply STOP to opt out."
439
439
  },
440
440
  {
441
441
  "ref": "sms_template.n_sms_2",
@@ -460,7 +460,7 @@
460
460
  {
461
461
  "ref": "sms_template.w_sms_1",
462
462
  "name": "Win-back Text 1",
463
- "body": "Hi {{contact.first_name}}, it's {{custom_values.owner_first_name}} at {{custom_values.business_name}}. Is your skin still on the list this season, or should I close your file for now? Reply STOP to opt out."
463
+ "body": "Hi {{contact.first_name}}, it's {{custom_values.owner_first_name}} at {{custom_values.business_name}}. Is your skin still on the list this season, or should I close your file for now?\nReply STOP to opt out."
464
464
  },
465
465
  {
466
466
  "ref": "sms_template.w_sms_2",
@@ -767,7 +767,7 @@
767
767
  "custom_values.owner_first_name",
768
768
  "custom_values.business_name"
769
769
  ],
770
- "body": "{{contact.first_name}}, quick one from {{custom_values.owner_first_name}} at {{custom_values.business_name}}: is it lines, volume, or skin texture you'd most like to fix first? One word is enough. Reply STOP to opt out."
770
+ "body": "{{contact.first_name}}, quick one from {{custom_values.owner_first_name}} at {{custom_values.business_name}}: is it lines, volume, or skin texture you'd most like to fix first? One word is enough.\nReply STOP to opt out."
771
771
  },
772
772
  {
773
773
  "ref": "sms.n_sms_2",
@@ -817,7 +817,7 @@
817
817
  "custom_values.owner_first_name",
818
818
  "custom_values.business_name"
819
819
  ],
820
- "body": "Hi {{contact.first_name}}, it's {{custom_values.owner_first_name}} at {{custom_values.business_name}}. Is your skin still on the list this season, or should I close your file for now? Reply STOP to opt out."
820
+ "body": "Hi {{contact.first_name}}, it's {{custom_values.owner_first_name}} at {{custom_values.business_name}}. Is your skin still on the list this season, or should I close your file for now?\nReply STOP to opt out."
821
821
  },
822
822
  {
823
823
  "ref": "sms.w_sms_2",
@@ -402,7 +402,7 @@
402
402
  {
403
403
  "ref": "sms_template.n_sms_1",
404
404
  "name": "Nurture Text 1 - One Word",
405
- "body": "{{contact.first_name}}, quick one from {{custom_values.owner_first_name}} at {{custom_values.business_name}}: what matters most to you here, speed, price, or getting it right first time? One word is enough. Reply STOP to opt out."
405
+ "body": "{{contact.first_name}}, quick one from {{custom_values.owner_first_name}} at {{custom_values.business_name}}: what matters most to you here, speed, price, or getting it right first time? One word is enough.\nReply STOP to opt out."
406
406
  },
407
407
  {
408
408
  "ref": "sms_template.n_sms_2",
@@ -427,7 +427,7 @@
427
427
  {
428
428
  "ref": "sms_template.w_sms_1",
429
429
  "name": "Win-back Text 1",
430
- "body": "Hi {{contact.first_name}}, it's {{custom_values.owner_first_name}} at {{custom_values.business_name}}. Is this still on your list this quarter, or should I close your file for now? Reply STOP to opt out."
430
+ "body": "Hi {{contact.first_name}}, it's {{custom_values.owner_first_name}} at {{custom_values.business_name}}. Is this still on your list this quarter, or should I close your file for now?\nReply STOP to opt out."
431
431
  },
432
432
  {
433
433
  "ref": "sms_template.w_sms_2",
@@ -734,7 +734,7 @@
734
734
  "custom_values.owner_first_name",
735
735
  "custom_values.business_name"
736
736
  ],
737
- "body": "{{contact.first_name}}, quick one from {{custom_values.owner_first_name}} at {{custom_values.business_name}}: what matters most to you here, speed, price, or getting it right first time? One word is enough. Reply STOP to opt out."
737
+ "body": "{{contact.first_name}}, quick one from {{custom_values.owner_first_name}} at {{custom_values.business_name}}: what matters most to you here, speed, price, or getting it right first time? One word is enough.\nReply STOP to opt out."
738
738
  },
739
739
  {
740
740
  "ref": "sms.n_sms_2",
@@ -784,7 +784,7 @@
784
784
  "custom_values.owner_first_name",
785
785
  "custom_values.business_name"
786
786
  ],
787
- "body": "Hi {{contact.first_name}}, it's {{custom_values.owner_first_name}} at {{custom_values.business_name}}. Is this still on your list this quarter, or should I close your file for now? Reply STOP to opt out."
787
+ "body": "Hi {{contact.first_name}}, it's {{custom_values.owner_first_name}} at {{custom_values.business_name}}. Is this still on your list this quarter, or should I close your file for now?\nReply STOP to opt out."
788
788
  },
789
789
  {
790
790
  "ref": "sms.w_sms_2",
@@ -175,15 +175,21 @@ const stageRefs = new Set();
175
175
  const days = (acts) => (acts || []).reduce((d, a) => a.type === 'wait' ? d + a.value * ({ minutes: 1 / 1440, hours: 1 / 24, days: 1 }[a.unit] || 0) : d, 0);
176
176
  const spanDays = (w) => { let d = days(w.actions); (w.actions || []).forEach(a => { if (a.type === 'find_opportunity') d += Math.max(days(a.found), days(a.notFound)); }); return d; };
177
177
  const nodeCount = (w) => (w.actions || []).reduce((n, a) => n + (a.type === 'find_opportunity' ? 3 + (a.found || []).length + (a.notFound || []).length : 1), 0);
178
- const nurtureTags = new Set((plan.workflows || []).filter(x => /nurture/i.test(x.name || '') && x.trigger && x.trigger.tagRef).map(x => x.trigger.tagRef));
178
+ const nurtureTags = new Set((plan.workflows || []).filter(x => /nurture/i.test(x.name || '') && x.trigger && !Array.isArray(x.trigger) && x.trigger.tagRef).map(x => x.trigger.tagRef));
179
179
  (plan.workflows || []).forEach(w => {
180
- if (!w.trigger || !w.trigger.type) E(`workflow ${w.ref}: missing trigger`);
181
- if (w.trigger) {
182
- 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`);
183
- need(w.trigger.pipelineRef, `wf ${w.ref} trigger.pipelineRef`); need(w.trigger.hasTagRef, `wf ${w.ref} trigger.hasTagRef`);
184
- if (w.trigger.stageRef && !stageRefs.has(w.trigger.stageRef)) E(`wf ${w.ref} trigger.stageRef ${w.trigger.stageRef} not a defined stage`);
185
- if (w.trigger.type === 'call_status' && !(w.trigger.callStatuses && w.trigger.callStatuses.length)) E(`workflow ${w.ref}: a call_status trigger needs callStatuses (else it fires on EVERY call)`);
186
- if (['tag_added', 'appointment_status', 'appointment_booked'].includes(w.trigger.type)) E(`workflow ${w.ref}: trigger type "${w.trigger.type}" is not native — use contact_tag / appointment`);
180
+ // A preset may list trigger variants, each with its own conditionalOn (the
181
+ // composer keeps the first that holds, e.g. the intake's review timing): each
182
+ // variant is checked as a trigger; the last must be the unconditional default.
183
+ const variants = Array.isArray(w.trigger) ? w.trigger : (w.trigger ? [w.trigger] : []);
184
+ if (Array.isArray(w.trigger) && (!variants.length || variants[variants.length - 1].conditionalOn)) E(`workflow ${w.ref}: a trigger list must end with a variant that has no conditionalOn (the default)`);
185
+ if (!variants.length || variants.some(t => !t || !t.type)) E(`workflow ${w.ref}: missing trigger`);
186
+ for (const t of variants) {
187
+ if (!t) continue;
188
+ need(t.formRef, `wf ${w.ref} trigger.formRef`); need(t.tagRef, `wf ${w.ref} trigger.tagRef`); need(t.calendarRef, `wf ${w.ref} trigger.calendarRef`);
189
+ need(t.pipelineRef, `wf ${w.ref} trigger.pipelineRef`); need(t.hasTagRef, `wf ${w.ref} trigger.hasTagRef`);
190
+ if (t.stageRef && !stageRefs.has(t.stageRef)) E(`wf ${w.ref} trigger.stageRef ${t.stageRef} not a defined stage`);
191
+ if (t.type === 'call_status' && !(t.callStatuses && t.callStatuses.length)) E(`workflow ${w.ref}: a call_status trigger needs callStatuses (else it fires on EVERY call)`);
192
+ if (['tag_added', 'appointment_status', 'appointment_booked'].includes(t.type)) E(`workflow ${w.ref}: trigger type "${t.type}" is not native — use contact_tag / appointment`);
187
193
  }
188
194
  if (!Array.isArray(w.actions) || w.actions.length === 0) E(`workflow ${w.ref}: no actions`);
189
195
  if ((w.actions || []).length > 40) E(`workflow ${w.ref}: >40 actions (split required)`);
@@ -200,7 +206,7 @@ const nurtureTags = new Set((plan.workflows || []).filter(x => /nurture/i.test(x
200
206
  if (a.type === 'assign_user' && !a.userRef) E(`${at}: needs userRef`);
201
207
  if (a.stageRef && !stageRefs.has(a.stageRef)) E(`${at}: stageRef ${a.stageRef} not a defined stage`);
202
208
  if (a.type === 'wait' && !(a.value > 0 && ['minutes', 'hours', 'days'].includes(a.unit))) E(`${at}: wait needs value > 0 and unit minutes|hours|days`);
203
- if (a.type === 'wait_appointment' && (!w.trigger || w.trigger.type !== 'appointment')) W(`${at}: wait_appointment outside an appointment-triggered workflow`);
209
+ if (a.type === 'wait_appointment' && !variants.some(t => t && t.type === 'appointment')) W(`${at}: wait_appointment outside an appointment-triggered workflow`);
204
210
  };
205
211
  let seenFind = false;
206
212
  (w.actions || []).forEach((a, i) => {
@@ -53,32 +53,39 @@
53
53
  "default": [
54
54
  {
55
55
  "ref": "stage.new_lead",
56
+ "aliases": ["new patient lead", "new patient", "new lead", "new inquiry", "inquiry", "new", "lead", "prospect"],
56
57
  "name": "New Patient Lead"
57
58
  },
58
59
  {
59
60
  "ref": "stage.contacted",
61
+ "aliases": ["contacted", "engaged", "in conversation", "qualified", "follow up", "working"],
60
62
  "name": "Contacted"
61
63
  },
62
64
  {
63
65
  "ref": "stage.visit_booked",
66
+ "aliases": ["visit booked", "appointment booked", "consult booked", "consultation booked", "exam booked", "booked", "scheduled", "=consultation"],
64
67
  "name": "Visit Booked",
65
68
  "conditionalOn": "goal.bookingNeeded == true"
66
69
  },
67
70
  {
68
71
  "ref": "stage.showed",
72
+ "aliases": ["showed", "attended", "visit completed", "exam completed", "consultation completed", "consultation done", "consultation attended", "consultation showed", "consultation complete", "consultation held"],
69
73
  "name": "Showed",
70
74
  "conditionalOn": "goal.bookingNeeded == true"
71
75
  },
72
76
  {
73
77
  "ref": "stage.plan_accepted",
78
+ "aliases": ["care plan accepted", "plan accepted", "accepted", "started care", "sold"],
74
79
  "name": "Care Plan Accepted"
75
80
  },
76
81
  {
77
82
  "ref": "stage.active",
83
+ "aliases": ["active patient", "active", "in care"],
78
84
  "name": "Active Patient"
79
85
  },
80
86
  {
81
87
  "ref": "stage.lost",
88
+ "aliases": ["lost", "closed lost", "not interested"],
82
89
  "name": "Lost"
83
90
  }
84
91
  ]
@@ -267,6 +274,12 @@
267
274
  "name": "review-requested",
268
275
  "_note": "A review was asked for after a completed visit.",
269
276
  "conditionalOn": "goal.bookingNeeded == true"
277
+ },
278
+ {
279
+ "ref": "tag.service_done",
280
+ "name": "service-complete",
281
+ "_note": "Only when the intake chose to ask for a review when the work is done: the team adds it, Review Request starts from it and removes it.",
282
+ "conditionalOn": "goal.bookingNeeded == true && goal.reviewTiming == 'When you mark the work done'"
270
283
  }
271
284
  ],
272
285
  "_tagNote": "Interest tags mirror the What Brings You In options and are intentionally non-clinical. The skill may add service tags from offer.summary only if compliance_notes allow it.",
@@ -517,7 +530,7 @@
517
530
  {
518
531
  "ref": "sms_template.n_sms_1",
519
532
  "name": "Nurture Text 1 - Insurance or Cash",
520
- "body": "{{contact.first_name}}, quick one from {{custom_values.owner_first_name}} at {{custom_values.business_name}}: will this be insurance or cash-pay? One word and we'll check your benefits before you come in. Reply STOP to opt out."
533
+ "body": "{{contact.first_name}}, quick one from {{custom_values.owner_first_name}} at {{custom_values.business_name}}: will this be insurance or cash-pay? One word and we'll check your benefits before you come in.\nReply STOP to opt out."
521
534
  },
522
535
  {
523
536
  "ref": "sms_template.n_sms_2",
@@ -542,7 +555,7 @@
542
555
  {
543
556
  "ref": "sms_template.w_sms_1",
544
557
  "name": "Win-back Text 1",
545
- "body": "Hi {{contact.first_name}}, it's {{custom_values.owner_first_name}} at {{custom_values.business_name}}. Are you still planning a first visit this season, or should I close your file for now? Reply STOP to opt out."
558
+ "body": "Hi {{contact.first_name}}, it's {{custom_values.owner_first_name}} at {{custom_values.business_name}}. Are you still planning a first visit this season, or should I close your file for now?\nReply STOP to opt out."
546
559
  },
547
560
  {
548
561
  "ref": "sms_template.w_sms_2",
@@ -1065,6 +1078,7 @@
1065
1078
  "workflows": [
1066
1079
  {
1067
1080
  "ref": "workflow.speed_to_lead",
1081
+ "allowMultiple": false,
1068
1082
  "name": "Speed to Lead",
1069
1083
  "trigger": {
1070
1084
  "type": "form_submission",
@@ -1150,6 +1164,7 @@
1150
1164
  },
1151
1165
  {
1152
1166
  "ref": "workflow.missed_call",
1167
+ "allowMultiple": true,
1153
1168
  "name": "Missed Call Text-Back",
1154
1169
  "conditionalOn": "channels.sms == true",
1155
1170
  "trigger": {
@@ -1204,13 +1219,14 @@
1204
1219
  },
1205
1220
  {
1206
1221
  "ref": "workflow.lead_nurture",
1222
+ "allowMultiple": false,
1207
1223
  "name": "Lead Nurture (30 days)",
1208
1224
  "trigger": {
1209
1225
  "type": "contact_tag",
1210
1226
  "tagRef": "tag.nurture_start"
1211
1227
  },
1212
1228
  "stopOnResponse": true,
1213
- "_design": "Starts from the Speed to Lead hand-off tag. 11 touches (6 emails + 5 texts) across 31 days of waits: give, give, ask. Removes its own trigger tag first so a later hand-off can start it again. Ends by tagging lapsed and handing off to Win-back. Any reply stops it (stopOnResponse) and Replied - Stop & Route takes over.",
1229
+ "_design": "Starts from the Speed to Lead hand-off tag. 11 touches (6 emails + 5 texts) across 31 days of waits: give, give, ask. Removes its own trigger tag first; each contact goes through it once (re-entry off). Ends by tagging lapsed and handing off to Win-back. Any reply stops it (stopOnResponse) and Replied - Stop & Route takes over.",
1214
1230
  "actions": [
1215
1231
  {
1216
1232
  "type": "remove_contact_tag",
@@ -1345,6 +1361,7 @@
1345
1361
  },
1346
1362
  {
1347
1363
  "ref": "workflow.reply_route",
1364
+ "allowMultiple": true,
1348
1365
  "name": "Replied - Stop & Route",
1349
1366
  "trigger": {
1350
1367
  "type": "customer_reply"
@@ -1399,6 +1416,7 @@
1399
1416
  },
1400
1417
  {
1401
1418
  "ref": "workflow.win_back",
1419
+ "allowMultiple": false,
1402
1420
  "name": "Win-back (30 days)",
1403
1421
  "trigger": {
1404
1422
  "type": "contact_tag",
@@ -1470,6 +1488,7 @@
1470
1488
  },
1471
1489
  {
1472
1490
  "ref": "workflow.appointment_booked",
1491
+ "allowMultiple": true,
1473
1492
  "name": "Visit Booked - Confirm & Move",
1474
1493
  "trigger": {
1475
1494
  "type": "appointment",
@@ -1491,6 +1510,15 @@
1491
1510
  "type": "remove_from_workflow",
1492
1511
  "workflowRef": "workflow.win_back"
1493
1512
  },
1513
+ {
1514
+ "type": "remove_from_workflow",
1515
+ "workflowRef": "workflow.no_show"
1516
+ },
1517
+ {
1518
+ "type": "remove_from_workflow",
1519
+ "workflowRef": "workflow.missed_call",
1520
+ "conditionalOn": "channels.sms == true"
1521
+ },
1494
1522
  {
1495
1523
  "type": "add_contact_tag",
1496
1524
  "tagRef": "tag.lc_booked"
@@ -1534,6 +1562,7 @@
1534
1562
  },
1535
1563
  {
1536
1564
  "ref": "workflow.appointment_reminders",
1565
+ "allowMultiple": true,
1537
1566
  "name": "Visit Reminders",
1538
1567
  "trigger": {
1539
1568
  "type": "appointment",
@@ -1572,6 +1601,8 @@
1572
1601
  },
1573
1602
  {
1574
1603
  "ref": "workflow.no_show",
1604
+ "allowMultiple": true,
1605
+ "stagePreference": { "stage.contacted": ["no show", "noshow", "missed appointment", "did not show"] },
1575
1606
  "name": "No-Show Rescue",
1576
1607
  "trigger": {
1577
1608
  "type": "appointment",
@@ -1660,6 +1691,7 @@
1660
1691
  },
1661
1692
  {
1662
1693
  "ref": "workflow.quote_follow_up",
1694
+ "allowMultiple": true,
1663
1695
  "name": "Quote Follow-up",
1664
1696
  "trigger": {
1665
1697
  "type": "contact_tag",
@@ -1721,24 +1753,44 @@
1721
1753
  },
1722
1754
  {
1723
1755
  "ref": "workflow.review_request",
1756
+ "allowMultiple": false,
1724
1757
  "name": "Review Request",
1725
- "trigger": {
1726
- "type": "appointment",
1727
- "appointmentStatus": "showed",
1728
- "calendarRef": "calendar.new_patient"
1729
- },
1758
+ "trigger": [
1759
+ {
1760
+ "conditionalOn": "goal.reviewTiming == 'When you mark the work done'",
1761
+ "type": "contact_tag",
1762
+ "tagRef": "tag.service_done"
1763
+ },
1764
+ {
1765
+ "type": "appointment",
1766
+ "appointmentStatus": "showed",
1767
+ "calendarRef": "calendar.new_patient"
1768
+ }
1769
+ ],
1730
1770
  "stopOnResponse": true,
1731
1771
  "conditionalOn": "goal.bookingNeeded == true",
1732
- "_design": "On a completed visit (status showed): tag, wait two hours, text the review link, then an email three days later if nothing came back. Any reply stops it. The review link is a custom value the operator pastes once (handoff.review_link). Closes the Layer 4 gap named on the hub: review requests had no build feature (2026-09-14).",
1772
+ "_design": "On a completed visit (status showed): tag, wait two hours, text the review link, then an email three days later if nothing came back. Any reply stops it. The review link is a custom value the operator pastes once (handoff.review_link). Closes the Layer 4 gap named on the hub: review requests had no build feature (2026-09-14). The intake's review timing question (goal.reviewTiming, 2026-09-29) can make it the day after the visit instead, or start it from the service-complete tag the team adds when the work is done.",
1733
1773
  "actions": [
1774
+ {
1775
+ "type": "remove_contact_tag",
1776
+ "tagRef": "tag.service_done",
1777
+ "conditionalOn": "goal.reviewTiming == 'When you mark the work done'"
1778
+ },
1734
1779
  {
1735
1780
  "type": "add_contact_tag",
1736
1781
  "tagRef": "tag.review_requested"
1737
1782
  },
1783
+ {
1784
+ "type": "wait",
1785
+ "value": 1,
1786
+ "unit": "days",
1787
+ "conditionalOn": "goal.reviewTiming == 'The day after a visit marked showed'"
1788
+ },
1738
1789
  {
1739
1790
  "type": "wait",
1740
1791
  "value": 2,
1741
- "unit": "hours"
1792
+ "unit": "hours",
1793
+ "conditionalOn": "goal.reviewTiming != 'The day after a visit marked showed'"
1742
1794
  },
1743
1795
  {
1744
1796
  "type": "send_sms",
@@ -1764,7 +1816,7 @@
1764
1816
  "owner": "OPERATOR-UI",
1765
1817
  "title": "Add the people who get lead alerts and call tasks",
1766
1818
  "trigger": "the brief listed no staff",
1767
- "instruction": "In the sub-account go to Settings > My Staff and add at least one user (yourself is fine): name, email, role. Every \"alert a real person\" and \"call task\" step in the workflows is already built and waiting; the moment a user exists, open each workflow and pick them in the Notify and Task steps (or re-run the build with useSavedPlan:true so the pending steps are re-pointed).",
1819
+ "instruction": "In the sub-account go to Settings → My Staff (called Users in newer accounts) → + Add User and add at least one person (yourself is fine): enter their name and the same email address listed in the intake. Every \"alert a real person\" and \"call task\" step in the workflows is already built and waiting; the moment a user exists, open each workflow and pick them in the Notify and Task steps (or re-run the build with useSavedPlan:true so the pending steps are re-pointed).",
1768
1820
  "produces": null,
1769
1821
  "successCheck": "At least one user exists and no workflow step still shows \"user pending\"",
1770
1822
  "blocks": [
@@ -1880,7 +1932,7 @@
1880
1932
  "owner": "TEAM",
1881
1933
  "title": "Read every template once for your compliance rules",
1882
1934
  "trigger": "always, before publishing",
1883
- "instruction": "Open Marketing > Emails > Templates and Conversations > Snippets. The copy never names a condition, diagnosis, treatment or medication; confirm your edits keep it that way and match the compliance notes you gave at intake (HIPAA, state advertising rules, insurance language). Ten minutes now saves a complaint later.",
1935
+ "instruction": "Open Marketing > Emails > Templates and Conversations > Snippets. The copy never names a condition, diagnosis, treatment or medication; confirm your edits keep it that way and match the compliance notes you gave at intake (privacy, state advertising rules, insurance language). Ten minutes now saves a complaint later.",
1884
1936
  "produces": null,
1885
1937
  "successCheck": "Every template read by a responsible person; no clinical detail in any text or email",
1886
1938
  "blocks": [
@@ -1942,6 +1994,15 @@
1942
1994
  "cv.review_link"
1943
1995
  ]
1944
1996
  },
1997
+ {
1998
+ "ref": "handoff.review_tag",
1999
+ "conditionalOn": "goal.bookingNeeded == true && goal.reviewTiming == 'When you mark the work done'",
2000
+ "owner": "OPERATOR-UI",
2001
+ "title": "Start Review Request by tagging service-complete when the work is done",
2002
+ "trigger": "always (1 minute to read)",
2003
+ "instruction": "The intake chose to ask for a review when the work is done, so Review Request starts from the tag service-complete, not from a visit. Add that tag on the contact when their care is finished. Each patient is asked for a review once.",
2004
+ "successCheck": "A contact tagged service-complete receives the review text two hours later"
2005
+ },
1945
2006
  {
1946
2007
  "ref": "handoff.quote_tag",
1947
2008
  "owner": "OPERATOR-UI",