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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@elitedcs/ghl-mcp",
3
- "version": "3.89.0-beta.1",
3
+ "version": "3.89.0-beta.10",
4
4
  "mcpName": "io.github.drjerryrelth/ghl-command",
5
5
  "description": "GoHighLevel MCP Server for Claude. 250 tools — full CRM, automation, marketing control, account-wide workflow audit, live funnel-capture verification, and the only programmatic GHL workflow builder, now multi-tenant across client accounts.",
6
6
  "main": "dist/index.js",
@@ -55,7 +55,7 @@ Load the chosen preset's `skeleton`.
55
55
 
56
56
  Apply, in order:
57
57
  1. **Evaluate `conditionalOn`** on every item (and every workflow action). Drop where false. Prune refs that now dangle, and record each prune (you will note these in the summary, not silently drop).
58
- 2. **Resolve `fillFrom`** lists. The main case is pipeline stages: if `goal.salesStages` is non-empty, build `stage.<slug>` refs from it in order; else use the preset `default`. If a workflow references a `stage.*` not in the chosen set, map it to the nearest equivalent (first stage for "new lead" creation) and note it — never ship a dead ref.
58
+ 2. **Resolve `fillFrom`** lists. The main case is pipeline stages: if `goal.salesStages` is non-empty, build `stage.<slug>` refs from it in order; else use the preset `default`. If a workflow references a `stage.*` not in the chosen set, map it to the nearest equivalent through the preset stage's `aliases` (first stage for "new lead" creation; a workflow's `stagePreference` wins for that workflow) and note it — never ship a dead ref, and never add a template stage after the client's Lost.
59
59
  3. **Substitute `{{tokens}}`** (with `||` fallback). Where a token resolves empty and has no fallback, omit the field and note the default you used.
60
60
  4. **Derive interest tags + field options** from the offer/business type (e.g. `interest-injectables` for a med spa) and add them to `tags` + the `field.interest` options, mirroring each other.
61
61
 
@@ -435,7 +435,7 @@
435
435
  {
436
436
  "ref": "sms_template.n_sms_1",
437
437
  "name": "Nurture Text 1 - One Word",
438
- "body": "{{contact.first_name}}, quick one from {{custom_values.owner_first_name}} at {{custom_values.business_name}}: is it lines, volume, or skin texture you'd most like to fix first? One word is enough. Reply STOP to opt out."
438
+ "body": "{{contact.first_name}}, quick one from {{custom_values.owner_first_name}} at {{custom_values.business_name}}: is it lines, volume, or skin texture you'd most like to fix first? One word is enough.\nReply STOP to opt out."
439
439
  },
440
440
  {
441
441
  "ref": "sms_template.n_sms_2",
@@ -460,7 +460,7 @@
460
460
  {
461
461
  "ref": "sms_template.w_sms_1",
462
462
  "name": "Win-back Text 1",
463
- "body": "Hi {{contact.first_name}}, it's {{custom_values.owner_first_name}} at {{custom_values.business_name}}. Is your skin still on the list this season, or should I close your file for now? Reply STOP to opt out."
463
+ "body": "Hi {{contact.first_name}}, it's {{custom_values.owner_first_name}} at {{custom_values.business_name}}. Is your skin still on the list this season, or should I close your file for now?\nReply STOP to opt out."
464
464
  },
465
465
  {
466
466
  "ref": "sms_template.w_sms_2",
@@ -767,7 +767,7 @@
767
767
  "custom_values.owner_first_name",
768
768
  "custom_values.business_name"
769
769
  ],
770
- "body": "{{contact.first_name}}, quick one from {{custom_values.owner_first_name}} at {{custom_values.business_name}}: is it lines, volume, or skin texture you'd most like to fix first? One word is enough. Reply STOP to opt out."
770
+ "body": "{{contact.first_name}}, quick one from {{custom_values.owner_first_name}} at {{custom_values.business_name}}: is it lines, volume, or skin texture you'd most like to fix first? One word is enough.\nReply STOP to opt out."
771
771
  },
772
772
  {
773
773
  "ref": "sms.n_sms_2",
@@ -817,7 +817,7 @@
817
817
  "custom_values.owner_first_name",
818
818
  "custom_values.business_name"
819
819
  ],
820
- "body": "Hi {{contact.first_name}}, it's {{custom_values.owner_first_name}} at {{custom_values.business_name}}. Is your skin still on the list this season, or should I close your file for now? Reply STOP to opt out."
820
+ "body": "Hi {{contact.first_name}}, it's {{custom_values.owner_first_name}} at {{custom_values.business_name}}. Is your skin still on the list this season, or should I close your file for now?\nReply STOP to opt out."
821
821
  },
822
822
  {
823
823
  "ref": "sms.w_sms_2",
@@ -402,7 +402,7 @@
402
402
  {
403
403
  "ref": "sms_template.n_sms_1",
404
404
  "name": "Nurture Text 1 - One Word",
405
- "body": "{{contact.first_name}}, quick one from {{custom_values.owner_first_name}} at {{custom_values.business_name}}: what matters most to you here, speed, price, or getting it right first time? One word is enough. Reply STOP to opt out."
405
+ "body": "{{contact.first_name}}, quick one from {{custom_values.owner_first_name}} at {{custom_values.business_name}}: what matters most to you here, speed, price, or getting it right first time? One word is enough.\nReply STOP to opt out."
406
406
  },
407
407
  {
408
408
  "ref": "sms_template.n_sms_2",
@@ -427,7 +427,7 @@
427
427
  {
428
428
  "ref": "sms_template.w_sms_1",
429
429
  "name": "Win-back Text 1",
430
- "body": "Hi {{contact.first_name}}, it's {{custom_values.owner_first_name}} at {{custom_values.business_name}}. Is this still on your list this quarter, or should I close your file for now? Reply STOP to opt out."
430
+ "body": "Hi {{contact.first_name}}, it's {{custom_values.owner_first_name}} at {{custom_values.business_name}}. Is this still on your list this quarter, or should I close your file for now?\nReply STOP to opt out."
431
431
  },
432
432
  {
433
433
  "ref": "sms_template.w_sms_2",
@@ -734,7 +734,7 @@
734
734
  "custom_values.owner_first_name",
735
735
  "custom_values.business_name"
736
736
  ],
737
- "body": "{{contact.first_name}}, quick one from {{custom_values.owner_first_name}} at {{custom_values.business_name}}: what matters most to you here, speed, price, or getting it right first time? One word is enough. Reply STOP to opt out."
737
+ "body": "{{contact.first_name}}, quick one from {{custom_values.owner_first_name}} at {{custom_values.business_name}}: what matters most to you here, speed, price, or getting it right first time? One word is enough.\nReply STOP to opt out."
738
738
  },
739
739
  {
740
740
  "ref": "sms.n_sms_2",
@@ -784,7 +784,7 @@
784
784
  "custom_values.owner_first_name",
785
785
  "custom_values.business_name"
786
786
  ],
787
- "body": "Hi {{contact.first_name}}, it's {{custom_values.owner_first_name}} at {{custom_values.business_name}}. Is this still on your list this quarter, or should I close your file for now? Reply STOP to opt out."
787
+ "body": "Hi {{contact.first_name}}, it's {{custom_values.owner_first_name}} at {{custom_values.business_name}}. Is this still on your list this quarter, or should I close your file for now?\nReply STOP to opt out."
788
788
  },
789
789
  {
790
790
  "ref": "sms.w_sms_2",
@@ -175,15 +175,21 @@ const stageRefs = new Set();
175
175
  const days = (acts) => (acts || []).reduce((d, a) => a.type === 'wait' ? d + a.value * ({ minutes: 1 / 1440, hours: 1 / 24, days: 1 }[a.unit] || 0) : d, 0);
176
176
  const spanDays = (w) => { let d = days(w.actions); (w.actions || []).forEach(a => { if (a.type === 'find_opportunity') d += Math.max(days(a.found), days(a.notFound)); }); return d; };
177
177
  const nodeCount = (w) => (w.actions || []).reduce((n, a) => n + (a.type === 'find_opportunity' ? 3 + (a.found || []).length + (a.notFound || []).length : 1), 0);
178
- const nurtureTags = new Set((plan.workflows || []).filter(x => /nurture/i.test(x.name || '') && x.trigger && x.trigger.tagRef).map(x => x.trigger.tagRef));
178
+ const nurtureTags = new Set((plan.workflows || []).filter(x => /nurture/i.test(x.name || '') && x.trigger && !Array.isArray(x.trigger) && x.trigger.tagRef).map(x => x.trigger.tagRef));
179
179
  (plan.workflows || []).forEach(w => {
180
- if (!w.trigger || !w.trigger.type) E(`workflow ${w.ref}: missing trigger`);
181
- if (w.trigger) {
182
- need(w.trigger.formRef, `wf ${w.ref} trigger.formRef`); need(w.trigger.tagRef, `wf ${w.ref} trigger.tagRef`); need(w.trigger.calendarRef, `wf ${w.ref} trigger.calendarRef`);
183
- need(w.trigger.pipelineRef, `wf ${w.ref} trigger.pipelineRef`); need(w.trigger.hasTagRef, `wf ${w.ref} trigger.hasTagRef`);
184
- if (w.trigger.stageRef && !stageRefs.has(w.trigger.stageRef)) E(`wf ${w.ref} trigger.stageRef ${w.trigger.stageRef} not a defined stage`);
185
- if (w.trigger.type === 'call_status' && !(w.trigger.callStatuses && w.trigger.callStatuses.length)) E(`workflow ${w.ref}: a call_status trigger needs callStatuses (else it fires on EVERY call)`);
186
- if (['tag_added', 'appointment_status', 'appointment_booked'].includes(w.trigger.type)) E(`workflow ${w.ref}: trigger type "${w.trigger.type}" is not native — use contact_tag / appointment`);
180
+ // A preset may list trigger variants, each with its own conditionalOn (the
181
+ // composer keeps the first that holds, e.g. the intake's review timing): each
182
+ // variant is checked as a trigger; the last must be the unconditional default.
183
+ const variants = Array.isArray(w.trigger) ? w.trigger : (w.trigger ? [w.trigger] : []);
184
+ if (Array.isArray(w.trigger) && (!variants.length || variants[variants.length - 1].conditionalOn)) E(`workflow ${w.ref}: a trigger list must end with a variant that has no conditionalOn (the default)`);
185
+ if (!variants.length || variants.some(t => !t || !t.type)) E(`workflow ${w.ref}: missing trigger`);
186
+ for (const t of variants) {
187
+ if (!t) continue;
188
+ need(t.formRef, `wf ${w.ref} trigger.formRef`); need(t.tagRef, `wf ${w.ref} trigger.tagRef`); need(t.calendarRef, `wf ${w.ref} trigger.calendarRef`);
189
+ need(t.pipelineRef, `wf ${w.ref} trigger.pipelineRef`); need(t.hasTagRef, `wf ${w.ref} trigger.hasTagRef`);
190
+ if (t.stageRef && !stageRefs.has(t.stageRef)) E(`wf ${w.ref} trigger.stageRef ${t.stageRef} not a defined stage`);
191
+ if (t.type === 'call_status' && !(t.callStatuses && t.callStatuses.length)) E(`workflow ${w.ref}: a call_status trigger needs callStatuses (else it fires on EVERY call)`);
192
+ if (['tag_added', 'appointment_status', 'appointment_booked'].includes(t.type)) E(`workflow ${w.ref}: trigger type "${t.type}" is not native — use contact_tag / appointment`);
187
193
  }
188
194
  if (!Array.isArray(w.actions) || w.actions.length === 0) E(`workflow ${w.ref}: no actions`);
189
195
  if ((w.actions || []).length > 40) E(`workflow ${w.ref}: >40 actions (split required)`);
@@ -200,7 +206,7 @@ const nurtureTags = new Set((plan.workflows || []).filter(x => /nurture/i.test(x
200
206
  if (a.type === 'assign_user' && !a.userRef) E(`${at}: needs userRef`);
201
207
  if (a.stageRef && !stageRefs.has(a.stageRef)) E(`${at}: stageRef ${a.stageRef} not a defined stage`);
202
208
  if (a.type === 'wait' && !(a.value > 0 && ['minutes', 'hours', 'days'].includes(a.unit))) E(`${at}: wait needs value > 0 and unit minutes|hours|days`);
203
- if (a.type === 'wait_appointment' && (!w.trigger || w.trigger.type !== 'appointment')) W(`${at}: wait_appointment outside an appointment-triggered workflow`);
209
+ if (a.type === 'wait_appointment' && !variants.some(t => t && t.type === 'appointment')) W(`${at}: wait_appointment outside an appointment-triggered workflow`);
204
210
  };
205
211
  let seenFind = false;
206
212
  (w.actions || []).forEach((a, i) => {
@@ -53,32 +53,39 @@
53
53
  "default": [
54
54
  {
55
55
  "ref": "stage.new_lead",
56
+ "aliases": ["new patient lead", "new patient", "new lead", "new inquiry", "inquiry", "new", "lead", "prospect"],
56
57
  "name": "New Patient Lead"
57
58
  },
58
59
  {
59
60
  "ref": "stage.contacted",
61
+ "aliases": ["contacted", "engaged", "in conversation", "qualified", "follow up", "working"],
60
62
  "name": "Contacted"
61
63
  },
62
64
  {
63
65
  "ref": "stage.visit_booked",
66
+ "aliases": ["visit booked", "appointment booked", "consult booked", "consultation booked", "exam booked", "booked", "scheduled", "=consultation"],
64
67
  "name": "Visit Booked",
65
68
  "conditionalOn": "goal.bookingNeeded == true"
66
69
  },
67
70
  {
68
71
  "ref": "stage.showed",
72
+ "aliases": ["showed", "attended", "visit completed", "exam completed", "consultation completed", "consultation done", "consultation attended", "consultation showed", "consultation complete", "consultation held"],
69
73
  "name": "Showed",
70
74
  "conditionalOn": "goal.bookingNeeded == true"
71
75
  },
72
76
  {
73
77
  "ref": "stage.plan_accepted",
78
+ "aliases": ["care plan accepted", "plan accepted", "accepted", "started care", "sold"],
74
79
  "name": "Care Plan Accepted"
75
80
  },
76
81
  {
77
82
  "ref": "stage.active",
83
+ "aliases": ["active patient", "active", "in care"],
78
84
  "name": "Active Patient"
79
85
  },
80
86
  {
81
87
  "ref": "stage.lost",
88
+ "aliases": ["lost", "closed lost", "not interested"],
82
89
  "name": "Lost"
83
90
  }
84
91
  ]
@@ -267,6 +274,12 @@
267
274
  "name": "review-requested",
268
275
  "_note": "A review was asked for after a completed visit.",
269
276
  "conditionalOn": "goal.bookingNeeded == true"
277
+ },
278
+ {
279
+ "ref": "tag.service_done",
280
+ "name": "service-complete",
281
+ "_note": "Only when the intake chose to ask for a review when the work is done: the team adds it, Review Request starts from it and removes it.",
282
+ "conditionalOn": "goal.bookingNeeded == true && goal.reviewTiming == 'When you mark the work done'"
270
283
  }
271
284
  ],
272
285
  "_tagNote": "Interest tags mirror the What Brings You In options and are intentionally non-clinical. The skill may add service tags from offer.summary only if compliance_notes allow it.",
@@ -517,7 +530,7 @@
517
530
  {
518
531
  "ref": "sms_template.n_sms_1",
519
532
  "name": "Nurture Text 1 - Insurance or Cash",
520
- "body": "{{contact.first_name}}, quick one from {{custom_values.owner_first_name}} at {{custom_values.business_name}}: will this be insurance or cash-pay? One word and we'll check your benefits before you come in. Reply STOP to opt out."
533
+ "body": "{{contact.first_name}}, quick one from {{custom_values.owner_first_name}} at {{custom_values.business_name}}: will this be insurance or cash-pay? One word and we'll check your benefits before you come in.\nReply STOP to opt out."
521
534
  },
522
535
  {
523
536
  "ref": "sms_template.n_sms_2",
@@ -542,7 +555,7 @@
542
555
  {
543
556
  "ref": "sms_template.w_sms_1",
544
557
  "name": "Win-back Text 1",
545
- "body": "Hi {{contact.first_name}}, it's {{custom_values.owner_first_name}} at {{custom_values.business_name}}. Are you still planning a first visit this season, or should I close your file for now? Reply STOP to opt out."
558
+ "body": "Hi {{contact.first_name}}, it's {{custom_values.owner_first_name}} at {{custom_values.business_name}}. Are you still planning a first visit this season, or should I close your file for now?\nReply STOP to opt out."
546
559
  },
547
560
  {
548
561
  "ref": "sms_template.w_sms_2",
@@ -1065,6 +1078,7 @@
1065
1078
  "workflows": [
1066
1079
  {
1067
1080
  "ref": "workflow.speed_to_lead",
1081
+ "allowMultiple": false,
1068
1082
  "name": "Speed to Lead",
1069
1083
  "trigger": {
1070
1084
  "type": "form_submission",
@@ -1150,6 +1164,7 @@
1150
1164
  },
1151
1165
  {
1152
1166
  "ref": "workflow.missed_call",
1167
+ "allowMultiple": true,
1153
1168
  "name": "Missed Call Text-Back",
1154
1169
  "conditionalOn": "channels.sms == true",
1155
1170
  "trigger": {
@@ -1204,13 +1219,14 @@
1204
1219
  },
1205
1220
  {
1206
1221
  "ref": "workflow.lead_nurture",
1222
+ "allowMultiple": false,
1207
1223
  "name": "Lead Nurture (30 days)",
1208
1224
  "trigger": {
1209
1225
  "type": "contact_tag",
1210
1226
  "tagRef": "tag.nurture_start"
1211
1227
  },
1212
1228
  "stopOnResponse": true,
1213
- "_design": "Starts from the Speed to Lead hand-off tag. 11 touches (6 emails + 5 texts) across 31 days of waits: give, give, ask. Removes its own trigger tag first so a later hand-off can start it again. Ends by tagging lapsed and handing off to Win-back. Any reply stops it (stopOnResponse) and Replied - Stop & Route takes over.",
1229
+ "_design": "Starts from the Speed to Lead hand-off tag. 11 touches (6 emails + 5 texts) across 31 days of waits: give, give, ask. Removes its own trigger tag first; each contact goes through it once (re-entry off). Ends by tagging lapsed and handing off to Win-back. Any reply stops it (stopOnResponse) and Replied - Stop & Route takes over.",
1214
1230
  "actions": [
1215
1231
  {
1216
1232
  "type": "remove_contact_tag",
@@ -1345,6 +1361,7 @@
1345
1361
  },
1346
1362
  {
1347
1363
  "ref": "workflow.reply_route",
1364
+ "allowMultiple": true,
1348
1365
  "name": "Replied - Stop & Route",
1349
1366
  "trigger": {
1350
1367
  "type": "customer_reply"
@@ -1399,6 +1416,7 @@
1399
1416
  },
1400
1417
  {
1401
1418
  "ref": "workflow.win_back",
1419
+ "allowMultiple": false,
1402
1420
  "name": "Win-back (30 days)",
1403
1421
  "trigger": {
1404
1422
  "type": "contact_tag",
@@ -1470,6 +1488,7 @@
1470
1488
  },
1471
1489
  {
1472
1490
  "ref": "workflow.appointment_booked",
1491
+ "allowMultiple": true,
1473
1492
  "name": "Visit Booked - Confirm & Move",
1474
1493
  "trigger": {
1475
1494
  "type": "appointment",
@@ -1491,6 +1510,15 @@
1491
1510
  "type": "remove_from_workflow",
1492
1511
  "workflowRef": "workflow.win_back"
1493
1512
  },
1513
+ {
1514
+ "type": "remove_from_workflow",
1515
+ "workflowRef": "workflow.no_show"
1516
+ },
1517
+ {
1518
+ "type": "remove_from_workflow",
1519
+ "workflowRef": "workflow.missed_call",
1520
+ "conditionalOn": "channels.sms == true"
1521
+ },
1494
1522
  {
1495
1523
  "type": "add_contact_tag",
1496
1524
  "tagRef": "tag.lc_booked"
@@ -1534,6 +1562,7 @@
1534
1562
  },
1535
1563
  {
1536
1564
  "ref": "workflow.appointment_reminders",
1565
+ "allowMultiple": true,
1537
1566
  "name": "Visit Reminders",
1538
1567
  "trigger": {
1539
1568
  "type": "appointment",
@@ -1572,6 +1601,8 @@
1572
1601
  },
1573
1602
  {
1574
1603
  "ref": "workflow.no_show",
1604
+ "allowMultiple": true,
1605
+ "stagePreference": { "stage.contacted": ["no show", "noshow", "missed appointment", "did not show"] },
1575
1606
  "name": "No-Show Rescue",
1576
1607
  "trigger": {
1577
1608
  "type": "appointment",
@@ -1660,6 +1691,7 @@
1660
1691
  },
1661
1692
  {
1662
1693
  "ref": "workflow.quote_follow_up",
1694
+ "allowMultiple": true,
1663
1695
  "name": "Quote Follow-up",
1664
1696
  "trigger": {
1665
1697
  "type": "contact_tag",
@@ -1721,24 +1753,44 @@
1721
1753
  },
1722
1754
  {
1723
1755
  "ref": "workflow.review_request",
1756
+ "allowMultiple": false,
1724
1757
  "name": "Review Request",
1725
- "trigger": {
1726
- "type": "appointment",
1727
- "appointmentStatus": "showed",
1728
- "calendarRef": "calendar.new_patient"
1729
- },
1758
+ "trigger": [
1759
+ {
1760
+ "conditionalOn": "goal.reviewTiming == 'When you mark the work done'",
1761
+ "type": "contact_tag",
1762
+ "tagRef": "tag.service_done"
1763
+ },
1764
+ {
1765
+ "type": "appointment",
1766
+ "appointmentStatus": "showed",
1767
+ "calendarRef": "calendar.new_patient"
1768
+ }
1769
+ ],
1730
1770
  "stopOnResponse": true,
1731
1771
  "conditionalOn": "goal.bookingNeeded == true",
1732
- "_design": "On a completed visit (status showed): tag, wait two hours, text the review link, then an email three days later if nothing came back. Any reply stops it. The review link is a custom value the operator pastes once (handoff.review_link). Closes the Layer 4 gap named on the hub: review requests had no build feature (2026-09-14).",
1772
+ "_design": "On a completed visit (status showed): tag, wait two hours, text the review link, then an email three days later if nothing came back. Any reply stops it. The review link is a custom value the operator pastes once (handoff.review_link). Closes the Layer 4 gap named on the hub: review requests had no build feature (2026-09-14). The intake's review timing question (goal.reviewTiming, 2026-09-29) can make it the day after the visit instead, or start it from the service-complete tag the team adds when the work is done.",
1733
1773
  "actions": [
1774
+ {
1775
+ "type": "remove_contact_tag",
1776
+ "tagRef": "tag.service_done",
1777
+ "conditionalOn": "goal.reviewTiming == 'When you mark the work done'"
1778
+ },
1734
1779
  {
1735
1780
  "type": "add_contact_tag",
1736
1781
  "tagRef": "tag.review_requested"
1737
1782
  },
1783
+ {
1784
+ "type": "wait",
1785
+ "value": 1,
1786
+ "unit": "days",
1787
+ "conditionalOn": "goal.reviewTiming == 'The day after a visit marked showed'"
1788
+ },
1738
1789
  {
1739
1790
  "type": "wait",
1740
1791
  "value": 2,
1741
- "unit": "hours"
1792
+ "unit": "hours",
1793
+ "conditionalOn": "goal.reviewTiming != 'The day after a visit marked showed'"
1742
1794
  },
1743
1795
  {
1744
1796
  "type": "send_sms",
@@ -1942,6 +1994,15 @@
1942
1994
  "cv.review_link"
1943
1995
  ]
1944
1996
  },
1997
+ {
1998
+ "ref": "handoff.review_tag",
1999
+ "conditionalOn": "goal.bookingNeeded == true && goal.reviewTiming == 'When you mark the work done'",
2000
+ "owner": "OPERATOR-UI",
2001
+ "title": "Start Review Request by tagging service-complete when the work is done",
2002
+ "trigger": "always (1 minute to read)",
2003
+ "instruction": "The intake chose to ask for a review when the work is done, so Review Request starts from the tag service-complete, not from a visit. Add that tag on the contact when their care is finished. Each patient is asked for a review once.",
2004
+ "successCheck": "A contact tagged service-complete receives the review text two hours later"
2005
+ },
1945
2006
  {
1946
2007
  "ref": "handoff.quote_tag",
1947
2008
  "owner": "OPERATOR-UI",
@@ -50,36 +50,44 @@
50
50
  "default": [
51
51
  {
52
52
  "ref": "stage.new_lead",
53
+ "aliases": ["new lead", "new inquiry", "inquiry", "new", "lead", "prospect"],
53
54
  "name": "New Lead"
54
55
  },
55
56
  {
56
57
  "ref": "stage.contacted",
58
+ "aliases": ["contacted", "engaged", "in conversation", "qualified", "follow up", "working"],
57
59
  "name": "Contacted"
58
60
  },
59
61
  {
60
62
  "ref": "stage.call_booked",
63
+ "aliases": ["call booked", "discovery call booked", "strategy call booked", "consult booked", "appointment booked", "booked", "scheduled", "=consultation"],
61
64
  "name": "Call Booked",
62
65
  "conditionalOn": "goal.bookingNeeded == true"
63
66
  },
64
67
  {
65
68
  "ref": "stage.call_held",
69
+ "aliases": ["call held", "call completed", "showed", "attended", "consultation completed", "consultation done", "consultation attended", "consultation showed", "consultation complete", "consultation held"],
66
70
  "name": "Call Held",
67
71
  "conditionalOn": "goal.bookingNeeded == true"
68
72
  },
69
73
  {
70
74
  "ref": "stage.offer_made",
75
+ "aliases": ["offer made", "offer sent", "proposal sent", "pitched"],
71
76
  "name": "Offer Made"
72
77
  },
73
78
  {
74
79
  "ref": "stage.enrolled",
80
+ "aliases": ["enrolled", "signed", "sold", "purchased"],
75
81
  "name": "Enrolled"
76
82
  },
77
83
  {
78
84
  "ref": "stage.alumni",
85
+ "aliases": ["alumni", "referral", "graduated"],
79
86
  "name": "Alumni / Referral"
80
87
  },
81
88
  {
82
89
  "ref": "stage.lost",
90
+ "aliases": ["lost", "closed lost", "not interested"],
83
91
  "name": "Lost"
84
92
  }
85
93
  ]
@@ -266,7 +274,7 @@
266
274
  {
267
275
  "ref": "tag.service_done",
268
276
  "name": "service-complete",
269
- "_note": "Added when the work is done and paid for. Starts Review Request; removed by it so the next job can start it again."
277
+ "_note": "Added when the work is done and paid for. Starts Review Request; removed by it. Each contact is asked for a review once."
270
278
  }
271
279
  ],
272
280
  "_tagNote": "Interest tags mirror the Program Interest options; the skill renames them to the real program names from the offer at fill time.",
@@ -520,7 +528,7 @@
520
528
  {
521
529
  "ref": "sms_template.n_sms_1",
522
530
  "name": "Nurture Text 1 - One Word",
523
- "body": "{{contact.first_name}}, quick one from {{custom_values.owner_first_name}} at {{custom_values.business_name}}: what's the bigger gap right now, clarity on what to do, or structure to actually do it? One word is enough. Reply STOP to opt out."
531
+ "body": "{{contact.first_name}}, quick one from {{custom_values.owner_first_name}} at {{custom_values.business_name}}: what's the bigger gap right now, clarity on what to do, or structure to actually do it? One word is enough.\nReply STOP to opt out."
524
532
  },
525
533
  {
526
534
  "ref": "sms_template.n_sms_2",
@@ -545,7 +553,7 @@
545
553
  {
546
554
  "ref": "sms_template.w_sms_1",
547
555
  "name": "Win-back Text 1",
548
- "body": "Hi {{contact.first_name}}, it's {{custom_values.owner_first_name}} at {{custom_values.business_name}}. Is {{audience.painPoints[0] || the thing you want to change}} still on your list this quarter, or should I close your application for now? Reply STOP to opt out."
556
+ "body": "Hi {{contact.first_name}}, it's {{custom_values.owner_first_name}} at {{custom_values.business_name}}. Is {{audience.painPoints[0] || the thing you want to change}} still on your list this quarter, or should I close your application for now?\nReply STOP to opt out."
549
557
  },
550
558
  {
551
559
  "ref": "sms_template.w_sms_2",
@@ -1065,6 +1073,7 @@
1065
1073
  "workflows": [
1066
1074
  {
1067
1075
  "ref": "workflow.speed_to_lead",
1076
+ "allowMultiple": false,
1068
1077
  "name": "Speed to Lead",
1069
1078
  "trigger": {
1070
1079
  "type": "form_submission",
@@ -1150,6 +1159,7 @@
1150
1159
  },
1151
1160
  {
1152
1161
  "ref": "workflow.missed_call",
1162
+ "allowMultiple": true,
1153
1163
  "name": "Missed Call Text-Back",
1154
1164
  "conditionalOn": "channels.sms == true",
1155
1165
  "trigger": {
@@ -1204,13 +1214,14 @@
1204
1214
  },
1205
1215
  {
1206
1216
  "ref": "workflow.lead_nurture",
1217
+ "allowMultiple": false,
1207
1218
  "name": "Lead Nurture (30 days)",
1208
1219
  "trigger": {
1209
1220
  "type": "contact_tag",
1210
1221
  "tagRef": "tag.nurture_start"
1211
1222
  },
1212
1223
  "stopOnResponse": true,
1213
- "_design": "Starts from the Speed to Lead hand-off tag. 11 touches (6 emails + 5 texts) across 31 days of waits: give, give, ask. Removes its own trigger tag first so a later hand-off can start it again. Ends by tagging lapsed and handing off to Win-back. Any reply stops it (stopOnResponse) and Replied - Stop & Route takes over.",
1224
+ "_design": "Starts from the Speed to Lead hand-off tag. 11 touches (6 emails + 5 texts) across 31 days of waits: give, give, ask. Removes its own trigger tag first; each contact goes through it once (re-entry off). Ends by tagging lapsed and handing off to Win-back. Any reply stops it (stopOnResponse) and Replied - Stop & Route takes over.",
1214
1225
  "actions": [
1215
1226
  {
1216
1227
  "type": "remove_contact_tag",
@@ -1345,6 +1356,7 @@
1345
1356
  },
1346
1357
  {
1347
1358
  "ref": "workflow.reply_route",
1359
+ "allowMultiple": true,
1348
1360
  "name": "Replied - Stop & Route",
1349
1361
  "trigger": {
1350
1362
  "type": "customer_reply"
@@ -1399,6 +1411,7 @@
1399
1411
  },
1400
1412
  {
1401
1413
  "ref": "workflow.win_back",
1414
+ "allowMultiple": false,
1402
1415
  "name": "Win-back (30 days)",
1403
1416
  "trigger": {
1404
1417
  "type": "contact_tag",
@@ -1470,6 +1483,7 @@
1470
1483
  },
1471
1484
  {
1472
1485
  "ref": "workflow.appointment_booked",
1486
+ "allowMultiple": true,
1473
1487
  "name": "Call Booked - Confirm & Move",
1474
1488
  "trigger": {
1475
1489
  "type": "appointment",
@@ -1491,6 +1505,15 @@
1491
1505
  "type": "remove_from_workflow",
1492
1506
  "workflowRef": "workflow.win_back"
1493
1507
  },
1508
+ {
1509
+ "type": "remove_from_workflow",
1510
+ "workflowRef": "workflow.no_show"
1511
+ },
1512
+ {
1513
+ "type": "remove_from_workflow",
1514
+ "workflowRef": "workflow.missed_call",
1515
+ "conditionalOn": "channels.sms == true"
1516
+ },
1494
1517
  {
1495
1518
  "type": "add_contact_tag",
1496
1519
  "tagRef": "tag.lc_booked"
@@ -1534,6 +1557,7 @@
1534
1557
  },
1535
1558
  {
1536
1559
  "ref": "workflow.appointment_reminders",
1560
+ "allowMultiple": true,
1537
1561
  "name": "Call Reminders",
1538
1562
  "trigger": {
1539
1563
  "type": "appointment",
@@ -1572,6 +1596,8 @@
1572
1596
  },
1573
1597
  {
1574
1598
  "ref": "workflow.no_show",
1599
+ "allowMultiple": true,
1600
+ "stagePreference": { "stage.contacted": ["no show", "noshow", "missed appointment", "did not show"] },
1575
1601
  "name": "No-Show Rescue",
1576
1602
  "trigger": {
1577
1603
  "type": "appointment",
@@ -1660,6 +1686,7 @@
1660
1686
  },
1661
1687
  {
1662
1688
  "ref": "workflow.quote_follow_up",
1689
+ "allowMultiple": true,
1663
1690
  "name": "Quote Follow-up",
1664
1691
  "trigger": {
1665
1692
  "type": "contact_tag",
@@ -1721,6 +1748,7 @@
1721
1748
  },
1722
1749
  {
1723
1750
  "ref": "workflow.review_request",
1751
+ "allowMultiple": false,
1724
1752
  "name": "Review Request",
1725
1753
  "trigger": {
1726
1754
  "type": "contact_tag",