@elitedcs/ghl-mcp 3.64.1 → 3.65.1
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/CHANGELOG.md +74 -0
- package/README.md +5 -5
- package/dist/index.js +291 -14
- package/guide/guide.html +36 -10
- package/package.json +2 -2
- package/templates/action-schemas.json +27 -15
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,79 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 3.65.1 — the guide caught up with the product
|
|
4
|
+
|
|
5
|
+
No code changes. The built-in user guide ships inside the install, so it goes
|
|
6
|
+
out as its own release.
|
|
7
|
+
|
|
8
|
+
- **Fix: the booking-calendar guide was telling you to do something by hand that
|
|
9
|
+
the product now does.** It said minimum booking notice lived on GHL's calendar
|
|
10
|
+
screen "and not here", and sent you there to set it. 3.65.0 made it settable
|
|
11
|
+
directly. Anyone following that page yesterday was sent clicking through GHL
|
|
12
|
+
for no reason.
|
|
13
|
+
- **Booking calendars**: daily caps and minimum notice are now in the guide, in
|
|
14
|
+
the same ask that sets your hours and slot length. Plus what to do if an older
|
|
15
|
+
version reset your slot lengths or opening hours when you changed something
|
|
16
|
+
else.
|
|
17
|
+
- **Nurture sequences**: quiet hours. What they are, why every workflow that
|
|
18
|
+
texts needs its own, and the one thing to go check: if you set quiet hours
|
|
19
|
+
before 3.65.0, they were dropped the next time that workflow was saved. Open
|
|
20
|
+
anything that texts and set them once more.
|
|
21
|
+
- **Contact lists**: how to save a group as a list you can reopen, rather than
|
|
22
|
+
rebuilding the same filter every Monday. These tools never worked before
|
|
23
|
+
3.65.0, so if you tried this and found nothing in GHL, that is why.
|
|
24
|
+
- **Blueprint**: how appointment, reply, missed-call and number-check triggers
|
|
25
|
+
get scoped to the thing you actually meant, and why a stage move needs to find
|
|
26
|
+
its deal first.
|
|
27
|
+
|
|
28
|
+
Every prompt in the guide is bound to a run that actually happened in a real
|
|
29
|
+
sub-account. The three new ones were proven live before this shipped.
|
|
30
|
+
|
|
31
|
+
## 3.65.0 — nine calls that reported success and did the wrong thing
|
|
32
|
+
|
|
33
|
+
Found while building a 139-requirement home-care front office end to end. Every
|
|
34
|
+
fix below has the same shape: the call succeeds, the response reads correctly,
|
|
35
|
+
and something quietly did not happen. None raised an error. Four were findable
|
|
36
|
+
only by firing the system and watching, not by reading settings back.
|
|
37
|
+
|
|
38
|
+
**Three of these were damaging live accounts.**
|
|
39
|
+
|
|
40
|
+
- **Fix: saving a calendar wiped its own settings.** Updating one field on a
|
|
41
|
+
calendar sent only that field, and GoHighLevel treated the omissions as
|
|
42
|
+
deletions — slot duration, slot interval, buffers and the entire open-hours
|
|
43
|
+
grid were erased. The call returned 200 and the calendar looked fine in the
|
|
44
|
+
list. Calendar updates now read the existing record first and merge; if that
|
|
45
|
+
read fails the update refuses rather than falling back to a write that would
|
|
46
|
+
clobber.
|
|
47
|
+
- **Fix: saving a workflow silently deleted its quiet hours.** The update never
|
|
48
|
+
sent the sending-window field, so any workflow with contact-hour restrictions
|
|
49
|
+
lost them on the next save, with no warning. For anyone using quiet hours for
|
|
50
|
+
TCPA compliance, the protection disappeared the first time the workflow was
|
|
51
|
+
touched.
|
|
52
|
+
- **Fix: every smart-list tool pointed at the wrong service.** All five called an
|
|
53
|
+
endpoint that does not serve contact smart lists, so they never worked.
|
|
54
|
+
|
|
55
|
+
Silent no-ops:
|
|
56
|
+
|
|
57
|
+
- **Fix: appointment triggers ignored the calendar you chose** and fired for every
|
|
58
|
+
calendar in the account. A caregiver interview booking would start the family
|
|
59
|
+
consultation sequence.
|
|
60
|
+
- **Fix: reply triggers fired on every inbound message**, because the conditions
|
|
61
|
+
were emitted empty.
|
|
62
|
+
- **Fix: moving a deal to a new stage did nothing** unless the workflow had first
|
|
63
|
+
looked the opportunity up. The step reported success at every save. Now caught
|
|
64
|
+
before the build runs.
|
|
65
|
+
|
|
66
|
+
New:
|
|
67
|
+
|
|
68
|
+
- **`create_contact_smart_list`, `get_contact_smart_list`, `delete_contact_smart_list`.**
|
|
69
|
+
238 tools total, 110 free.
|
|
70
|
+
- **Calendar per-day limits and minimum booking notice** are now reachable
|
|
71
|
+
(`appointmentPerDay`, `allowBookingAfter`, `allowBookingAfterUnit`).
|
|
72
|
+
- **Workflow quiet hours** can now be set through the tool — a sending window
|
|
73
|
+
with days, applied per workflow.
|
|
74
|
+
- **Four trigger builders**: calendar-scoped appointments, reply-intent routing,
|
|
75
|
+
call status, and phone-number validation failures.
|
|
76
|
+
|
|
3
77
|
## 3.64.1 — three more calls that failed quietly
|
|
4
78
|
|
|
5
79
|
- **Fix: client pricing was mangled on the way into a build plan.** The Blueprint
|
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# GHL Command — GoHighLevel MCP Server
|
|
2
2
|
|
|
3
|
-
**Full GoHighLevel API access for Claude.**
|
|
3
|
+
**Full GoHighLevel API access for Claude.** 238 tools across 50 modules — manage contacts, conversations, pipelines, calendars, funnels, workflows, invoices, custom objects, webhooks, and more. **Includes full workflow builder, funnel/page editor, form builder, pipeline builder, bulk operations, account export, and workflow cloning** — capabilities no other GHL tool offers. **Multi-tenant:** one install can run the workflow builder across multiple clients' GHL accounts.
|
|
4
4
|
|
|
5
5
|
**Distributed via npm as [`@elitedcs/ghl-mcp`](https://www.npmjs.com/package/@elitedcs/ghl-mcp).** Buyers install with one config block — no git, no Node.js setup, no terminal commands. Updates flow automatically (`npx @latest` re-resolves on every Claude restart).
|
|
6
6
|
|
|
@@ -8,7 +8,7 @@ Works with **both the Claude Desktop App and Claude Code terminal** — your cho
|
|
|
8
8
|
|
|
9
9
|
**License required.** Get it at [ghlcommand.com](https://ghlcommand.com?utm_source=npm&utm_medium=readme) — $97/mo, covering UNLIMITED GoHighLevel sub-accounts on up to 3 machines (never billed per account). **30-day time-back guarantee**: save 5+ hours on one client build or your first month back.
|
|
10
10
|
|
|
11
|
-
**Try it FREE (read-only tier).** Grab an instant free key at [ghlcommand.com/free](https://ghlcommand.com/free?utm_source=npm&utm_medium=readme) —
|
|
11
|
+
**Try it FREE (read-only tier).** Grab an instant free key at [ghlcommand.com/free](https://ghlcommand.com/free?utm_source=npm&utm_medium=readme) — 110 read-only tools on your own GHL account once the two-step install is finished (the same install as the paid tier — upgrading later is just a license swap): the account-wide workflow audit (`audit_workflows` finds the silently broken references GHL never warns you about), account health, and every read across contacts, pipelines, funnels, and workflows. Write tools stay visible and answer with upgrade info. Buying later with the same email upgrades your key in place — 1 machine on free, 3 when you upgrade.
|
|
12
12
|
|
|
13
13
|
**How this differs from HighLevel's official MCP.** HighLevel ships an official Anthropic MCP that reads and writes your CRM (contacts, conversations, calendars, opportunities, payments, social) over the public API — it's free and it's good; use it for that. What it *cannot* do, because the public API doesn't expose it: **build or edit workflows, build funnels/landing pages, or build forms.** That's exactly what GHL Command does — the programmatic Workflow Builder, funnel/page builder, form builder, deep workflow cloning, the silent-failure `audit_workflows`, and Blueprint (build a whole client account from one intake — ships as a guided skill that **installs itself automatically** with this package; nothing to download), all on GHL's internal APIs. **Official MCP = read/write your CRM. GHL Command = build and fix it.** They stack fine together.
|
|
14
14
|
|
|
@@ -135,7 +135,7 @@ Run setup_ghl_mcp to activate GHL Command:
|
|
|
135
135
|
ghl_location_id: YOUR_LOCATION_ID
|
|
136
136
|
```
|
|
137
137
|
|
|
138
|
-
Approve the tool call. Server validates your license, verifies your GHL credentials, writes them to a per-user config file. **Quit Claude one more time and reopen** — the full core toolset is now unlocked (
|
|
138
|
+
Approve the tool call. Server validates your license, verifies your GHL credentials, writes them to a per-user config file. **Quit Claude one more time and reopen** — the full core toolset is now unlocked (238 tools total with the optional Workflow Builder Firebase add-on).
|
|
139
139
|
|
|
140
140
|
### 4. Try it
|
|
141
141
|
|
|
@@ -518,7 +518,7 @@ Highlights (full guide: [`docs/HEADLESS.md`](docs/HEADLESS.md)):
|
|
|
518
518
|
|---|---|
|
|
519
519
|
| `get_mcp_version` | Check installed version against the latest published to npm. Confirms an upgrade landed after restarting Claude. Available even before GHL credentials are configured. |
|
|
520
520
|
| `health_check` | Run a full health check: npm registry + version status, GHL API key validity, default location reachability, Firebase auth status, token registry presence. Use when something feels broken. |
|
|
521
|
-
| `enable_workflow_builder` | Add Firebase credentials to an existing install to unlock
|
|
521
|
+
| `enable_workflow_builder` | Add Firebase credentials to an existing install to unlock 55 additional Firebase-gated tools (workflow builder, funnel builder, form builder, pipeline builder, workflow cloner, smart lists, reputation, email campaigns, email templates, memberships, validate_workflow). No need to re-enter license / API key / location ID. Run this any time after the basic setup, on the buyer's schedule. |
|
|
522
522
|
|
|
523
523
|
### Other Modules
|
|
524
524
|
|
|
@@ -798,7 +798,7 @@ Source repo is private. Contributors need an invitation from `drjerryrelth`. The
|
|
|
798
798
|
|
|
799
799
|
### Reducing context / token usage
|
|
800
800
|
|
|
801
|
-
Every registered MCP tool's schema is shipped to the model on every message. With
|
|
801
|
+
Every registered MCP tool's schema is shipped to the model on every message. With 238 tools that's a meaningful per-message context cost even in chats that never touch GHL. If you only use a slice of GHL Command, restrict the tool surface with `GHL_ENABLED_MODULES` and/or `GHL_ENABLED_TOOLS`:
|
|
802
802
|
|
|
803
803
|
```jsonc
|
|
804
804
|
// Claude Desktop config — enable whole modules
|
package/dist/index.js
CHANGED
|
@@ -1648,7 +1648,7 @@ Note: Firebase credentials rejected (${fb.error}).`;
|
|
|
1648
1648
|
const telemetryLine = telemetryDisabled(process.env) ? "" : `
|
|
1649
1649
|
|
|
1650
1650
|
${TELEMETRY_DISCLOSURE}`;
|
|
1651
|
-
const finishedCount = isFree ? "
|
|
1651
|
+
const finishedCount = isFree ? "110" : "238";
|
|
1652
1652
|
const freeTip = isFree ? `
|
|
1653
1653
|
|
|
1654
1654
|
Free tier: read-only. Write tools stay visible but answer with upgrade info instead of acting. Full version ($97/mo founding rate) upgrades in place \u2014 same install, you only swap the license key: https://ghlcommand.com` : "";
|
|
@@ -1693,7 +1693,7 @@ Free tier: read-only. Write tools stay visible but answer with upgrade info inst
|
|
|
1693
1693
|
function registerEnableWorkflowBuilderTool(server2) {
|
|
1694
1694
|
server2.tool(
|
|
1695
1695
|
"enable_workflow_builder",
|
|
1696
|
-
"Add Firebase credentials to an existing GHL Command install to unlock
|
|
1696
|
+
"Add Firebase credentials to an existing GHL Command install to unlock 55 additional tools across the internal-API modules: workflow builder (create/edit/clone/delete/publish/validate workflows, build_if_else_branch, build_goal_event, get_trigger_registry), funnel + page builder, form builder, pipeline builder, workflow cloner, smart lists, reputation, email campaigns, email templates, and memberships, plus the pre-deploy validator. On the FREE tier this same login unlocks the read-only auditor suite (audit_workflows, validate_workflow, full-detail workflow/funnel/pipeline reads). Requires you've already run setup_ghl_mcp. EASIEST PATH: run `capture_firebase_interactive` instead \u2014 a Chrome window opens, you log into GHL, zero pasting. Use THIS tool when you have JSON from `auto_capture_firebase_script` (console-paste path) to put in `firebase_paste`, or the three manual DevTools fields. Tool count goes from 183 to 238 after the next Claude restart.",
|
|
1697
1697
|
{
|
|
1698
1698
|
// v3.25.0: one-paste path. Tool runs `auto_capture_firebase_script` to
|
|
1699
1699
|
// get the console script; the script returns a JSON object that pastes
|
|
@@ -1774,7 +1774,7 @@ DevTools steps: https://elitedcs.com/ghl-mcp-firebase`
|
|
|
1774
1774
|
"",
|
|
1775
1775
|
"**You MUST restart Claude before using any workflow-builder tool.** Quit Claude completely (Cmd+Q on Mac, full exit on Windows) and reopen. Without a restart, the workflow builder tools will keep using the OLD Firebase auth from before this call and fail with 401 errors \u2014 even though this tool reported success.",
|
|
1776
1776
|
"",
|
|
1777
|
-
'After restart, all
|
|
1777
|
+
'After restart, all 238 tools load. Try: "List my workflows in full detail" or "Validate workflow <id>".',
|
|
1778
1778
|
"",
|
|
1779
1779
|
"Note: Firebase refresh tokens rotate every few weeks. If workflow tools stop working in a few weeks (run `health_check` to confirm Firebase auth: FAIL), run `auto_capture_firebase_script` for fresh values and re-run this tool with the new firebase_paste."
|
|
1780
1780
|
].join("\n")
|
|
@@ -1870,7 +1870,7 @@ The login in the capture window may belong to the wrong GHL account, or the sess
|
|
|
1870
1870
|
"",
|
|
1871
1871
|
"**You MUST restart Claude before using any workflow-builder tool.** Quit Claude completely (Cmd+Q on Mac, full exit on Windows) and reopen.",
|
|
1872
1872
|
"",
|
|
1873
|
-
'After restart, all
|
|
1873
|
+
'After restart, all 238 tools load. Try: "List my workflows in full detail".',
|
|
1874
1874
|
"",
|
|
1875
1875
|
"Future token rotations re-capture silently \u2014 if workflow tools ever 401, just run capture_firebase_interactive again; no window should appear."
|
|
1876
1876
|
].join("\n")
|
|
@@ -2854,9 +2854,9 @@ var require_package = __commonJS({
|
|
|
2854
2854
|
"package.json"(exports2, module2) {
|
|
2855
2855
|
module2.exports = {
|
|
2856
2856
|
name: "@elitedcs/ghl-mcp",
|
|
2857
|
-
version: "3.
|
|
2857
|
+
version: "3.65.1",
|
|
2858
2858
|
mcpName: "io.github.drjerryrelth/ghl-command",
|
|
2859
|
-
description: "GoHighLevel MCP Server for Claude.
|
|
2859
|
+
description: "GoHighLevel MCP Server for Claude. 238 tools \u2014 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.",
|
|
2860
2860
|
main: "dist/index.js",
|
|
2861
2861
|
bin: {
|
|
2862
2862
|
"ghl-mcp": "dist/index.js"
|
|
@@ -6271,6 +6271,9 @@ ${errorBody}`
|
|
|
6271
6271
|
allowMultipleOpportunity: updates.allowMultipleOpportunity ?? current.allowMultipleOpportunity ?? false,
|
|
6272
6272
|
autoMarkAsRead: updates.autoMarkAsRead ?? current.autoMarkAsRead ?? false,
|
|
6273
6273
|
removeContactFromLastStep: updates.removeContactFromLastStep ?? current.removeContactFromLastStep ?? true,
|
|
6274
|
+
// Preserve unless explicitly set (including an explicit null to clear).
|
|
6275
|
+
// `?? ` would be wrong here — null is a meaningful value.
|
|
6276
|
+
window: updates.window !== void 0 ? updates.window : current.window ?? null,
|
|
6274
6277
|
workflowData: { templates: linkedActions },
|
|
6275
6278
|
updatedBy: this.userId,
|
|
6276
6279
|
// Triggers live in Firestore and are managed out-of-band; the workflow
|
|
@@ -7593,6 +7596,9 @@ function applyCalendarFields(body, args) {
|
|
|
7593
7596
|
if (args.slotBuffer !== void 0) body.slotBuffer = args.slotBuffer;
|
|
7594
7597
|
if (args.slotInterval !== void 0) body.slotInterval = args.slotInterval;
|
|
7595
7598
|
if (args.appointmentPerSlot !== void 0) body.appointmentPerSlot = args.appointmentPerSlot;
|
|
7599
|
+
if (args.appointmentPerDay !== void 0) body.appointmentPerDay = args.appointmentPerDay;
|
|
7600
|
+
if (args.allowBookingAfter !== void 0) body.allowBookingAfter = args.allowBookingAfter;
|
|
7601
|
+
if (args.allowBookingAfterUnit !== void 0) body.allowBookingAfterUnit = args.allowBookingAfterUnit;
|
|
7596
7602
|
if (args.openHours !== void 0) {
|
|
7597
7603
|
body.openHours = expandOpenHours(args.openHours);
|
|
7598
7604
|
body.availabilityType = args.availabilityType ?? 0;
|
|
@@ -7614,6 +7620,30 @@ function buildUpdateCalendarBody(args) {
|
|
|
7614
7620
|
applyCalendarFields(body, args);
|
|
7615
7621
|
return body;
|
|
7616
7622
|
}
|
|
7623
|
+
var MERGE_PRESERVED_FIELDS = [
|
|
7624
|
+
"slotDuration",
|
|
7625
|
+
"slotInterval",
|
|
7626
|
+
"slotBuffer",
|
|
7627
|
+
"appointmentPerSlot",
|
|
7628
|
+
"appointmentPerDay",
|
|
7629
|
+
"allowBookingAfter",
|
|
7630
|
+
"allowBookingAfterUnit",
|
|
7631
|
+
"availabilityType"
|
|
7632
|
+
];
|
|
7633
|
+
function mergeCalendarUpdate(existing, args) {
|
|
7634
|
+
if (!existing) return args;
|
|
7635
|
+
const merged = { ...args };
|
|
7636
|
+
for (const key of MERGE_PRESERVED_FIELDS) {
|
|
7637
|
+
if (merged[key] === void 0 && existing[key] !== void 0 && existing[key] !== "") {
|
|
7638
|
+
merged[key] = existing[key];
|
|
7639
|
+
}
|
|
7640
|
+
}
|
|
7641
|
+
if (merged.openHours === void 0) {
|
|
7642
|
+
const hours = existing.openHours;
|
|
7643
|
+
if (Array.isArray(hours) && hours.length > 0) merged.openHours = hours;
|
|
7644
|
+
}
|
|
7645
|
+
return merged;
|
|
7646
|
+
}
|
|
7617
7647
|
function tzOffsetMillis(utcMillis, timeZone) {
|
|
7618
7648
|
const dtf = new Intl.DateTimeFormat("en-US", {
|
|
7619
7649
|
timeZone,
|
|
@@ -7724,6 +7754,9 @@ function registerCalendarTools(server2, client) {
|
|
|
7724
7754
|
slotBuffer: import_zod12.z.number().optional().describe("Buffer time between slots in minutes"),
|
|
7725
7755
|
slotInterval: import_zod12.z.number().optional().describe("Slot interval in minutes"),
|
|
7726
7756
|
appointmentPerSlot: import_zod12.z.number().optional().describe("Max appointments per slot"),
|
|
7757
|
+
appointmentPerDay: import_zod12.z.number().optional().describe("Max appointments bookable per DAY on this calendar (the daily ceiling, distinct from appointmentPerSlot). Without it the day's capacity is whatever the open hours allow."),
|
|
7758
|
+
allowBookingAfter: import_zod12.z.number().optional().describe("Minimum advance notice before a slot can be booked, in allowBookingAfterUnit. E.g. 24 with unit 'hours' blocks same-day and short-notice bookings."),
|
|
7759
|
+
allowBookingAfterUnit: import_zod12.z.string().optional().describe("Unit for allowBookingAfter: 'minutes' | 'hours' | 'days'. Send alongside allowBookingAfter."),
|
|
7727
7760
|
openHours: import_zod12.z.array(OpenHoursBlockSchema).optional().describe(
|
|
7728
7761
|
"Weekly business hours that make slots bookable (the 'assign availability hours' step). Each entry maps weekdays (0=Sun \u2026 6=Sat) to one or more open windows on a 24-hour clock; multi-day entries are auto-expanded to GHL's required one-block-per-day form. Example Mon-Fri 9-6 + Sat 10-1: [{daysOfTheWeek:[1,2,3,4,5],hours:[{openHour:9,openMinute:0,closeHour:18,closeMinute:0}]},{daysOfTheWeek:[6],hours:[{openHour:10,openMinute:0,closeHour:13,closeMinute:0}]}]. Behavior (verified live): on event/simple calendars these hours are the sole source of availability. On round_robin calendars the bookable slots are the INTERSECTION of these hours and each assigned user's working hours, so a day only opens if a team member is also available then (e.g. a Saturday window needs a user with Saturday availability). Omit to use GHL's defaults."
|
|
7729
7762
|
),
|
|
@@ -7753,13 +7786,26 @@ function registerCalendarTools(server2, client) {
|
|
|
7753
7786
|
slotBuffer: import_zod12.z.number().optional().describe("Buffer time between slots in minutes"),
|
|
7754
7787
|
slotInterval: import_zod12.z.number().optional().describe("Slot interval in minutes"),
|
|
7755
7788
|
appointmentPerSlot: import_zod12.z.number().optional().describe("Max appointments per slot"),
|
|
7789
|
+
appointmentPerDay: import_zod12.z.number().optional().describe("Max appointments bookable per DAY on this calendar (the daily ceiling, distinct from appointmentPerSlot)."),
|
|
7790
|
+
allowBookingAfter: import_zod12.z.number().optional().describe("Minimum advance notice before a slot can be booked, in allowBookingAfterUnit. E.g. 24 with unit 'hours' blocks same-day and short-notice bookings."),
|
|
7791
|
+
allowBookingAfterUnit: import_zod12.z.string().optional().describe("Unit for allowBookingAfter: 'minutes' | 'hours' | 'days'. Send alongside allowBookingAfter."),
|
|
7756
7792
|
openHours: import_zod12.z.array(OpenHoursBlockSchema).optional().describe(
|
|
7757
7793
|
"Weekly business hours that make slots bookable. Each entry maps weekdays (0=Sun \u2026 6=Sat) to one or more open windows on a 24-hour clock; multi-day entries are auto-expanded to GHL's required one-block-per-day form (e.g. [{daysOfTheWeek:[1,2,3,4,5],hours:[{openHour:9,openMinute:0,closeHour:18,closeMinute:0}]}] = Mon-Fri 9am-6pm). Replaces the calendar's existing hours. Behavior (verified live): event/simple calendars use these hours directly; round_robin calendars intersect them with each assigned user's working hours (a day only opens if a team member is available then)."
|
|
7758
7794
|
),
|
|
7759
7795
|
availabilityType: import_zod12.z.number().int().optional().describe("Availability mode. 0 = standard weekly hours (from openHours); auto-set when you pass openHours. Leave unset otherwise.")
|
|
7760
7796
|
},
|
|
7761
7797
|
async (args) => {
|
|
7762
|
-
|
|
7798
|
+
let existing;
|
|
7799
|
+
try {
|
|
7800
|
+
const current = await client.get(`/calendars/${args.calendarId}`);
|
|
7801
|
+
const cal = current?.calendar ?? current;
|
|
7802
|
+
if (cal && typeof cal === "object") existing = cal;
|
|
7803
|
+
} catch (e) {
|
|
7804
|
+
throw new Error(
|
|
7805
|
+
`update_calendar: could not read calendar ${args.calendarId} before updating it (${e instanceof Error ? e.message : String(e)}). Refusing to write: GHL's calendar update does not merge, so a partial write here would silently reset this calendar's slot length, buffer and availability hours.`
|
|
7806
|
+
);
|
|
7807
|
+
}
|
|
7808
|
+
const body = buildUpdateCalendarBody(mergeCalendarUpdate(existing, args));
|
|
7763
7809
|
return await client.put(`/calendars/${args.calendarId}`, { body });
|
|
7764
7810
|
}
|
|
7765
7811
|
);
|
|
@@ -10380,9 +10426,17 @@ function registerWorkflowBuilderTools(server2, client) {
|
|
|
10380
10426
|
stopOnResponse: import_zod36.z.boolean().optional().describe("Stop the workflow when the contact replies. If omitted, the current value is preserved."),
|
|
10381
10427
|
autoMarkAsRead: import_zod36.z.boolean().optional().describe("Auto-mark conversations as read. If omitted, the current value is preserved."),
|
|
10382
10428
|
removeContactFromLastStep: import_zod36.z.boolean().optional().describe("Remove the contact when they reach the last step. If omitted, the current value is preserved."),
|
|
10383
|
-
allowMultipleOpportunity: import_zod36.z.boolean().optional().describe("Allow creating multiple opportunities. If omitted, the current value is preserved.")
|
|
10429
|
+
allowMultipleOpportunity: import_zod36.z.boolean().optional().describe("Allow creating multiple opportunities. If omitted, the current value is preserved."),
|
|
10430
|
+
window: import_zod36.z.object({
|
|
10431
|
+
condition: import_zod36.z.string().describe('Usually "when" \u2014 run only DURING the window.'),
|
|
10432
|
+
start: import_zod36.z.string().describe('Start of the window, 24-hour "HH:MM" (e.g. "08:00").'),
|
|
10433
|
+
end: import_zod36.z.string().describe('End of the window, 24-hour "HH:MM" (e.g. "21:00").'),
|
|
10434
|
+
days: import_zod36.z.array(import_zod36.z.number().int().min(0).max(6)).describe("Days the window applies to. 0=Sunday \u2026 6=Saturday.")
|
|
10435
|
+
}).nullable().optional().describe(
|
|
10436
|
+
`EXECUTION WINDOW \u2014 the hours this workflow is allowed to run. This is where quiet hours / TCPA contact-hour restrictions are enforced: there is NO time-of-day setting inside the Voice AI or Conversation AI builders, so a "never text before 8am or after 9pm" rule has to live here, on every workflow that sends SMS. Example: {condition:"when", start:"08:00", end:"21:00", days:[0,1,2,3,4,5,6]}. Pass null to clear. OMIT to preserve the workflow's existing window (it was previously dropped from the payload entirely, so saving a workflow silently wiped its window).`
|
|
10437
|
+
)
|
|
10384
10438
|
},
|
|
10385
|
-
async ({ workflowId, name, status, actions, triggers, allowMultiple, stopOnResponse, autoMarkAsRead, removeContactFromLastStep, allowMultipleOpportunity }) => {
|
|
10439
|
+
async ({ workflowId, name, status, actions, triggers, allowMultiple, stopOnResponse, autoMarkAsRead, removeContactFromLastStep, allowMultipleOpportunity, window }) => {
|
|
10386
10440
|
try {
|
|
10387
10441
|
const result = await client.updateWorkflow(workflowId, {
|
|
10388
10442
|
name,
|
|
@@ -10393,7 +10447,8 @@ function registerWorkflowBuilderTools(server2, client) {
|
|
|
10393
10447
|
stopOnResponse,
|
|
10394
10448
|
autoMarkAsRead,
|
|
10395
10449
|
removeContactFromLastStep,
|
|
10396
|
-
allowMultipleOpportunity
|
|
10450
|
+
allowMultipleOpportunity,
|
|
10451
|
+
window
|
|
10397
10452
|
});
|
|
10398
10453
|
return jsonResponse(result);
|
|
10399
10454
|
} catch (error) {
|
|
@@ -13681,6 +13736,33 @@ function registerWorkflowClonerTools(server2, builderClient) {
|
|
|
13681
13736
|
var import_zod48 = require("zod");
|
|
13682
13737
|
var SMARTLIST_BASE = "https://backend.leadconnectorhq.com/lists/dynamic";
|
|
13683
13738
|
var OBJECT_KEYS = ["contacts", "opportunity"];
|
|
13739
|
+
var CONTACT_SMARTLIST_BASE = "https://services.leadconnectorhq.com/contacts/smartlist";
|
|
13740
|
+
var CONTACT_SMARTLIST_DELETE = "https://api.leadconnectorhq.com/smartlist/delete";
|
|
13741
|
+
var DEFAULT_CONTACT_SMARTLIST_COLUMNS = [
|
|
13742
|
+
{ key: "name", value: "name", order: 0 },
|
|
13743
|
+
{ key: "phone", value: "phone", order: 1 },
|
|
13744
|
+
{ key: "email", value: "email", order: 2 },
|
|
13745
|
+
{ key: "companyName", value: "companyName", order: 3 },
|
|
13746
|
+
{ key: "dateAdded", value: "dateAdded", order: 4 },
|
|
13747
|
+
{ key: "lastActivity", value: "lastActivity", order: 5 },
|
|
13748
|
+
{ key: "tags", value: "tags", order: 6 }
|
|
13749
|
+
];
|
|
13750
|
+
function buildContactTagFilter(tagNames) {
|
|
13751
|
+
return { field: "tags", operator: "eq", value: tagNames, options: { minimumMatch: "all" } };
|
|
13752
|
+
}
|
|
13753
|
+
function buildContactSmartListBody(args) {
|
|
13754
|
+
return {
|
|
13755
|
+
columns: args.columns ?? DEFAULT_CONTACT_SMARTLIST_COLUMNS,
|
|
13756
|
+
filterSpecs: {
|
|
13757
|
+
filters: [{ group: "OR", filters: [{ group: "AND", filters: args.filters }] }],
|
|
13758
|
+
page: 1,
|
|
13759
|
+
limit: args.limit ?? 20
|
|
13760
|
+
},
|
|
13761
|
+
listName: args.listName,
|
|
13762
|
+
sortSpecs: [],
|
|
13763
|
+
locationId: args.locationId
|
|
13764
|
+
};
|
|
13765
|
+
}
|
|
13684
13766
|
function registerSmartListTools(server2, builderClient) {
|
|
13685
13767
|
const client = builderClient;
|
|
13686
13768
|
if (!client) return;
|
|
@@ -13714,6 +13796,19 @@ ${text2}`);
|
|
|
13714
13796
|
async ({ objectKey, query, limit, startAfter, locationId: locationId2 }) => {
|
|
13715
13797
|
try {
|
|
13716
13798
|
const loc = locationId2 ?? client.locationId;
|
|
13799
|
+
if (objectKey === "contacts") {
|
|
13800
|
+
const headers = await client.buildHeaders();
|
|
13801
|
+
const p = new URLSearchParams({
|
|
13802
|
+
locationId: loc,
|
|
13803
|
+
userId: client.getUserId(),
|
|
13804
|
+
globals: "true",
|
|
13805
|
+
transform: "true"
|
|
13806
|
+
});
|
|
13807
|
+
const res = await fetch(`${CONTACT_SMARTLIST_BASE}/search?${p.toString()}`, { headers });
|
|
13808
|
+
if (!res.ok) throw new Error(`Smart Lists API Error ${res.status}: GET /contacts/smartlist/search
|
|
13809
|
+
${await res.text()}`);
|
|
13810
|
+
return jsonResponse(JSON.parse(await res.text() || "{}"));
|
|
13811
|
+
}
|
|
13717
13812
|
const params = new URLSearchParams({ objectKey });
|
|
13718
13813
|
if (query) params.set("query", query);
|
|
13719
13814
|
if (limit !== void 0) params.set("limit", String(limit));
|
|
@@ -13725,6 +13820,74 @@ ${text2}`);
|
|
|
13725
13820
|
}
|
|
13726
13821
|
}
|
|
13727
13822
|
);
|
|
13823
|
+
server2.tool(
|
|
13824
|
+
"create_contact_smart_list",
|
|
13825
|
+
"Create a CONTACT smart list (saved filter over contacts) with working filters \u2014 the owner-exception views a dashboard needs. Uses GHL's contacts/smartlist service and its captured OR-of-AND filter envelope. `tagFilters` is the common case: each entry is a list of tag names the contact must ALL carry. For anything else pass `rawFilters` (leaf objects: {field, operator, value, options}). NOTE: create_smart_list (the older tool) targets the opportunity-list service and cannot express contact filters.",
|
|
13826
|
+
{
|
|
13827
|
+
name: import_zod48.z.string().describe("Display name for the smart list."),
|
|
13828
|
+
tagFilters: import_zod48.z.array(import_zod48.z.array(import_zod48.z.string())).optional().describe('Tag conditions. Each inner array is one filter requiring ALL those tags, e.g. [["escalated"]] for "has the escalated tag".'),
|
|
13829
|
+
rawFilters: import_zod48.z.array(import_zod48.z.record(import_zod48.z.unknown())).optional().describe("Advanced: raw filter leaves {field, operator, value, options}. Combined with tagFilters into one AND group."),
|
|
13830
|
+
locationId: import_zod48.z.string().optional().describe("Location ID. Falls back to the active builder client's location.")
|
|
13831
|
+
},
|
|
13832
|
+
async ({ name, tagFilters, rawFilters, locationId: locationId2 }) => {
|
|
13833
|
+
try {
|
|
13834
|
+
const loc = locationId2 ?? client.locationId;
|
|
13835
|
+
const filters = [
|
|
13836
|
+
...(tagFilters ?? []).map((tags) => buildContactTagFilter(tags)),
|
|
13837
|
+
...rawFilters ?? []
|
|
13838
|
+
];
|
|
13839
|
+
const body = buildContactSmartListBody({ listName: name, locationId: loc, filters });
|
|
13840
|
+
const headers = await client.buildHeaders();
|
|
13841
|
+
const res = await fetch(CONTACT_SMARTLIST_BASE, { method: "POST", headers, body: JSON.stringify(body) });
|
|
13842
|
+
if (!res.ok) throw new Error(`Smart Lists API Error ${res.status}: POST /contacts/smartlist
|
|
13843
|
+
${await res.text()}`);
|
|
13844
|
+
return jsonResponse(JSON.parse(await res.text() || "{}"));
|
|
13845
|
+
} catch (error) {
|
|
13846
|
+
return errorResponse(error);
|
|
13847
|
+
}
|
|
13848
|
+
}
|
|
13849
|
+
);
|
|
13850
|
+
server2.tool(
|
|
13851
|
+
"get_contact_smart_list",
|
|
13852
|
+
"Get a CONTACT smart list by id INCLUDING its filterSpecs. Use this to verify a contact smart list's filters after creating it \u2014 get_smart_list targets the opportunity-list service and returns the record without any filters, which reads as 'no filters set'.",
|
|
13853
|
+
{
|
|
13854
|
+
listId: import_zod48.z.string().describe("The contact smart list id.")
|
|
13855
|
+
},
|
|
13856
|
+
async ({ listId }) => {
|
|
13857
|
+
try {
|
|
13858
|
+
const headers = await client.buildHeaders();
|
|
13859
|
+
const res = await fetch(`${CONTACT_SMARTLIST_BASE}/${listId}?transform=true`, { headers });
|
|
13860
|
+
if (!res.ok) throw new Error(`Smart Lists API Error ${res.status}: GET /contacts/smartlist/${listId}
|
|
13861
|
+
${await res.text()}`);
|
|
13862
|
+
return jsonResponse(JSON.parse(await res.text() || "{}"));
|
|
13863
|
+
} catch (error) {
|
|
13864
|
+
return errorResponse(error);
|
|
13865
|
+
}
|
|
13866
|
+
}
|
|
13867
|
+
);
|
|
13868
|
+
server2.tool(
|
|
13869
|
+
"delete_contact_smart_list",
|
|
13870
|
+
"Delete a CONTACT smart list by id.",
|
|
13871
|
+
{
|
|
13872
|
+
listId: import_zod48.z.string().describe("The contact smart list id."),
|
|
13873
|
+
confirm: import_zod48.z.literal("DELETE").describe('Type "DELETE" to confirm.')
|
|
13874
|
+
},
|
|
13875
|
+
async ({ listId }) => {
|
|
13876
|
+
try {
|
|
13877
|
+
const headers = await client.buildHeaders();
|
|
13878
|
+
const res = await fetch(CONTACT_SMARTLIST_DELETE, {
|
|
13879
|
+
method: "POST",
|
|
13880
|
+
headers,
|
|
13881
|
+
body: JSON.stringify({ smartlist_id: listId })
|
|
13882
|
+
});
|
|
13883
|
+
if (!res.ok) throw new Error(`Smart Lists API Error ${res.status}: POST /smartlist/delete
|
|
13884
|
+
${await res.text()}`);
|
|
13885
|
+
return jsonResponse(JSON.parse(await res.text() || "{}"));
|
|
13886
|
+
} catch (error) {
|
|
13887
|
+
return errorResponse(error);
|
|
13888
|
+
}
|
|
13889
|
+
}
|
|
13890
|
+
);
|
|
13728
13891
|
server2.tool(
|
|
13729
13892
|
"get_smart_list",
|
|
13730
13893
|
"Get a single smart list by ID with its full configuration: filters, columns, permissions, and metadata. The filters array is what defines who/what is in the list.",
|
|
@@ -16320,6 +16483,8 @@ var findOpportunitySchema = import_zod58.z.object({
|
|
|
16320
16483
|
});
|
|
16321
16484
|
var actionSchema = import_zod58.z.discriminatedUnion("type", [...branchActionOptions, findOpportunitySchema]);
|
|
16322
16485
|
var APPOINTMENT_STATUSES = ["new", "confirmed", "showed", "noshow", "cancelled", "invalid"];
|
|
16486
|
+
var CALL_STATUSES = ["busy", "canceled", "voicemail", "no-answer", "completed"];
|
|
16487
|
+
var NUMBER_VALIDATION_STATES = ["not_valid", "sms_incapable"];
|
|
16323
16488
|
var triggerSchema = import_zod58.z.object({
|
|
16324
16489
|
type: import_zod58.z.string(),
|
|
16325
16490
|
formRef: nsRef("form").optional(),
|
|
@@ -16328,7 +16493,34 @@ var triggerSchema = import_zod58.z.object({
|
|
|
16328
16493
|
pipelineRef: nsRef("pipeline").optional(),
|
|
16329
16494
|
stageRef: nsRef("stage").optional(),
|
|
16330
16495
|
// Required for a native `appointment` trigger (the status it fires on).
|
|
16331
|
-
appointmentStatus: import_zod58.z.enum(APPOINTMENT_STATUSES).optional()
|
|
16496
|
+
appointmentStatus: import_zod58.z.enum(APPOINTMENT_STATUSES).optional(),
|
|
16497
|
+
// ── customer_reply scoping ──────────────────────────────────────
|
|
16498
|
+
// A bare customer_reply trigger fires on EVERY inbound reply from EVERY
|
|
16499
|
+
// contact. That is almost never what a plan means: "alert the owner when a
|
|
16500
|
+
// referral partner replies" must not fire for a family's reply. GHL's
|
|
16501
|
+
// ContactReplyFilter supports both of these (see templates/trigger-schemas.json):
|
|
16502
|
+
// hasTagRef → contact.tags / index-of-true (only replies from contacts
|
|
16503
|
+
// carrying this tag)
|
|
16504
|
+
// replyIntent → message.body / matches_intent (GHL classifies the reply as
|
|
16505
|
+
// a positive or negative response; the option values are
|
|
16506
|
+
// "schedule-yes" and "schedule-no")
|
|
16507
|
+
// Both optional and additive — omitting them keeps the long-standing
|
|
16508
|
+
// fires-on-any-reply baseline.
|
|
16509
|
+
hasTagRef: nsRef("tag").optional(),
|
|
16510
|
+
replyIntent: import_zod58.z.enum(["positive", "negative"]).optional(),
|
|
16511
|
+
// ── call_status scoping (missed-call text-back) ─────────────────
|
|
16512
|
+
// GHL's call_status trigger fires on a completed call attempt. The states it
|
|
16513
|
+
// exposes are exactly what a "missed call" means operationally. Required for
|
|
16514
|
+
// a call_status trigger — without them the trigger would fire on EVERY call
|
|
16515
|
+
// including answered ones, so the executor refuses rather than guess.
|
|
16516
|
+
callStatuses: import_zod58.z.array(import_zod58.z.enum(CALL_STATUSES)).optional(),
|
|
16517
|
+
callDirection: import_zod58.z.enum(["inbound", "outbound"]).optional(),
|
|
16518
|
+
// ── validation_error (GHL UI: "Number validation") ──────────────
|
|
16519
|
+
// Fires after a phone number passes or fails a validation check. The UI shows
|
|
16520
|
+
// "Not valid" / "SMS incapable"; the WIRE values are snake_case. Required for
|
|
16521
|
+
// a validation_error trigger — without them the executor refuses rather than
|
|
16522
|
+
// guess which failure states to fire on.
|
|
16523
|
+
numberValidation: import_zod58.z.array(import_zod58.z.enum(NUMBER_VALIDATION_STATES)).optional()
|
|
16332
16524
|
});
|
|
16333
16525
|
var workflowSchema = import_zod58.z.object({
|
|
16334
16526
|
ref: nsRef("workflow"),
|
|
@@ -16447,6 +16639,7 @@ function checkRefIntegrity(plan, defined) {
|
|
|
16447
16639
|
const t = w.trigger;
|
|
16448
16640
|
check(t.formRef, "form", `workflows[${w.ref}].trigger.formRef`);
|
|
16449
16641
|
check(t.tagRef, "tag", `workflows[${w.ref}].trigger.tagRef`);
|
|
16642
|
+
check(t.hasTagRef, "tag", `workflows[${w.ref}].trigger.hasTagRef`);
|
|
16450
16643
|
check(t.calendarRef, "calendar", `workflows[${w.ref}].trigger.calendarRef`);
|
|
16451
16644
|
check(t.pipelineRef, "pipeline", `workflows[${w.ref}].trigger.pipelineRef`);
|
|
16452
16645
|
check(t.stageRef, "stage", `workflows[${w.ref}].trigger.stageRef`);
|
|
@@ -16609,6 +16802,21 @@ function validateBuildPlan(input) {
|
|
|
16609
16802
|
);
|
|
16610
16803
|
}
|
|
16611
16804
|
}
|
|
16805
|
+
for (const wf of plan.workflows ?? []) {
|
|
16806
|
+
let seenFind = false;
|
|
16807
|
+
for (const a of wf.actions) {
|
|
16808
|
+
if (a.type === "find_opportunity") {
|
|
16809
|
+
seenFind = true;
|
|
16810
|
+
continue;
|
|
16811
|
+
}
|
|
16812
|
+
if (a.type === "update_opportunity" && !seenFind) {
|
|
16813
|
+
warnings.push(
|
|
16814
|
+
`workflow "${wf.ref}" has an update_opportunity ("move to stage") with no find_opportunity before it \u2014 GHL updates the opportunity currently in workflow context, so with nothing loaded this action SILENTLY DOES NOTHING and the workflow carries on as if it worked. Put the move inside a find_opportunity "found" branch.`
|
|
16815
|
+
);
|
|
16816
|
+
break;
|
|
16817
|
+
}
|
|
16818
|
+
}
|
|
16819
|
+
}
|
|
16612
16820
|
const MAX_NODES = 40;
|
|
16613
16821
|
for (const wf of plan.workflows ?? []) {
|
|
16614
16822
|
let nodeCount = 0;
|
|
@@ -17158,18 +17366,87 @@ function expandTrigger(trigger, idx, idMap) {
|
|
|
17158
17366
|
case "appointment": {
|
|
17159
17367
|
const status = trigger.appointmentStatus;
|
|
17160
17368
|
if (!status) return manual(`Set this workflow's appointment trigger in the GHL UI: ${triggerPlainEnglish(trigger, idx)} (the plan trigger has no appointmentStatus to fire on, e.g. confirmed / noshow).`);
|
|
17369
|
+
const conditions = [
|
|
17370
|
+
{ operator: "==", field: "appointment.eventType", value: "normal", title: "Event Type", type: "select" },
|
|
17371
|
+
{ operator: "==", field: "appointment.status", value: status, title: "Appointment status is", type: "select" }
|
|
17372
|
+
];
|
|
17373
|
+
const pending = [];
|
|
17374
|
+
if (trigger.calendarRef) {
|
|
17375
|
+
const calendarId = resolveId(trigger.calendarRef, idMap);
|
|
17376
|
+
if (isPending(calendarId)) pending.push(trigger.calendarRef);
|
|
17377
|
+
conditions.unshift({
|
|
17378
|
+
operator: "==",
|
|
17379
|
+
field: "calendar.id",
|
|
17380
|
+
value: calendarId,
|
|
17381
|
+
title: "In calendar",
|
|
17382
|
+
type: "select"
|
|
17383
|
+
});
|
|
17384
|
+
}
|
|
17161
17385
|
return native({
|
|
17162
17386
|
name: `Appointment Status \u2014 ${status}`,
|
|
17163
17387
|
type: "appointment",
|
|
17388
|
+
conditions
|
|
17389
|
+
}, pending);
|
|
17390
|
+
}
|
|
17391
|
+
case "call_status": {
|
|
17392
|
+
const statuses = trigger.callStatuses;
|
|
17393
|
+
if (!statuses || statuses.length === 0) {
|
|
17394
|
+
return manual(
|
|
17395
|
+
`Set this workflow's call trigger in the GHL UI and choose which call outcomes it fires on (e.g. no-answer, busy, voicemail for a missed call). Blueprint will not build it without them: an unscoped call trigger fires on EVERY call, answered ones included.`
|
|
17396
|
+
);
|
|
17397
|
+
}
|
|
17398
|
+
const callConditions = [
|
|
17399
|
+
{ operator: "contains-any", field: "call_status", value: statuses, title: "Call Status" }
|
|
17400
|
+
];
|
|
17401
|
+
if (trigger.callDirection) {
|
|
17402
|
+
callConditions.push({
|
|
17403
|
+
operator: "==",
|
|
17404
|
+
field: "message.direction",
|
|
17405
|
+
value: trigger.callDirection,
|
|
17406
|
+
title: "Call Direction",
|
|
17407
|
+
type: "select"
|
|
17408
|
+
});
|
|
17409
|
+
}
|
|
17410
|
+
return native({ name: "Call Status", type: "call_status", conditions: callConditions });
|
|
17411
|
+
}
|
|
17412
|
+
case "validation_error": {
|
|
17413
|
+
const states = trigger.numberValidation;
|
|
17414
|
+
if (!states || states.length === 0) {
|
|
17415
|
+
return manual(
|
|
17416
|
+
`Set this workflow's "Number validation" trigger in the GHL UI and choose which states it fires on (Not valid / SMS incapable). Blueprint will not build it without them rather than guess which failures matter.`
|
|
17417
|
+
);
|
|
17418
|
+
}
|
|
17419
|
+
return native({
|
|
17420
|
+
name: "Number Validation",
|
|
17421
|
+
type: "validation_error",
|
|
17164
17422
|
conditions: [
|
|
17165
|
-
{ operator: "
|
|
17166
|
-
{ operator: "==", field: "appointment.status", value: status, title: "Appointment status is", type: "select" }
|
|
17423
|
+
{ operator: "contains-any", field: "contact.phoneInfo", value: states, title: "Number is", type: "multiselect" }
|
|
17167
17424
|
]
|
|
17168
17425
|
});
|
|
17169
17426
|
}
|
|
17170
17427
|
case "customer_reply":
|
|
17171
17428
|
case "contact_replied": {
|
|
17172
|
-
|
|
17429
|
+
const replyConditions = [];
|
|
17430
|
+
if (trigger.hasTagRef) {
|
|
17431
|
+
const tagName = idx.tagName.get(trigger.hasTagRef) ?? trigger.hasTagRef;
|
|
17432
|
+
replyConditions.push({
|
|
17433
|
+
operator: "index-of-true",
|
|
17434
|
+
field: "contact.tags",
|
|
17435
|
+
value: tagName,
|
|
17436
|
+
title: "Has Tag",
|
|
17437
|
+
type: "select"
|
|
17438
|
+
});
|
|
17439
|
+
}
|
|
17440
|
+
if (trigger.replyIntent) {
|
|
17441
|
+
replyConditions.push({
|
|
17442
|
+
operator: "matches_intent",
|
|
17443
|
+
field: "message.body",
|
|
17444
|
+
value: trigger.replyIntent === "positive" ? "schedule-yes" : "schedule-no",
|
|
17445
|
+
title: "Intent type",
|
|
17446
|
+
type: "select"
|
|
17447
|
+
});
|
|
17448
|
+
}
|
|
17449
|
+
return native({ name: "Customer Replied", type: "customer_reply", conditions: replyConditions });
|
|
17173
17450
|
}
|
|
17174
17451
|
case "pipeline_stage_updated": {
|
|
17175
17452
|
if (!trigger.pipelineRef || !trigger.stageRef) return manual("Set this workflow's pipeline-stage trigger in the GHL UI (the plan trigger needs both a pipeline and a stage).");
|
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:
|
|
7
|
+
<!-- guides-hash: f8bcba4d92d295fa -->
|
|
8
8
|
<style>
|
|
9
9
|
:root{
|
|
10
10
|
--bg:#F2F4F6; --surface:#FFFFFF; --ink:#1C2833; --muted:#5B6B78; --line:#DDE3E8;
|
|
@@ -173,14 +173,14 @@ main{max-width:1180px}
|
|
|
173
173
|
</div>
|
|
174
174
|
<div class="catsec"><h2>Getting set up</h2><div class="cards"><a class="card pub" href="#g-install-ghl-command" data-k="install connect setup start windows mac workflow builder browser login step 2 full version"><h3>Install GHL Command</h3><p>One install, two short steps, whether you are on the Free plan or the full license. Nothing left to add later.</p><div class="meta"><span class="pill free">Free plan</span><span class="time">~6 minutes</span><span class="open">Read below ↓</span></div></a>
|
|
175
175
|
<a class="card pub" href="#g-switch-between-client-accounts" data-k="switch subaccount sub-account location client account change which account wrong account"><h3>Switch between client accounts</h3><p>Point Claude at any of your sub-accounts by name, and confirm which one is active.</p><div class="meta"><span class="pill free">Free plan</span><span class="time">~10 seconds</span><span class="open">Read below ↓</span></div></a></div></div>
|
|
176
|
-
<div class="catsec"><h2>Contacts and leads</h2><div class="cards"><a class="card pub" href="#g-bulk-tag-and-clean-up" data-k="bulk tag untag update clean duplicates many contacts segment mass"><h3>Bulk tag and clean up</h3><p>Tag, untag, or update hundreds of contacts in one instruction.</p><div class="meta"><span class="pill full">Full license</span><span class="time">~2 minutes</span><span class="open">Read below ↓</span></div></a>
|
|
176
|
+
<div class="catsec"><h2>Contacts and leads</h2><div class="cards"><a class="card pub" href="#g-bulk-tag-and-clean-up" data-k="bulk tag untag update clean duplicates many contacts segment mass smart list saved list view"><h3>Bulk tag and clean up</h3><p>Tag, untag, or update hundreds of contacts in one instruction, then save the group as a list.</p><div class="meta"><span class="pill full">Full license</span><span class="time">~2 minutes</span><span class="open">Read below ↓</span></div></a>
|
|
177
177
|
<a class="card pub" href="#g-count-and-report-contacts" data-k="count how many contacts report number leads week month total added"><h3>Count and report your contacts</h3><p>Ask for a number, get the real number, with the date range printed next to it.</p><div class="meta"><span class="pill free">Free plan</span><span class="time">~10 seconds</span><span class="open">Read below ↓</span></div></a>
|
|
178
178
|
<a class="card pub" href="#g-find-and-list-contacts" data-k="list find contacts export spreadsheet csv excel search leads who signed up"><h3>Find and list contacts</h3><p>Pull a clean list into a spreadsheet you can open in Excel or Google Sheets.</p><div class="meta"><span class="pill free">Free plan</span><span class="time">~1 minute</span><span class="open">Read below ↓</span></div></a>
|
|
179
179
|
<a class="card pub" href="#g-weekly-account-report" data-k="weekly report account snapshot health summary how are we doing client report monday"><h3>Weekly account report</h3><p>One ask, a full snapshot of the account's week: leads, pipeline, and conversations.</p><div class="meta"><span class="pill free">Free plan</span><span class="time">~1 minute</span><span class="open">Read below ↓</span></div></a></div></div>
|
|
180
180
|
<div class="catsec"><h2>Automations</h2><div class="cards"><a class="card pub" href="#g-add-an-exit-workflow" data-k="exit workflow remove stop replied tag note notify pull out stop texting"><h3>Add an exit workflow</h3><p>Cleanly pull people out of a sequence when they reply, tag them, and get you notified.</p><div class="meta"><span class="pill full">Full license</span><span class="time">~5 minutes</span><span class="open">Read below ↓</span></div></a>
|
|
181
181
|
<a class="card pub" href="#g-audit-your-workflows" data-k="audit workflows broken silent failure check review existing validate not sending nothing happens"><h3>Audit your workflows</h3><p>Find silent failures and dead ends in existing workflows before your client does.</p><div class="meta"><span class="pill free">Free plan</span><span class="time">~2 minutes</span><span class="open">Read below ↓</span></div></a>
|
|
182
182
|
<a class="card pub" href="#g-branch-on-tags-and-fields" data-k="if else branch condition vip tag field split two paths different message"><h3>Branch on tags and fields</h3><p>If they are a VIP, send this. Otherwise, send that. Built correctly the first time.</p><div class="meta"><span class="pill full">Full license</span><span class="time">~5 minutes</span><span class="open">Read below ↓</span></div></a>
|
|
183
|
-
<a class="card pub" href="#g-build-nurture-sequence" data-k="nurture sequence drip texts emails follow up workflow build"><h3>Build a nurture sequence</h3><p>A follow-up machine that texts, emails, waits, and stops the second someone replies.</p><div class="meta"><span class="pill full">Full license</span><span class="time">~15 minutes</span><span class="open">Read below ↓</span></div></a></div></div>
|
|
183
|
+
<a class="card pub" href="#g-build-nurture-sequence" data-k="nurture sequence drip texts emails follow up workflow build quiet hours sending window contact hours"><h3>Build a nurture sequence</h3><p>A follow-up machine that texts, emails, waits, and stops the second someone replies.</p><div class="meta"><span class="pill full">Full license</span><span class="time">~15 minutes</span><span class="open">Read below ↓</span></div></a></div></div>
|
|
184
184
|
<div class="catsec"><h2>Pipelines and sales</h2><div class="cards"><a class="card pub" href="#g-documents-and-contracts" data-k="documents contracts proposals signature send sign agreement esign template"><h3>Documents and contracts</h3><p>Proposals and contracts sent for signature and tracked from Claude.</p><div class="meta"><span class="pill full">Full license</span><span class="time">~5 minutes</span><span class="open">Read below ↓</span></div></a>
|
|
185
185
|
<a class="card pub" href="#g-stand-up-a-pipeline" data-k="pipeline stages create new client setup opportunity sales process deal stages"><h3>Stand up a pipeline</h3><p>Stages, fields, and tags for a new client in one conversation.</p><div class="meta"><span class="pill full">Full license</span><span class="time">~10 minutes</span><span class="open">Read below ↓</span></div></a>
|
|
186
186
|
<a class="card pub" href="#g-track-and-move-opportunities" data-k="opportunities deals move stage track stuck value won lost pipeline board"><h3>Track and move opportunities</h3><p>Move deals between stages, update values, and see what is stuck.</p><div class="meta"><span class="pill full">Full license</span><span class="time">~1 minute</span><span class="open">Read below ↓</span></div></a>
|
|
@@ -189,7 +189,7 @@ main{max-width:1180px}
|
|
|
189
189
|
<button class="card locked" type="button" data-k="page studio compose website landing pages funnel" data-toast="This guide arrives in a product update — your install picks it up automatically." title="Coming in a product update." aria-label="Page Studio: pages and sites. Coming in a product update."><h3>Page Studio: pages and sites</h3><p>Compose landing pages and full sites that render correctly in GHL.</p><div class="meta"><span class="pill full">Full license</span><span class="time">~20 min</span><span class="applabel">Coming in an update</span></div></button>
|
|
190
190
|
<button class="card locked" type="button" data-k="clone site copy rebrand landing page existing url" data-toast="This guide arrives in a product update — your install picks it up automatically." title="Coming in a product update." aria-label="Clone and rebrand a site. Coming in a product update."><h3>Clone and rebrand a site</h3><p>Rebuild an existing page for a new client, with a rights check and a review report.</p><div class="meta"><span class="pill full">Full license</span><span class="time">~30 min</span><span class="applabel">Coming in an update</span></div></button></div></div>
|
|
191
191
|
<div class="catsec"><h2>Booking and calendars</h2><div class="cards"><a class="card pub" href="#g-see-and-manage-appointments" data-k="appointments today schedule upcoming who is booked list no show cancel reschedule diary"><h3>See and manage appointments</h3><p>Who is booked today, this week, or for a specific client, in one ask.</p><div class="meta"><span class="pill free">Free plan</span><span class="time">~10 seconds</span><span class="open">Read below ↓</span></div></a>
|
|
192
|
-
<a class="card pub" href="#g-set-up-a-booking-calendar" data-k="calendar create booking consult slots availability buffer appointment types schedule"><h3>Set up a booking calendar</h3><p>A consult calendar with the right slots, buffers, and the right person attached.</p><div class="meta"><span class="pill full">Full license</span><span class="time">~10 minutes</span><span class="open">Read below ↓</span></div></a></div></div>
|
|
192
|
+
<a class="card pub" href="#g-set-up-a-booking-calendar" data-k="calendar create booking consult slots availability buffer appointment types schedule daily limit notice same day"><h3>Set up a booking calendar</h3><p>A consult calendar with the right slots, buffers, and the right person attached.</p><div class="meta"><span class="pill full">Full license</span><span class="time">~10 minutes</span><span class="open">Read below ↓</span></div></a></div></div>
|
|
193
193
|
<div class="catsec"><h2>Email and SMS</h2><div class="cards"><a class="card pub" href="#g-build-email-templates" data-k="email template design create campaign builder reusable branded rename archive"><h3>Build email templates</h3><p>Reusable, branded templates created and edited without the GHL editor.</p><div class="meta"><span class="pill full">Full license</span><span class="time">~10 minutes</span><span class="open">Read below ↓</span></div></a>
|
|
194
194
|
<a class="card pub" href="#g-review-your-conversations" data-k="conversations inbox read unread replies check messages who replied catch up missed"><h3>Review your conversations</h3><p>Catch up on replies and unread threads across every channel.</p><div class="meta"><span class="pill free">Free plan</span><span class="time">~1 minute</span><span class="open">Read below ↓</span></div></a>
|
|
195
195
|
<a class="card pub" href="#g-send-texts-and-emails" data-k="send message text sms email contact reply conversation follow up one off"><h3>Send texts and emails</h3><p>Message any contact from Claude, in your voice, on the right channel.</p><div class="meta"><span class="pill full">Full license</span><span class="time">~30 seconds</span><span class="open">Read below ↓</span></div></a></div></div>
|
|
@@ -490,16 +490,19 @@ When it saves, list the templates in the account so I can see it landed, and tel
|
|
|
490
490
|
<ul>
|
|
491
491
|
<li>A ready-to-review workflow in your GHL account: texts, emails, and waits in the right order, every message written out.</li>
|
|
492
492
|
<li>Stop-on-reply built in, so nobody gets a robot follow-up after a human conversation has started.</li>
|
|
493
|
+
<li>Quiet hours, so the sequence never texts somebody at two in the morning because a wait timer landed there.</li>
|
|
493
494
|
<li>A matching exit workflow: when someone replies, they leave the nurture, get tagged, and you get notified.</li>
|
|
494
495
|
</ul>
|
|
495
496
|
</div>
|
|
496
497
|
<div class="gsec"><h2>Quick start</h2>
|
|
497
498
|
<div class="prompt"><q>Build a 7-day nurture for new leads: text right away, email an hour later, then follow up daily until they reply.</q><button class="copy" type="button">Copy</button></div>
|
|
499
|
+
<div class="prompt"><q>Only let this workflow send between 8am and 9pm.</q><button class="copy" type="button">Copy</button></div>
|
|
498
500
|
<div class="prompt"><q>Show me the full plan first. Do not build anything until I approve it.</q><button class="copy" type="button">Copy</button></div>
|
|
499
501
|
</div>
|
|
500
502
|
<div class="gsec"><h2>The pro prompt</h2>
|
|
501
503
|
<div class="pro"><div class="pro-head"><b>The exact ask we run on real client accounts</b><span class="pill full">Proven live</span><button class="copy" type="button">Copy</button></div><p class="pro-body">Build a nurture workflow in my [Radiance Med Spa] account called "New Lead Nurture 7 Day". Trigger: when the tag [new-lead] is added.
|
|
502
504
|
The sequence: (1) Immediately, text: "Hi {{first name}}, this is [Dana] from [Radiance Med Spa]. Thanks for reaching out about our [body contouring special]! Want me to grab you a free consult this week?" (2) Wait 1 hour, then send an email, subject "Your consult is waiting", friendly, three short paragraphs, one button to my booking link. (3) Wait 1 day, then text a soft check-in. (4) Wait 2 days, then email a before-and-after story from a real client. (5) Wait 3 days, then text a last-call message about the special ending.
|
|
505
|
+
Set quiet hours on this workflow: it may only send between [8am] and [9pm], [every day]. Nothing goes out outside that window.
|
|
503
506
|
Stop the sequence the moment they reply on any channel. Also build the exit workflow: when they reply, remove them from the nurture, tag them [warm-lead], add a note with what they said, and notify me.
|
|
504
507
|
Before you build anything: show me every message written out for my approval, and verify my account's real pipeline, tag, and calendar IDs so nothing fails silently.</p></div>
|
|
505
508
|
<p class="gpara">Everything in [brackets] is yours to change. The last paragraph is the safety net: it makes Claude plan first and check your account's real setup before touching it.</p>
|
|
@@ -521,6 +524,8 @@ Before you build anything: show me every message written out for my approval, an
|
|
|
521
524
|
<div class="fnode"><span class="tag em">Email</span>before-and-after story</div>
|
|
522
525
|
<div class="fconn"></div>
|
|
523
526
|
<div class="fnode"><span class="tag sms">Text</span>last call</div>
|
|
527
|
+
<div class="fconn"></div>
|
|
528
|
+
<div class="fnode"><span class="tag wait">QUIET HOURS: nothing sends outside 8am to 9pm, whenever a wait timer lands</span>then</div>
|
|
524
529
|
<div class="fstop">the whole track stops the moment they reply, on any channel</div>
|
|
525
530
|
</div>
|
|
526
531
|
</div>
|
|
@@ -533,13 +538,17 @@ Before you build anything: show me every message written out for my approval, an
|
|
|
533
538
|
</div>
|
|
534
539
|
<div class="gsec"><h2>Good to know</h2>
|
|
535
540
|
<ul>
|
|
541
|
+
<li>Ask for quiet hours on anything that sends texts. Wait timers do not care what time it is: a one-day wait started at 11pm fires at 11pm. A window of 8am to 9pm is the common answer, and it is set per workflow, so every sequence that texts needs its own.</li>
|
|
542
|
+
<li>Quiet hours are the only place a sending window lives. There is no time-of-day setting inside the AI agent builders, so if a bot hands off to a workflow, this is what protects the handoff.</li>
|
|
543
|
+
<li>Once set, quiet hours stay set. Edit a message or add a step later and the window comes through untouched.</li>
|
|
536
544
|
<li>GHL's editor struggles past about 40 steps in one workflow. Claude knows this and splits long sequences automatically.</li>
|
|
537
545
|
<li>Messages come out in your voice if you give Claude a sample first: paste one text you would actually send and say "write like this".</li>
|
|
538
546
|
<li>The nurture and its exit workflow are a pair. If you ever remove one, ask Claude to update the other, so nothing points at a workflow that is gone.</li>
|
|
547
|
+
<li>If a step in your sequence moves a deal to a new stage, it needs to find that deal first. Say "look up the opportunity, then move it" and Claude wires both. A move on its own saves cleanly and then does nothing.</li>
|
|
539
548
|
</ul>
|
|
540
549
|
</div>
|
|
541
550
|
<div class="gsec"><h2>Where people go wrong</h2>
|
|
542
|
-
<div style="overflow-x:auto"><table class="wrongtbl"><tr><th>What happened</th><th>The fix</th></tr><tr><td>"It built the workflow but nothing ever sends."</td><td>Say "audit this workflow". GHL fails silently when something inside is misconfigured; the audit finds it in seconds.</td></tr><tr><td>"The messages sound generic."</td><td>Give Claude your voice before building: paste a real text you have sent and say "write like this".</td></tr><tr><td>"I want to change one message."</td><td>Say "update step 3's text to..." and never rebuild from scratch. Updates keep everything connected.</td></tr></table></div>
|
|
551
|
+
<div style="overflow-x:auto"><table class="wrongtbl"><tr><th>What happened</th><th>The fix</th></tr><tr><td>"It built the workflow but nothing ever sends."</td><td>Say "audit this workflow". GHL fails silently when something inside is misconfigured; the audit finds it in seconds.</td></tr><tr><td>"A text went out at 3am."</td><td>The workflow has no quiet hours. Say "only let this workflow send between 8am and 9pm" and Claude sets the window.</td></tr><tr><td>"I set quiet hours and later they were gone."</td><td>Versions before 3.65.0 dropped the window whenever the workflow was saved again. Open any workflow that texts, check its window, and set it once more. From 3.65.0 on it survives every later edit.</td></tr><tr><td>"The messages sound generic."</td><td>Give Claude your voice before building: paste a real text you have sent and say "write like this".</td></tr><tr><td>"I want to change one message."</td><td>Say "update step 3's text to..." and never rebuild from scratch. Updates keep everything connected.</td></tr></table></div>
|
|
543
552
|
</div>
|
|
544
553
|
<div class="gsec"><h2>Related guides</h2>
|
|
545
554
|
<ul>
|
|
@@ -552,19 +561,21 @@ Before you build anything: show me every message written out for my approval, an
|
|
|
552
561
|
|
|
553
562
|
<section class="gview" id="g-bulk-tag-and-clean-up">
|
|
554
563
|
<h1 class="gtitle serif">Bulk tag and clean up</h1>
|
|
555
|
-
<p class="gpromise">Tag, untag, or update hundreds of contacts in one instruction.</p>
|
|
564
|
+
<p class="gpromise">Tag, untag, or update hundreds of contacts in one instruction, then save the group as a list.</p>
|
|
556
565
|
<div class="gmeta"><span class="pill full">Full license</span><span class="time">~2 minutes</span><span class="time">Works in Claude Desktop and Claude Code</span></div>
|
|
557
566
|
<div class="gsec"><h2>What you'll get</h2>
|
|
558
567
|
<ul>
|
|
559
568
|
<li>One instruction that tags, untags, or updates a whole group of contacts at once.</li>
|
|
560
569
|
<li>A count of exactly who is about to be touched, before anything is touched.</li>
|
|
561
570
|
<li>A report of what actually landed: how many succeeded, how many failed, out of how many.</li>
|
|
571
|
+
<li>The group saved as a list you can open in GHL any day, instead of rebuilding the same filter every week.</li>
|
|
562
572
|
</ul>
|
|
563
573
|
</div>
|
|
564
574
|
<div class="gsec"><h2>Say this</h2>
|
|
565
575
|
<div class="prompt"><q>Tag everyone who came in from the September ad as september-lead.</q><button class="copy" type="button">Copy</button></div>
|
|
566
576
|
<div class="prompt"><q>Remove the cold-lead tag from anyone who has replied to us.</q><button class="copy" type="button">Copy</button></div>
|
|
567
577
|
<div class="prompt"><q>Tag these twelve contacts as needs-follow-up.</q><button class="copy" type="button">Copy</button></div>
|
|
578
|
+
<div class="prompt"><q>Save everyone tagged needs-follow-up as a list called Needs Follow Up.</q><button class="copy" type="button">Copy</button></div>
|
|
568
579
|
</div>
|
|
569
580
|
<div class="gsec"><h2>The pro prompt</h2>
|
|
570
581
|
<div class="pro"><div class="pro-head"><b>The exact ask we run on real client accounts</b><span class="pill full">Proven live</span><button class="copy" type="button">Copy</button></div><p class="pro-body">In my [Radiance Med Spa] account, find every contact tagged [website-lead] that also has [no tag of consult-booked].
|
|
@@ -573,11 +584,19 @@ Once I say go: add the tag [needs-follow-up] to all of them, then tell me how ma
|
|
|
573
584
|
Do not delete anything, do not send anything, and do not change any other field.</p></div>
|
|
574
585
|
<p class="gpara">Everything in [brackets] is yours to change. The two safety lines matter more than the rest: Claude counts and stops before it touches anything, and the last line rules out everything you did not ask for.</p>
|
|
575
586
|
</div>
|
|
587
|
+
<div class="gsec"><h2>Save the group as a list</h2>
|
|
588
|
+
<p class="gpara">Tagging answers "who needs chasing today". A saved list answers it again next Monday without you rebuilding the filter. Once a group is tagged, ask Claude to save it.</p>
|
|
589
|
+
<div class="pro"><div class="pro-head"><b>The exact ask we run on real client accounts</b><span class="pill full">Proven live</span><button class="copy" type="button">Copy</button></div><p class="pro-body">In my [Radiance Med Spa] account, save a contact list called "[Needs Follow Up]" that shows everyone tagged [needs-follow-up].
|
|
590
|
+
Read it back to me once it is saved so I know it exists, and tell me where to find it in GHL.
|
|
591
|
+
Do not change any contact, and do not delete any list that is already there.</p></div>
|
|
592
|
+
<p class="gpara">The list appears in your contacts view in GHL like any other saved list. Ask Claude to delete one by name when a campaign is over.</p>
|
|
593
|
+
</div>
|
|
576
594
|
<div class="gsec"><h2>What happens when you ask</h2>
|
|
577
595
|
<ol class="steps">
|
|
578
596
|
<li>Claude builds the group and tells you the size, with a few examples so you can spot a mistake.</li>
|
|
579
597
|
<li>You approve, or you narrow it. Nothing has changed yet.</li>
|
|
580
598
|
<li>Claude applies the change in batches and reports back what actually landed.</li>
|
|
599
|
+
<li>If you asked for a saved list, it creates it and reads it back so you can see it exists.</li>
|
|
581
600
|
</ol>
|
|
582
601
|
</div>
|
|
583
602
|
<div class="gsec"><h2>Good to know</h2>
|
|
@@ -585,11 +604,13 @@ Do not delete anything, do not send anything, and do not change any other field.
|
|
|
585
604
|
<li>Ask for the count first, every time. It is the difference between tagging 40 people and tagging 4,000.</li>
|
|
586
605
|
<li>Bulk delete is real, and it does not come back. Claude will make you confirm, and it is worth reading that message twice.</li>
|
|
587
606
|
<li>Tags are the backbone of GHL automation. Tag now, and your workflows have something to trigger on later.</li>
|
|
607
|
+
<li>Saved lists are built from conditions, not from a frozen set of names. Tag somebody tomorrow and they appear in the list on their own.</li>
|
|
608
|
+
<li>Saving lists needs version 3.65.0 or later. In every version before it, the list tools reported success and quietly created nothing, so if you tried this before and found nothing in GHL, that is why. Ask Claude "what version are you running" if you are not sure.</li>
|
|
588
609
|
<li>Bulk changes need the full license. The Free plan can find and count the group, but not change it.</li>
|
|
589
610
|
</ul>
|
|
590
611
|
</div>
|
|
591
612
|
<div class="gsec"><h2>Where people go wrong</h2>
|
|
592
|
-
<div style="overflow-x:auto"><table class="wrongtbl"><tr><th>What happened</th><th>The fix</th></tr><tr><td>"It tagged more people than I meant."</td><td>Always ask for the count and the examples first. Approve after you see them, not before.</td></tr><tr><td>"I asked for duplicates to be merged."</td><td>GHL does not merge from the outside. Claude can find likely duplicates and list them, then you merge in GHL.</td></tr><tr><td>"The tags are not showing in GHL yet."</td><td>GHL's contact index lags a few seconds behind. The report of what succeeded is the truth; the list view catches up.</td></tr></table></div>
|
|
613
|
+
<div style="overflow-x:auto"><table class="wrongtbl"><tr><th>What happened</th><th>The fix</th></tr><tr><td>"It tagged more people than I meant."</td><td>Always ask for the count and the examples first. Approve after you see them, not before.</td></tr><tr><td>"I saved a list before and it never showed up in GHL."</td><td>Versions before 3.65.0 could not actually save lists, though they said they had. Update, then ask again.</td></tr><tr><td>"I asked for duplicates to be merged."</td><td>GHL does not merge from the outside. Claude can find likely duplicates and list them, then you merge in GHL.</td></tr><tr><td>"The tags are not showing in GHL yet."</td><td>GHL's contact index lags a few seconds behind. The report of what succeeded is the truth; the list view catches up.</td></tr></table></div>
|
|
593
614
|
</div>
|
|
594
615
|
<div class="gsec"><h2>Related guides</h2>
|
|
595
616
|
<ul>
|
|
@@ -1040,18 +1061,21 @@ Before you send: confirm the exact contact you matched, show me their phone numb
|
|
|
1040
1061
|
<div class="gsec"><h2>What you'll get</h2>
|
|
1041
1062
|
<ul>
|
|
1042
1063
|
<li>A booking calendar with real opening hours, appointment length, and gaps between appointments.</li>
|
|
1064
|
+
<li>A daily booking cap and a minimum notice period, so a full day stays full and nobody books your next hour.</li>
|
|
1043
1065
|
<li>The right person attached to it, which is the step that quietly breaks calendars built in a hurry.</li>
|
|
1044
1066
|
<li>A read-back of what actually saved, plus the next available slots so you know it is bookable.</li>
|
|
1045
1067
|
</ul>
|
|
1046
1068
|
</div>
|
|
1047
1069
|
<div class="gsec"><h2>Say this</h2>
|
|
1048
1070
|
<div class="prompt"><q>Create a 30-minute consult calendar, weekdays 9 to 5, with a 15-minute gap between appointments.</q><button class="copy" type="button">Copy</button></div>
|
|
1071
|
+
<div class="prompt"><q>Cap the consult calendar at 6 bookings a day and require 24 hours notice.</q><button class="copy" type="button">Copy</button></div>
|
|
1049
1072
|
<div class="prompt"><q>Show me the calendars in this account.</q><button class="copy" type="button">Copy</button></div>
|
|
1050
1073
|
<div class="prompt"><q>What slots are free on the consult calendar this week?</q><button class="copy" type="button">Copy</button></div>
|
|
1051
1074
|
</div>
|
|
1052
1075
|
<div class="gsec"><h2>The pro prompt</h2>
|
|
1053
1076
|
<div class="pro"><div class="pro-head"><b>The exact ask we run on real client accounts</b><span class="pill full">Proven live</span><button class="copy" type="button">Copy</button></div><p class="pro-body">In my [MCP Testing] account, create a booking calendar called "[Free Consult]".
|
|
1054
1077
|
Appointments are [30] minutes, available [Monday to Friday, 9am to 5pm] in the account's timezone, with a [15] minute gap between appointments.
|
|
1078
|
+
Cap it at [6] bookings a day, and require at least [24] hours notice so nobody books the same morning.
|
|
1055
1079
|
Attach it to [me], and look up my real user record in this account rather than guessing at a name.
|
|
1056
1080
|
When it saves, read the calendar back to me and show me the next five bookable slots so I know it is genuinely open for business.</p></div>
|
|
1057
1081
|
<p class="gpara">Everything in [brackets] is yours to change. Asking for the next five slots at the end is the whole test. A calendar that saves but offers no slots looks perfect and books nobody.</p>
|
|
@@ -1059,21 +1083,23 @@ When it saves, read the calendar back to me and show me the next five bookable s
|
|
|
1059
1083
|
<div class="gsec"><h2>What happens when you ask</h2>
|
|
1060
1084
|
<ol class="steps">
|
|
1061
1085
|
<li>Claude looks up the real user in the account so the calendar has an owner who exists.</li>
|
|
1062
|
-
<li>It creates the calendar with your hours, length, gaps, and notice period.</li>
|
|
1086
|
+
<li>It creates the calendar with your hours, length, gaps, daily cap, and notice period.</li>
|
|
1063
1087
|
<li>It reads the calendar back and pulls the next available slots so you can see it is live.</li>
|
|
1064
1088
|
</ol>
|
|
1065
1089
|
</div>
|
|
1066
1090
|
<div class="gsec"><h2>Good to know</h2>
|
|
1067
1091
|
<ul>
|
|
1068
1092
|
<li>Say who the appointments belong to. A calendar with nobody attached will happily save and then show no availability.</li>
|
|
1093
|
+
<li>Ask for the daily cap and the notice period in the same breath as the hours. Six bookings protects a clinic day that also has patients in it, and twenty-four hours notice is what stops someone taking your next open slot while you are still in a room.</li>
|
|
1069
1094
|
<li>Timezones follow the sub-account's setting. If the account is set to the wrong city, every slot is wrong. Check that first on a new client account.</li>
|
|
1070
1095
|
<li>Round robin calendars, where several staff share the bookings, need each person listed. Name them all in the ask. On that type, a slot only opens if the calendar hours AND that person's own working hours both allow it, so a Saturday window needs somebody who works Saturdays.</li>
|
|
1071
|
-
<li>
|
|
1096
|
+
<li>Changing one setting later leaves the others alone. Rename a calendar or raise its daily cap and the slot length, the gaps and the opening hours all stay exactly as you left them.</li>
|
|
1097
|
+
<li>Confirmation and reminder messages still live on GHL's calendar screen. Set those once in GHL. If you want real control over reminder wording, ask Claude for a reminder workflow instead.</li>
|
|
1072
1098
|
<li>Needs the full license.</li>
|
|
1073
1099
|
</ul>
|
|
1074
1100
|
</div>
|
|
1075
1101
|
<div class="gsec"><h2>Where people go wrong</h2>
|
|
1076
|
-
<div style="overflow-x:auto"><table class="wrongtbl"><tr><th>What happened</th><th>The fix</th></tr><tr><td>"The calendar exists but shows no times."</td><td>Nobody is attached to it, or the hours fall outside the account's timezone. Ask for the next five slots and Claude will show you which it is.</td></tr><tr><td>"Clients are booking with no notice."</td><td>
|
|
1102
|
+
<div style="overflow-x:auto"><table class="wrongtbl"><tr><th>What happened</th><th>The fix</th></tr><tr><td>"The calendar exists but shows no times."</td><td>Nobody is attached to it, or the hours fall outside the account's timezone. Ask for the next five slots and Claude will show you which it is.</td></tr><tr><td>"Clients are booking with no notice."</td><td>Say how much notice you want and Claude sets it on the calendar. Twenty-four hours is the usual answer for a consult.</td></tr><tr><td>"One busy day swallowed my whole week."</td><td>Ask for a daily cap. It limits how many bookings a single day can take, no matter how many hours are open.</td></tr><tr><td>"My slot lengths and opening hours went back to defaults on their own."</td><td>Versions before 3.65.0 could reset untouched settings when you changed a different one. Ask Claude to read the calendar back and set them again. From 3.65.0 on, settings you do not mention are left alone.</td></tr><tr><td>"Appointments are back to back."</td><td>Ask for a gap between appointments. Fifteen minutes is the usual answer for a clinic.</td></tr></table></div>
|
|
1077
1103
|
</div>
|
|
1078
1104
|
<div class="gsec"><h2>Related guides</h2>
|
|
1079
1105
|
<ul>
|
package/package.json
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@elitedcs/ghl-mcp",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.65.1",
|
|
4
4
|
"mcpName": "io.github.drjerryrelth/ghl-command",
|
|
5
|
-
"description": "GoHighLevel MCP Server for Claude.
|
|
5
|
+
"description": "GoHighLevel MCP Server for Claude. 238 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",
|
|
7
7
|
"bin": {
|
|
8
8
|
"ghl-mcp": "dist/index.js"
|
|
@@ -1,17 +1,17 @@
|
|
|
1
1
|
{
|
|
2
2
|
"_description": "AUTHORITATIVE REFERENCE: Correct GHL workflow action attribute schemas. Derived from real working workflows built in GHL UI. ALL sessions MUST use these exact formats.",
|
|
3
|
-
"_version": "2.0
|
|
3
|
+
"_version": "2.0 \u2014 2026-03-24",
|
|
4
4
|
"_critical_rules": [
|
|
5
|
-
"NEVER use 'body' for emails
|
|
6
|
-
"NEVER use 'tagName'
|
|
7
|
-
"NEVER use 'duration' for waits
|
|
8
|
-
"NEVER use singular min units
|
|
5
|
+
"NEVER use 'body' for emails \u2014 use 'html' with HTML markup",
|
|
6
|
+
"NEVER use 'tagName' \u2014 use 'tags' array",
|
|
7
|
+
"NEVER use 'duration' for waits \u2014 use startAfter object",
|
|
8
|
+
"NEVER use singular min units \u2014 real GHL uses 'minutes'",
|
|
9
9
|
"ALWAYS include 'attachments: []' on SMS actions",
|
|
10
10
|
"ALWAYS include 'trackingOptions' on email actions",
|
|
11
11
|
"ALWAYS include all 5 fields on wait actions (type, startAfter, isHybridAction, hybridActionType, transitions)",
|
|
12
12
|
"ALWAYS use 'parent' AND 'parentKey' on branch nodes",
|
|
13
13
|
"ALWAYS use 'sibling' arrays on branch-yes and branch-no nodes",
|
|
14
|
-
"NEVER use if_else as a single flat action
|
|
14
|
+
"NEVER use if_else as a single flat action \u2014 it is only valid on condition-node, branch-yes, and branch-no nodes",
|
|
15
15
|
"ALWAYS include workflow_id ARRAY in remove_from_workflow (not just workflowId string)",
|
|
16
16
|
"For linear workflows, the buildActionChain needs BOTH next (forward) and parentKey (backward) on every action",
|
|
17
17
|
"Keep workflows under 40 actions to avoid GHL UI renderer freezes",
|
|
@@ -48,10 +48,9 @@
|
|
|
48
48
|
"hasTrackingLinks": false,
|
|
49
49
|
"hasUtmTracking": false,
|
|
50
50
|
"hasTags": false
|
|
51
|
-
}
|
|
52
|
-
"cc": "{{contact.sales_rep_email}}"
|
|
51
|
+
}
|
|
53
52
|
},
|
|
54
|
-
"notes": "Use 'html' NOT 'body'. Content MUST be HTML with <p> tags. Subject supports merge fields. trackingOptions is REQUIRED.
|
|
53
|
+
"notes": "Use 'html' NOT 'body'. Content MUST be HTML with <p> tags. Subject supports merge fields. trackingOptions is REQUIRED."
|
|
55
54
|
},
|
|
56
55
|
"wait": {
|
|
57
56
|
"example_1hour": {
|
|
@@ -107,7 +106,20 @@
|
|
|
107
106
|
"selectedUser": ""
|
|
108
107
|
}
|
|
109
108
|
},
|
|
110
|
-
"notes": "Nested 'notification' object REQUIRED. selectedUser empty string for all users, or specific user ID."
|
|
109
|
+
"notes": "Nested 'notification' object REQUIRED. selectedUser empty string for all users, or specific user ID. EMAIL CHANNEL (verified live 2026-07-20, PWDJ workflow e91f28da): attributes.type is 'email' (NOT 'notification'), nested key is 'email' (NOT 'notification'), body field is 'html' (NOT 'body'), selectedUser is an ARRAY of user IDs, include attachments:[] and isCloned:false. A 'send_email' discriminator inside a 'notification' object saves but silently never sends.",
|
|
110
|
+
"emailChannelExample": {
|
|
111
|
+
"type": "email",
|
|
112
|
+
"email": {
|
|
113
|
+
"isCloned": false,
|
|
114
|
+
"html": "<p style=\"margin:0px;\">We received a new Collector story.</p>",
|
|
115
|
+
"subject": "New Painting Collector Story Received",
|
|
116
|
+
"selectedUser": [
|
|
117
|
+
"<userId>"
|
|
118
|
+
],
|
|
119
|
+
"userType": "user",
|
|
120
|
+
"attachments": []
|
|
121
|
+
}
|
|
122
|
+
}
|
|
111
123
|
},
|
|
112
124
|
"update_contact_field": {
|
|
113
125
|
"example": {
|
|
@@ -173,7 +185,7 @@
|
|
|
173
185
|
"workflowsActionType": "INTERNAL",
|
|
174
186
|
"type": "internal_update_opportunity"
|
|
175
187
|
},
|
|
176
|
-
"notes": "CREATABLE from scratch (re-enabled v3.41.0). The discriminator workflowsActionType:'INTERNAL' MUST sit at the NODE level, never nested in attributes
|
|
188
|
+
"notes": "CREATABLE from scratch (re-enabled v3.41.0). The discriminator workflowsActionType:'INTERNAL' MUST sit at the NODE level, never nested in attributes \u2014 a nested copy makes GHL reject the node as 'action has a corrupted type' and silently fail the whole save. update_workflow_actions normalizes this for you (hoists workflowsActionType to the node level, scaffolds allowBackward + __customInputs__, gives each __customInputFields__ entry an __customInputs__). The shape below (workflowsActionType at the node level alongside type/name/attributes) is correct for both creating and round-tripping. Use pipeline and stage IDs (not names) \u2014 get_pipelines / list_pipelines_full to find them FIRST. CRITICAL: if the pipelineId or pipelineStageId don't exist in the target sub-account, GHL silently fails this action AND can kill subsequent actions. A synthesized node needs BOTH a pipelineId and a pipelineStageId entry; a node round-tripped via get_workflow_full keeps its id and passes through unchanged."
|
|
177
189
|
},
|
|
178
190
|
"_if_else_branching": {
|
|
179
191
|
"notes": "if_else is a node type discriminator only. Do not send a single flat if_else action.",
|
|
@@ -297,7 +309,7 @@
|
|
|
297
309
|
"action": "exit"
|
|
298
310
|
}
|
|
299
311
|
},
|
|
300
|
-
"notes": "Goal-event nodes sit inline in the action chain
|
|
312
|
+
"notes": "Goal-event nodes sit inline in the action chain \u2014 when the goal condition fires during workflow execution, the configured action runs (default: 'exit' terminates the workflow path). The previous action's `next` should point to the goal node's id; the goal node itself does NOT have a `next` field. Use build_goal_event to construct the node \u2014 it handles UUID generation and the nested op/segments/conditions shape. Verified goal_condition value: 'review_request_clicked' (with extras { reviewTypes: ['sms', 'email'], reviewLinkId: '' }). Other goal_condition strings from GHL's UI pass through verbatim; the full catalogue of goal conditions has not been captured yet.",
|
|
301
313
|
"build_helper": "build_goal_event(goal_condition, extras?, action='exit', target_node_id?, name='Goal', op='or', inner_op='or')"
|
|
302
314
|
},
|
|
303
315
|
"_trigger_patterns": {
|
|
@@ -378,8 +390,8 @@
|
|
|
378
390
|
"notes": "Workflow GET requests must include ?includeTriggers=true or triggers silently disappear. Each trigger type has type-specific condition fields."
|
|
379
391
|
},
|
|
380
392
|
"_workflow_design_patterns": {
|
|
381
|
-
"nurture_sequence": "Use linear workflow with stopOnResponse:true. Create separate 'exit workflows' triggered by tags (appointment-booked, do-not-contact) that use remove_from_workflow to pull contacts out. Do NOT use inline if/else gates at every step
|
|
382
|
-
"exit_workflow": "Small 3-4 step workflow: remove_from_workflow
|
|
393
|
+
"nurture_sequence": "Use linear workflow with stopOnResponse:true. Create separate 'exit workflows' triggered by tags (appointment-booked, do-not-contact) that use remove_from_workflow to pull contacts out. Do NOT use inline if/else gates at every step \u2014 this creates too many actions and can freeze GHL.",
|
|
394
|
+
"exit_workflow": "Small 3-4 step workflow: remove_from_workflow \u2192 note \u2192 tag \u2192 notify. Triggered by a tag being added to the contact.",
|
|
383
395
|
"max_actions": "Keep workflows under 40 actions. GHL's UI renderer struggles with larger workflows. Split into multiple connected workflows if needed."
|
|
384
396
|
}
|
|
385
|
-
}
|
|
397
|
+
}
|