@elitedcs/ghl-mcp 3.64.1 → 3.65.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +46 -0
- package/README.md +5 -5
- package/dist/index.js +291 -14
- package/package.json +2 -2
- package/templates/action-schemas.json +27 -15
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,51 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 3.65.0 — nine calls that reported success and did the wrong thing
|
|
4
|
+
|
|
5
|
+
Found while building a 139-requirement home-care front office end to end. Every
|
|
6
|
+
fix below has the same shape: the call succeeds, the response reads correctly,
|
|
7
|
+
and something quietly did not happen. None raised an error. Four were findable
|
|
8
|
+
only by firing the system and watching, not by reading settings back.
|
|
9
|
+
|
|
10
|
+
**Three of these were damaging live accounts.**
|
|
11
|
+
|
|
12
|
+
- **Fix: saving a calendar wiped its own settings.** Updating one field on a
|
|
13
|
+
calendar sent only that field, and GoHighLevel treated the omissions as
|
|
14
|
+
deletions — slot duration, slot interval, buffers and the entire open-hours
|
|
15
|
+
grid were erased. The call returned 200 and the calendar looked fine in the
|
|
16
|
+
list. Calendar updates now read the existing record first and merge; if that
|
|
17
|
+
read fails the update refuses rather than falling back to a write that would
|
|
18
|
+
clobber.
|
|
19
|
+
- **Fix: saving a workflow silently deleted its quiet hours.** The update never
|
|
20
|
+
sent the sending-window field, so any workflow with contact-hour restrictions
|
|
21
|
+
lost them on the next save, with no warning. For anyone using quiet hours for
|
|
22
|
+
TCPA compliance, the protection disappeared the first time the workflow was
|
|
23
|
+
touched.
|
|
24
|
+
- **Fix: every smart-list tool pointed at the wrong service.** All five called an
|
|
25
|
+
endpoint that does not serve contact smart lists, so they never worked.
|
|
26
|
+
|
|
27
|
+
Silent no-ops:
|
|
28
|
+
|
|
29
|
+
- **Fix: appointment triggers ignored the calendar you chose** and fired for every
|
|
30
|
+
calendar in the account. A caregiver interview booking would start the family
|
|
31
|
+
consultation sequence.
|
|
32
|
+
- **Fix: reply triggers fired on every inbound message**, because the conditions
|
|
33
|
+
were emitted empty.
|
|
34
|
+
- **Fix: moving a deal to a new stage did nothing** unless the workflow had first
|
|
35
|
+
looked the opportunity up. The step reported success at every save. Now caught
|
|
36
|
+
before the build runs.
|
|
37
|
+
|
|
38
|
+
New:
|
|
39
|
+
|
|
40
|
+
- **`create_contact_smart_list`, `get_contact_smart_list`, `delete_contact_smart_list`.**
|
|
41
|
+
238 tools total, 110 free.
|
|
42
|
+
- **Calendar per-day limits and minimum booking notice** are now reachable
|
|
43
|
+
(`appointmentPerDay`, `allowBookingAfter`, `allowBookingAfterUnit`).
|
|
44
|
+
- **Workflow quiet hours** can now be set through the tool — a sending window
|
|
45
|
+
with days, applied per workflow.
|
|
46
|
+
- **Four trigger builders**: calendar-scoped appointments, reply-intent routing,
|
|
47
|
+
call status, and phone-number validation failures.
|
|
48
|
+
|
|
3
49
|
## 3.64.1 — three more calls that failed quietly
|
|
4
50
|
|
|
5
51
|
- **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.0",
|
|
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/package.json
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@elitedcs/ghl-mcp",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.65.0",
|
|
4
4
|
"mcpName": "io.github.drjerryrelth/ghl-command",
|
|
5
|
-
"description": "GoHighLevel MCP Server for Claude.
|
|
5
|
+
"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.",
|
|
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
|
+
}
|