@elitedcs/ghl-mcp 3.71.0 → 3.72.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (34) hide show
  1. package/CHANGELOG.md +34 -0
  2. package/README.md +4 -4
  3. package/dist/capture-helper.js +10 -1
  4. package/dist/index.js +9078 -3211
  5. package/guide/guide.html +2 -1
  6. package/package.json +2 -2
  7. package/skills/blueprint/SKILL.md +2 -0
  8. package/skills/blueprint/examples/medspa-approval-view.md +58 -53
  9. package/skills/blueprint/examples/medspa-brief.json +70 -8
  10. package/skills/blueprint/examples/medspa-build-plan.json +1435 -123
  11. package/skills/blueprint/examples/medspa-dry-run-report.md +2 -0
  12. package/skills/blueprint/examples/sample-approval-view.md +20 -61
  13. package/skills/blueprint/examples/sample-brief.json +95 -7
  14. package/skills/blueprint/examples/sample-build-plan.json +1448 -119
  15. package/skills/blueprint/examples/validate-plan.cjs +195 -13
  16. package/skills/blueprint/presets/clinic-launch-a2p.preset.json +1 -0
  17. package/skills/blueprint/presets/clinic.md +60 -0
  18. package/skills/blueprint/presets/clinic.preset.json +1737 -0
  19. package/skills/blueprint/presets/coach.md +58 -0
  20. package/skills/blueprint/presets/coach.preset.json +1723 -0
  21. package/skills/blueprint/presets/ecommerce.md +54 -0
  22. package/skills/blueprint/presets/ecommerce.preset.json +1287 -0
  23. package/skills/blueprint/presets/generic-client.md +49 -27
  24. package/skills/blueprint/presets/generic-client.preset.json +1552 -122
  25. package/skills/blueprint/presets/local-service.md +58 -0
  26. package/skills/blueprint/presets/local-service.preset.json +1733 -0
  27. package/skills/blueprint/presets/med-spa.md +47 -48
  28. package/skills/blueprint/presets/med-spa.preset.json +1557 -111
  29. package/skills/blueprint/references/brief-schema.md +48 -1
  30. package/skills/blueprint/references/build-plan-schema.md +66 -5
  31. package/skills/blueprint/references/copy-guide.md +167 -0
  32. package/skills/blueprint/references/intake-question-set.md +64 -3
  33. package/skills/blueprint/references/preset-format.md +97 -51
  34. package/templates/action-schemas.json +12 -0
@@ -0,0 +1,1733 @@
1
+ {
2
+ "presetId": "local_service",
3
+ "presetVersion": "2.0.0",
4
+ "schemaVersion": "0.1",
5
+ "title": "Local Service",
6
+ "description": "A local service business system at v2 depth (roofing, HVAC, plumbing, electrical, landscaping, cleaning, pest, restoration and similar). Jobs pipeline, service / urgency / address / quote fields, source + lifecycle + interest tags, a round-robin Estimate Visit calendar, an A2P-compliant quote-request form + funnel, 15 email + 17 SMS send-ready templates, and 8 workflows: Speed to Lead (15 actions, 5 touches in 24 h with an \"is this urgent?\" fork by reply, then a hand-off), Missed Call Text-Back (the single highest-value automation for a trade), Lead Nurture (31 days, 11 touches), Replied - Stop & Route, Win-back (31 days, 5 touches; seasonal), Estimate Booked - Confirm & Move, Estimate Reminders, No-Show Rescue.",
7
+ "default": false,
8
+ "selectors": {
9
+ "businessTypes": [
10
+ "Local service",
11
+ "Home services",
12
+ "Contractor",
13
+ "Roofing",
14
+ "HVAC",
15
+ "Plumbing",
16
+ "Electrical",
17
+ "Landscaping",
18
+ "Cleaning"
19
+ ],
20
+ "aliases": [
21
+ "local_service",
22
+ "local-service",
23
+ "home_services",
24
+ "contractor",
25
+ "trades"
26
+ ]
27
+ },
28
+ "source": "Built from the intake industry pack \"local-service\" (service_area, estimate_flow, emergency_service) and the missed-call text-back pattern; v2 depth per the owner review of 2026-08-26.",
29
+ "conventions": {
30
+ "triggerTokens": "Use ONLY the executor's native trigger types so triggers auto-build: contact_tag (tagRef), form_submission (formRef), appointment (appointmentStatus: confirmed|noshow|...; calendarRef optional), customer_reply, pipeline_stage_updated (pipelineRef+stageRef), call_status (callStatuses + callDirection; the missed-call text-back), inbound_webhook, payment_received. Never tag_added / form_submitted / appointment_status / appointment_booked / contact_replied.",
31
+ "handoffTag": "Sequences hand off with add_contact_tag + a contact_tag trigger (nurture-start, winback-start), NOT add_to_workflow. Both halves of the tag hand-off are live-proven action shapes (the add-tag node and the tag trigger are captured from working GHL UI-built workflows in templates/action-schemas.json); add_to_workflow has no captured native shape in action-schemas.json and has not been proven at runtime, so a preset may not use it. Each sequence removes its own trigger tag as its first action so a later hand-off can fire it again.",
32
+ "nurtureSpan": "Lead Nurture waits sum to 31 days (owner rule: nothing under 30 days). Win-back waits sum to 31 days. Speed to Lead must end with the nurture-start hand-off tag. The validator rejects a /nurture/i workflow under 30 days of waits and a /speed/i workflow without a hand-off.",
33
+ "staff": "Every internal_notification, task_notification and assign_user carries userRef. In the skeleton the ref is the ROLE PLACEHOLDER user.owner; the fill step maps it per step from brief.team (notifyName for notifications, callsName for tasks and assignment, each calendar's staffNames for teamMemberRefs; \"the owner\" = the first admin). No staff in the brief → userRef: \"user.__pending__\" on every such step, the step is KEPT, the build reports it as waiting, and handoff.add_staff holds that workflow DRAFT. Never write a literal user id (the legacy `to` / `assignedTo`).",
34
+ "publishDefault": "Workflows build DRAFT. The skill PROMPTS the operator (publish now or leave DRAFT for review?) and only passes publishWorkflows:true on an explicit yes. Workflows gated by an unmet handoff never auto-publish.",
35
+ "a2pGating": "handoff.a2p.blocks MUST list each SMS-bearing WORKFLOW ref (not just sms.* assets); only an explicit workflow.* ref keeps a workflow DRAFT.",
36
+ "copy": "All copy lives in templates.* (email_template.* / sms_template.*) and is COMPLETE. The build stage rewrites every template from the real brief per references/copy-guide.md (five tests: Jab, Value equation, Says-who, Lock-screen, Thumb), in the tone of brief.voice.threeWords, with brief.voice.signatureLine verbatim in the instant email and text, keeps merge fields and the single CTA, and writes the result to the template AND the matching email/sms asset body. An email/sms asset with only copyDirection and no body reports needsContent and will not build.",
37
+ "fortyCap": "Max 40 actions per workflow (a find_opportunity branch counts head + 2 transitions + children). The longest workflow here is Lead Nurture at 28 actions, so no split is needed; if a rewrite adds touches past 40, split into \"(days 1-14)\" and \"(days 15-30)\" with a hand-off tag between them."
38
+ },
39
+ "skeleton": {
40
+ "users": {
41
+ "fillFrom": "team.staff",
42
+ "transform": "userList",
43
+ "default": [],
44
+ "_note": "One plan user per brief.team.staff[] entry: ref user.<slug of name>, firstName/lastName split from name, email verbatim, role admin for the owner / whoever runs the account, else user, phone from mobile. In this skeleton \"user.owner\" is a ROLE PLACEHOLDER, never a real user: the fill step resolves it per step — internal_notification → the user named by brief.team.notifyName; task_notification and assign_user → brief.team.callsName; calendars[].teamMemberRefs → that calendar's staffNames; \"the owner\" / \"me\" → the first admin. If the brief lists nobody, every userRef becomes the sentinel \"user.__pending__\" and the step is KEPT (the build reports it as waiting for a staff member; handoff.add_staff holds that workflow DRAFT)."
45
+ },
46
+ "pipelines": [
47
+ {
48
+ "ref": "pipeline.jobs",
49
+ "name": "{{business.name}} Jobs || Jobs Pipeline",
50
+ "stages": {
51
+ "fillFrom": "goal.salesStages",
52
+ "transform": "stageList",
53
+ "default": [
54
+ {
55
+ "ref": "stage.new_lead",
56
+ "name": "New Lead"
57
+ },
58
+ {
59
+ "ref": "stage.contacted",
60
+ "name": "Contacted"
61
+ },
62
+ {
63
+ "ref": "stage.estimate_scheduled",
64
+ "name": "Estimate Scheduled",
65
+ "conditionalOn": "goal.bookingNeeded == true"
66
+ },
67
+ {
68
+ "ref": "stage.quote_sent",
69
+ "name": "Quote Sent"
70
+ },
71
+ {
72
+ "ref": "stage.job_won",
73
+ "name": "Job Won"
74
+ },
75
+ {
76
+ "ref": "stage.job_complete",
77
+ "name": "Job Complete"
78
+ },
79
+ {
80
+ "ref": "stage.maintenance",
81
+ "name": "Repeat / Maintenance"
82
+ },
83
+ {
84
+ "ref": "stage.lost",
85
+ "name": "Lost"
86
+ }
87
+ ]
88
+ }
89
+ }
90
+ ],
91
+ "customFields": [
92
+ {
93
+ "ref": "field.lead_source",
94
+ "name": "Lead Source",
95
+ "dataType": "SINGLE_OPTIONS",
96
+ "model": "contact",
97
+ "options": [
98
+ "Website",
99
+ "Google",
100
+ "Facebook",
101
+ "Nextdoor",
102
+ "Referral",
103
+ "Phone",
104
+ "Yard sign / truck",
105
+ "Other"
106
+ ]
107
+ },
108
+ {
109
+ "ref": "field.service_needed",
110
+ "name": "Service Needed",
111
+ "dataType": "SINGLE_OPTIONS",
112
+ "model": "contact",
113
+ "options": [
114
+ "Repair",
115
+ "Replacement / install",
116
+ "Maintenance",
117
+ "Emergency",
118
+ "Quote only"
119
+ ],
120
+ "_optionsNote": "The skill derives trade-specific options from offer.summary and mirrors them with interest-* tags."
121
+ },
122
+ {
123
+ "ref": "field.urgency",
124
+ "name": "Urgency",
125
+ "dataType": "SINGLE_OPTIONS",
126
+ "model": "contact",
127
+ "options": [
128
+ "Emergency (today)",
129
+ "This week",
130
+ "This month",
131
+ "Just pricing"
132
+ ]
133
+ },
134
+ {
135
+ "ref": "field.service_address",
136
+ "name": "Service Address",
137
+ "dataType": "TEXT",
138
+ "model": "contact"
139
+ },
140
+ {
141
+ "ref": "field.estimate_date",
142
+ "name": "Estimate Date",
143
+ "dataType": "DATE",
144
+ "model": "contact",
145
+ "conditionalOn": "goal.bookingNeeded == true"
146
+ },
147
+ {
148
+ "ref": "field.quote_amount",
149
+ "name": "Quote Amount",
150
+ "dataType": "MONETORY",
151
+ "model": "contact"
152
+ },
153
+ {
154
+ "ref": "field.ltv",
155
+ "name": "Estimated Lifetime Value",
156
+ "dataType": "MONETORY",
157
+ "model": "contact"
158
+ }
159
+ ],
160
+ "tags": [
161
+ {
162
+ "ref": "tag.source_website",
163
+ "name": "source-website"
164
+ },
165
+ {
166
+ "ref": "tag.source_instagram",
167
+ "name": "source-instagram"
168
+ },
169
+ {
170
+ "ref": "tag.source_facebook",
171
+ "name": "source-facebook"
172
+ },
173
+ {
174
+ "ref": "tag.source_google",
175
+ "name": "source-google"
176
+ },
177
+ {
178
+ "ref": "tag.source_referral",
179
+ "name": "source-referral"
180
+ },
181
+ {
182
+ "ref": "tag.source_phone",
183
+ "name": "source-phone"
184
+ },
185
+ {
186
+ "ref": "tag.source_other",
187
+ "name": "source-other"
188
+ },
189
+ {
190
+ "ref": "tag.lc_new_lead",
191
+ "name": "lifecycle-new-lead"
192
+ },
193
+ {
194
+ "ref": "tag.lc_contacted",
195
+ "name": "lifecycle-contacted"
196
+ },
197
+ {
198
+ "ref": "tag.lc_nurturing",
199
+ "name": "lifecycle-nurturing"
200
+ },
201
+ {
202
+ "ref": "tag.lc_booked",
203
+ "name": "lifecycle-booked",
204
+ "conditionalOn": "goal.bookingNeeded == true"
205
+ },
206
+ {
207
+ "ref": "tag.lc_quote_sent",
208
+ "name": "lifecycle-quote-sent"
209
+ },
210
+ {
211
+ "ref": "tag.lc_job_won",
212
+ "name": "lifecycle-job-won"
213
+ },
214
+ {
215
+ "ref": "tag.lc_maintenance",
216
+ "name": "lifecycle-maintenance"
217
+ },
218
+ {
219
+ "ref": "tag.lc_lapsed",
220
+ "name": "lifecycle-lapsed"
221
+ },
222
+ {
223
+ "ref": "tag.lc_lost",
224
+ "name": "lifecycle-lost"
225
+ },
226
+ {
227
+ "ref": "tag.nurture_start",
228
+ "name": "nurture-start"
229
+ },
230
+ {
231
+ "ref": "tag.winback_start",
232
+ "name": "winback-start"
233
+ },
234
+ {
235
+ "ref": "tag.missed_call",
236
+ "name": "missed-call",
237
+ "conditionalOn": "channels.sms == true"
238
+ },
239
+ {
240
+ "ref": "tag.no_show",
241
+ "name": "no-show",
242
+ "conditionalOn": "goal.bookingNeeded == true"
243
+ },
244
+ {
245
+ "ref": "tag.interest_repair",
246
+ "name": "interest-repair"
247
+ },
248
+ {
249
+ "ref": "tag.interest_install",
250
+ "name": "interest-install"
251
+ },
252
+ {
253
+ "ref": "tag.interest_maintenance",
254
+ "name": "interest-maintenance"
255
+ },
256
+ {
257
+ "ref": "tag.interest_emergency",
258
+ "name": "interest-emergency"
259
+ }
260
+ ],
261
+ "_tagNote": "Interest tags mirror the Service Needed options. The skill derives trade-specific ones from offer.summary (e.g. interest-roof-replacement) at fill time.",
262
+ "customValues": [
263
+ {
264
+ "ref": "cv.business_name",
265
+ "name": "business_name",
266
+ "value": "{{business.name}}"
267
+ },
268
+ {
269
+ "ref": "cv.business_phone",
270
+ "name": "business_phone",
271
+ "value": "",
272
+ "filledBy": "handoff.business_details",
273
+ "_fill": "the GHL number once one exists (handoff.phone_number), else blank until the operator fills it"
274
+ },
275
+ {
276
+ "ref": "cv.owner_first_name",
277
+ "name": "owner_first_name",
278
+ "value": "",
279
+ "filledBy": "handoff.business_details",
280
+ "_fill": "first name of the user resolved for team.notifyName (the person every message is signed by)"
281
+ },
282
+ {
283
+ "ref": "cv.booking_link",
284
+ "name": "booking_link",
285
+ "value": "",
286
+ "filledBy": "handoff.calendar_oauth",
287
+ "conditionalOn": "goal.bookingNeeded == true"
288
+ }
289
+ ],
290
+ "calendars": [
291
+ {
292
+ "ref": "calendar.estimate",
293
+ "conditionalOn": "goal.bookingNeeded == true",
294
+ "name": "{{business.name}} Estimate Visit || Estimate Visit",
295
+ "calendarType": "round_robin",
296
+ "_slotDuration": "Carry the appointment length from the brief (brief.calendars[].durationMinutes, or the words in the calendar answer: \"15 minutes\" \u2192 15) onto slotDuration (minutes, slotDurationUnit \"mins\"). This preset value applies ONLY when the brief gives no length; without any slotDuration GoHighLevel builds 30-minute slots (finding 25).",
297
+ "slotDuration": 60,
298
+ "slotDurationUnit": "mins",
299
+ "teamMemberRefs": [
300
+ "user.owner"
301
+ ],
302
+ "openHours": [
303
+ {
304
+ "daysOfTheWeek": [
305
+ 1,
306
+ 2,
307
+ 3,
308
+ 4,
309
+ 5
310
+ ],
311
+ "hours": [
312
+ {
313
+ "openHour": 8,
314
+ "openMinute": 0,
315
+ "closeHour": 18,
316
+ "closeMinute": 0
317
+ }
318
+ ]
319
+ }
320
+ ],
321
+ "availabilityType": 0,
322
+ "slotDuration": 60,
323
+ "slotBuffer": 30,
324
+ "requiresStaff": true,
325
+ "notes": "Default Mon-Fri 8-6, 60-minute estimate slots with 30 minutes drive time; operator edits after build. teamMemberRefs points at the estimators in the plan users list."
326
+ }
327
+ ],
328
+ "_calendarsNote": "One plan calendar per brief.calendars[] entry. The first booking calendar in the brief maps onto this ref (its name, type one_on_one→event / round_robin→round_robin / class→class_booking, and teamMemberRefs from staffNames replace the defaults); every further entry is appended as calendar.<slug> with its own teamMemberRefs. The appointment workflows attach to this first calendar. No calendars in the brief and goal.bookingNeeded true → keep this default and note the assumption.",
329
+ "forms": [
330
+ {
331
+ "ref": "form.intake",
332
+ "name": "{{business.name}} Quote Request || Quote Request",
333
+ "fields": [
334
+ {
335
+ "type": "standard",
336
+ "key": "first_name",
337
+ "required": true
338
+ },
339
+ {
340
+ "type": "standard",
341
+ "key": "last_name",
342
+ "required": true
343
+ },
344
+ {
345
+ "type": "standard",
346
+ "key": "email",
347
+ "required": true
348
+ },
349
+ {
350
+ "type": "standard",
351
+ "key": "phone",
352
+ "required": true
353
+ },
354
+ {
355
+ "type": "custom",
356
+ "fieldRef": "field.service_needed",
357
+ "required": true
358
+ },
359
+ {
360
+ "type": "custom",
361
+ "fieldRef": "field.urgency",
362
+ "required": true
363
+ },
364
+ {
365
+ "type": "custom",
366
+ "fieldRef": "field.service_address",
367
+ "required": false
368
+ }
369
+ ]
370
+ }
371
+ ],
372
+ "funnels": [
373
+ {
374
+ "ref": "funnel.main",
375
+ "name": "{{business.name}} Quote Funnel || Quote Funnel",
376
+ "target": "ghl",
377
+ "pages": [
378
+ {
379
+ "ref": "page.optin",
380
+ "name": "{{offer.leadMagnet}} || Get a Quote",
381
+ "role": "optin",
382
+ "formRef": "form.intake",
383
+ "copyDirection": "OPTIN page per templates/funnel-page-content-template.md: Hero (H1 = the promise, e.g. \"A written quote, same week, no surprises\") + service area (business.location / service_area) + GEO quotable summary + 3-5 outcome-led points (licensed/insured only if true, response time, written quotes) + the A2P-compliant capture form with a NOT-pre-checked SMS consent checkbox, legal name + address, Privacy/Terms links + a 4-8 Q FAQ with FAQPage JSON-LD + NAP footer."
384
+ },
385
+ {
386
+ "ref": "page.thankyou",
387
+ "name": "Thank You",
388
+ "role": "confirmation",
389
+ "calendarRef": "calendar.estimate",
390
+ "conditionalOn": "goal.bookingNeeded == true",
391
+ "copyDirection": "CONFIRMATION page: \"Got it\", what happens next (a text in a minute; if urgent, reply URGENT), and the estimate booking widget via cv.booking_link."
392
+ }
393
+ ]
394
+ }
395
+ ],
396
+ "templates": {
397
+ "emails": [
398
+ {
399
+ "ref": "email_template.stl_email_1",
400
+ "name": "Speed to Lead - Instant Email",
401
+ "subject": "Got your quote request, {{contact.first_name}}",
402
+ "html": "<div style=\"font-family:Arial,Helvetica,sans-serif;font-size:16px;line-height:1.5;color:#1f2937;max-width:600px;\"><p style=\"margin:0 0 14px;\">Hi {{contact.first_name}},</p><p style=\"margin:0 0 14px;\">Thanks for asking {{custom_values.business_name}} about {{offer.summary || the work you asked about}}. I'm {{custom_values.owner_first_name}}, and I see every request that comes in.</p><p style=\"margin:0 0 14px;\">Here's what happens next: in the next minute or two you'll get a text from this number. If this is urgent, reply URGENT and we'll call you right away. If it can wait a day, reply with a photo of the problem and we'll tell you what we're likely looking at before anyone drives out.</p><p style=\"margin:0 0 14px;\">If you'd rather book the estimate visit straight away:</p><p style=\"margin:0 0 14px;\"><a href=\"{{custom_values.booking_link}}\" style=\"display:inline-block;background:#1a56db;color:#ffffff;padding:12px 20px;border-radius:6px;text-decoration:none;font-weight:600;\">Book my estimate</a></p><p style=\"margin:0 0 14px;\">Every quote we give is in writing, itemized, and good for 30 days. No surprises when the invoice comes.</p><p style=\"margin:0;\">{{custom_values.owner_first_name}}<br>{{custom_values.business_name}} · {{custom_values.business_phone}}</p></div>"
403
+ },
404
+ {
405
+ "ref": "email_template.stl_email_2",
406
+ "name": "Speed to Lead - 3 Hour Email",
407
+ "subject": "What a quote from us includes",
408
+ "html": "<div style=\"font-family:Arial,Helvetica,sans-serif;font-size:16px;line-height:1.5;color:#1f2937;max-width:600px;\"><p style=\"margin:0 0 14px;\">Hi {{contact.first_name}},</p><p style=\"margin:0 0 14px;\">If you're getting a few quotes, here's what to compare. A quote from {{custom_values.business_name}} always includes:</p><ul style=\"margin:0 0 14px 20px;padding:0;\"><li style=\"margin:0 0 6px;\">Every line itemized: labor, materials, disposal, permits if any.</li><li style=\"margin:0 0 6px;\">A start date and how long the job takes.</li><li style=\"margin:0 0 6px;\">What's covered by warranty and for how long.</li><li style=\"margin:0 0 6px;\">What's NOT included, in plain words, so nothing is added later.</li></ul><p style=\"margin:0 0 14px;\">If another quote is missing any of those, ask why. That's usually where the surprise ends up.</p><p style=\"margin:0 0 14px;\"><a href=\"{{custom_values.booking_link}}\" style=\"display:inline-block;background:#1a56db;color:#ffffff;padding:12px 20px;border-radius:6px;text-decoration:none;font-weight:600;\">Book my estimate</a></p><p style=\"margin:0 0 14px;\">Or reply with a photo and a sentence about what's going on. I'll tell you honestly what it looks like from here.</p><p style=\"margin:0;\">{{custom_values.owner_first_name}}<br>{{custom_values.business_name}} · {{custom_values.business_phone}}</p></div>"
409
+ },
410
+ {
411
+ "ref": "email_template.n_email_1",
412
+ "name": "Nurture 1 - The Cheapest Quote",
413
+ "subject": "Why the cheapest quote costs the most",
414
+ "html": "<div style=\"font-family:Arial,Helvetica,sans-serif;font-size:16px;line-height:1.5;color:#1f2937;max-width:600px;\"><p style=\"margin:0 0 14px;\">Hi {{contact.first_name}},</p><p style=\"margin:0 0 14px;\">Here's the pattern we see most: three quotes, one is far lower, the homeowner picks it, and the final bill ends up above the highest quote.</p><p style=\"margin:0 0 14px;\">Low quotes are low because something's missing: disposal, permits, the part that only shows up once the old one is off, or the warranty. Nobody's lying; it's just not on the page. You find out on the invoice.</p><p style=\"margin:0 0 14px;\">So when you compare, don't compare the total. Compare what's included. Then ask each of us the same question: \"What could add to this once you start?\" The honest answer is worth more than the low number.</p><p style=\"margin:0 0 14px;\">When you're ready, an estimate visit is here: <a href=\"{{custom_values.booking_link}}\" style=\"color:#1a56db;text-decoration:underline;\">pick a time</a>. The quote is written, itemized, and free.</p><p style=\"margin:0;\">{{custom_values.owner_first_name}}<br>{{custom_values.business_name}} · {{custom_values.business_phone}}</p></div>"
415
+ },
416
+ {
417
+ "ref": "email_template.n_email_2",
418
+ "name": "Nurture 2 - How the Estimate Works",
419
+ "subject": "How the estimate visit actually works",
420
+ "html": "<div style=\"font-family:Arial,Helvetica,sans-serif;font-size:16px;line-height:1.5;color:#1f2937;max-width:600px;\"><p style=\"margin:0 0 14px;\">Hi {{contact.first_name}},</p><p style=\"margin:0 0 14px;\">People put off the estimate because they picture a hard sell at the kitchen table. That's not how {{custom_values.business_name}} works:</p><ul style=\"margin:0 0 14px 20px;padding:0;\"><li style=\"margin:0 0 6px;\">We arrive in the window we gave you, or you get a text before it, not after.</li><li style=\"margin:0 0 6px;\">We look at the actual problem and show you what we found, photos included, so you don't have to take our word for it.</li><li style=\"margin:0 0 6px;\">You get a written, itemized quote, usually the same day. You take your time. Nobody pushes for a signature in your driveway.</li></ul><p style=\"margin:0 0 14px;\">The objection we hear most is {{audience.objections[0] || price}}. That's why the quote shows options where there are options: fix what's broken now, or do the fuller job, with the price of each.</p><p style=\"margin:0 0 14px;\"><a href=\"{{custom_values.booking_link}}\" style=\"display:inline-block;background:#1a56db;color:#ffffff;padding:12px 20px;border-radius:6px;text-decoration:none;font-weight:600;\">Book my estimate</a></p><p style=\"margin:0;\">{{custom_values.owner_first_name}}<br>{{custom_values.business_name}} · {{custom_values.business_phone}}</p></div>"
421
+ },
422
+ {
423
+ "ref": "email_template.n_email_3",
424
+ "name": "Nurture 3 - The Three Questions",
425
+ "subject": "Three questions to ask any contractor",
426
+ "html": "<div style=\"font-family:Arial,Helvetica,sans-serif;font-size:16px;line-height:1.5;color:#1f2937;max-width:600px;\"><p style=\"margin:0 0 14px;\">Hi {{contact.first_name}},</p><p style=\"margin:0 0 14px;\">Whoever you end up hiring, ask them these three before you sign:</p><ul style=\"margin:0 0 14px 20px;padding:0;\"><li style=\"margin:0 0 6px;\">Can I see proof of license and insurance? (A yes with paperwork. A \"we're covered\" with nothing to show is a no.)</li><li style=\"margin:0 0 6px;\">Who actually does the work, your crew or a sub? (Either can be fine. You should know before the truck arrives.)</li><li style=\"margin:0 0 6px;\">What happens if it takes longer than quoted? (The answer should be \"the price doesn't change unless you change the job.\")</li></ul><p style=\"margin:0 0 14px;\">Our answers: paperwork on request, you'll know who's on site, and the written price is the price.</p><p style=\"margin:0 0 14px;\"><a href=\"{{custom_values.booking_link}}\" style=\"display:inline-block;background:#1a56db;color:#ffffff;padding:12px 20px;border-radius:6px;text-decoration:none;font-weight:600;\">Book my estimate</a></p><p style=\"margin:0 0 14px;\">Have a question about your specific situation? Reply with a photo. I answer these myself.</p><p style=\"margin:0;\">{{custom_values.owner_first_name}}<br>{{custom_values.business_name}} · {{custom_values.business_phone}}</p></div>"
427
+ },
428
+ {
429
+ "ref": "email_template.n_email_4",
430
+ "name": "Nurture 4 - About the Price",
431
+ "subject": "About the price",
432
+ "html": "<div style=\"font-family:Arial,Helvetica,sans-serif;font-size:16px;line-height:1.5;color:#1f2937;max-width:600px;\"><p style=\"margin:0 0 14px;\">Hi {{contact.first_name}},</p><p style=\"margin:0 0 14px;\">Let's talk about the thing most people are too polite to ask: what is this going to cost?</p><p style=\"margin:0 0 14px;\">The honest answer is \"it depends on what we find\", which is why we don't guess over the phone. What we can promise: the quote is itemized, it shows the repair-only option next to the full-job option where both exist, and payment terms are on the same page. Nothing is added after the fact unless you change the job, and you'd sign for that first.</p><p style=\"margin:0 0 14px;\">If you have a budget in mind, tell us at the estimate. It's easier to design the job to the budget than to argue about it after.</p><p style=\"margin:0 0 14px;\"><a href=\"{{custom_values.booking_link}}\" style=\"display:inline-block;background:#1a56db;color:#ffffff;padding:12px 20px;border-radius:6px;text-decoration:none;font-weight:600;\">Get my written quote</a></p><p style=\"margin:0;\">{{custom_values.owner_first_name}}<br>{{custom_values.business_name}} · {{custom_values.business_phone}}</p></div>"
433
+ },
434
+ {
435
+ "ref": "email_template.n_email_5",
436
+ "name": "Nurture 5 - What a Small Problem Becomes",
437
+ "subject": "What a small problem turns into",
438
+ "html": "<div style=\"font-family:Arial,Helvetica,sans-serif;font-size:16px;line-height:1.5;color:#1f2937;max-width:600px;\"><p style=\"margin:0 0 14px;\">Hi {{contact.first_name}},</p><p style=\"margin:0 0 14px;\">Most of the big jobs we do started as small ones somebody meant to get to.</p><p style=\"margin:0 0 14px;\">That's not a scare tactic; it's just how houses work. A small leak, a unit that's working a bit harder than it should, a patch that's been \"fine for now\": each one has a cheap window and an expensive window. The estimate tells you which window you're in.</p><p style=\"margin:0 0 14px;\">You don't have to do anything today. An hour of our time gets you a written answer you can sit on for 30 days.</p><p style=\"margin:0 0 14px;\"><a href=\"{{custom_values.booking_link}}\" style=\"display:inline-block;background:#1a56db;color:#ffffff;padding:12px 20px;border-radius:6px;text-decoration:none;font-weight:600;\">Book my estimate</a></p><p style=\"margin:0;\">{{custom_values.owner_first_name}}<br>{{custom_values.business_name}} · {{custom_values.business_phone}}</p></div>"
439
+ },
440
+ {
441
+ "ref": "email_template.n_email_6",
442
+ "name": "Nurture 6 - Everything in One Place",
443
+ "subject": "Everything you need to decide, in one place",
444
+ "html": "<div style=\"font-family:Arial,Helvetica,sans-serif;font-size:16px;line-height:1.5;color:#1f2937;max-width:600px;\"><p style=\"margin:0 0 14px;\">Hi {{contact.first_name}},</p><p style=\"margin:0 0 14px;\">If you're still weighing it up, here is everything in one place:</p><ul style=\"margin:0 0 14px 20px;padding:0;\"><li style=\"margin:0 0 6px;\">What: a free estimate visit from {{custom_values.business_name}} with a written, itemized quote, usually the same day, good for 30 days.</li><li style=\"margin:0 0 6px;\">Where: {{business.location || our service area}}.</li><li style=\"margin:0 0 6px;\">The quote includes: labor, materials, disposal, permits, timeline, warranty, and what's excluded.</li><li style=\"margin:0 0 6px;\">Our rule: the written price is the price unless you change the job.</li><li style=\"margin:0 0 6px;\">Book, cancel or move your estimate any time: <a href=\"{{custom_values.booking_link}}\" style=\"color:#1a56db;text-decoration:underline;\">your booking link</a>.</li></ul><p style=\"margin:0 0 14px;\">If the answer is \"not now\", that's fine. Reply with the word later and I'll stop the emails and check in next season.</p><p style=\"margin:0 0 14px;\"><a href=\"{{custom_values.booking_link}}\" style=\"display:inline-block;background:#1a56db;color:#ffffff;padding:12px 20px;border-radius:6px;text-decoration:none;font-weight:600;\">Book my estimate</a></p><p style=\"margin:0;\">{{custom_values.owner_first_name}}<br>{{custom_values.business_name}} · {{custom_values.business_phone}}</p></div>"
445
+ },
446
+ {
447
+ "ref": "email_template.w_email_1",
448
+ "name": "Win-back 1 - One Question",
449
+ "subject": "One question, {{contact.first_name}}",
450
+ "html": "<div style=\"font-family:Arial,Helvetica,sans-serif;font-size:16px;line-height:1.5;color:#1f2937;max-width:600px;\"><p style=\"margin:0 0 14px;\">Hi {{contact.first_name}},</p><p style=\"margin:0 0 14px;\">A while back you asked {{custom_values.business_name}} for a quote, and then things went quiet. That happens; sometimes the problem gets fixed, sometimes it just stops being loud.</p><p style=\"margin:0 0 14px;\">One question, and an honest answer is all I need: what stopped you? Went with someone else, price, timing, or fixed it yourself?</p><p style=\"margin:0 0 14px;\">Reply with a word or two. If it's something we can fix, I'll fix it. If it's done, congratulations, and I'll leave you alone.</p><p style=\"margin:0;\">{{custom_values.owner_first_name}}<br>{{custom_values.business_name}} · {{custom_values.business_phone}}</p></div>"
451
+ },
452
+ {
453
+ "ref": "email_template.w_email_2",
454
+ "name": "Win-back 2 - Before the Season Turns",
455
+ "subject": "Before the season turns",
456
+ "html": "<div style=\"font-family:Arial,Helvetica,sans-serif;font-size:16px;line-height:1.5;color:#1f2937;max-width:600px;\"><p style=\"margin:0 0 14px;\">Hi {{contact.first_name}},</p><p style=\"margin:0 0 14px;\">Quick heads-up from {{custom_values.business_name}}: our calendar is open now and fills once the season turns. If {{offer.summary || the work you asked about}} is still on your list, this is the cheap, unhurried window to get it looked at.</p><p style=\"margin:0 0 14px;\">Nothing's changed about how we work: free estimate visit, written itemized quote, good for 30 days, no pressure in the driveway.</p><p style=\"margin:0 0 14px;\"><a href=\"{{custom_values.booking_link}}\" style=\"display:inline-block;background:#1a56db;color:#ffffff;padding:12px 20px;border-radius:6px;text-decoration:none;font-weight:600;\">Book while the calendar is open</a></p><p style=\"margin:0;\">{{custom_values.owner_first_name}}<br>{{custom_values.business_name}} · {{custom_values.business_phone}}</p></div>"
457
+ },
458
+ {
459
+ "ref": "email_template.w_email_3",
460
+ "name": "Win-back 3 - The Door Stays Open",
461
+ "subject": "The door stays open",
462
+ "html": "<div style=\"font-family:Arial,Helvetica,sans-serif;font-size:16px;line-height:1.5;color:#1f2937;max-width:600px;\"><p style=\"margin:0 0 14px;\">Hi {{contact.first_name}},</p><p style=\"margin:0 0 14px;\">This is the last automatic email you'll get from me. I'd rather you hear from a person when the time is right than from a sequence.</p><p style=\"margin:0 0 14px;\">The free estimate doesn't expire. When you're ready, the link below works, and so does replying to this email, photo attached.</p><p style=\"margin:0 0 14px;\"><a href=\"{{custom_values.booking_link}}\" style=\"display:inline-block;background:#1a56db;color:#ffffff;padding:12px 20px;border-radius:6px;text-decoration:none;font-weight:600;\">Book when you're ready</a></p><p style=\"margin:0 0 14px;\">Thanks for considering us. And whoever you hire: get it itemized, in writing, before they start.</p><p style=\"margin:0;\">{{custom_values.owner_first_name}}<br>{{custom_values.business_name}} · {{custom_values.business_phone}}</p></div>"
463
+ },
464
+ {
465
+ "ref": "email_template.appt_confirm_email",
466
+ "name": "Estimate Confirmed",
467
+ "subject": "Confirmed, {{contact.first_name}}: your estimate visit",
468
+ "html": "<div style=\"font-family:Arial,Helvetica,sans-serif;font-size:16px;line-height:1.5;color:#1f2937;max-width:600px;\"><p style=\"margin:0 0 14px;\">Hi {{contact.first_name}},</p><p style=\"margin:0 0 14px;\">Your estimate visit with {{custom_values.business_name}} is confirmed for {{appointment.start_time}} at the address you gave us.</p><ul style=\"margin:0 0 14px 20px;padding:0;\"><li style=\"margin:0 0 6px;\">Please make sure we can get to the area (gate, dog, attic hatch, that sort of thing).</li><li style=\"margin:0 0 6px;\">If you have photos from when it started, or a previous quote, have them handy.</li><li style=\"margin:0 0 6px;\">If you have a budget in mind, tell us. We'll quote to it where we can.</li></ul><p style=\"margin:0 0 14px;\">You'll get a written, itemized quote, usually the same day. Need to move the visit? <a href=\"{{custom_values.booking_link}}\" style=\"color:#1a56db;text-decoration:underline;\">Reschedule here</a> or reply to this email.</p><p style=\"margin:0;\">{{custom_values.owner_first_name}}<br>{{custom_values.business_name}} · {{custom_values.business_phone}}</p></div>"
469
+ },
470
+ {
471
+ "ref": "email_template.appt_prep_email",
472
+ "name": "Estimate Prep (day before)",
473
+ "subject": "Tomorrow: 3 things that make the estimate quicker",
474
+ "html": "<div style=\"font-family:Arial,Helvetica,sans-serif;font-size:16px;line-height:1.5;color:#1f2937;max-width:600px;\"><p style=\"margin:0 0 14px;\">Hi {{contact.first_name}},</p><p style=\"margin:0 0 14px;\">See you tomorrow at {{appointment.start_time}}. Three small things that make the visit quicker and the quote more accurate:</p><ul style=\"margin:0 0 14px 20px;padding:0;\"><li style=\"margin:0 0 6px;\">Clear a path to the problem area so we can measure and photograph it properly.</li><li style=\"margin:0 0 6px;\">Note when it started and anything that makes it better or worse.</li><li style=\"margin:0 0 6px;\">Decide the one question you most want answered. We'll start there.</li></ul><p style=\"margin:0 0 14px;\">Running late or need to move it? Reply to this email or call {{custom_values.business_phone}}.</p><p style=\"margin:0;\">{{custom_values.owner_first_name}}<br>{{custom_values.business_name}} · {{custom_values.business_phone}}</p></div>"
475
+ },
476
+ {
477
+ "ref": "email_template.ns_email_1",
478
+ "name": "No-Show 1 - We Missed You",
479
+ "subject": "We missed you, {{contact.first_name}} (no guilt)",
480
+ "html": "<div style=\"font-family:Arial,Helvetica,sans-serif;font-size:16px;line-height:1.5;color:#1f2937;max-width:600px;\"><p style=\"margin:0 0 14px;\">Hi {{contact.first_name}},</p><p style=\"margin:0 0 14px;\">We came by for the estimate today and couldn't connect. It happens; nobody here is bothered.</p><p style=\"margin:0 0 14px;\">The free estimate is still yours. Pick a new time whenever it suits, and if something changed, reply and tell me. I'd rather know than guess.</p><p style=\"margin:0 0 14px;\"><a href=\"{{custom_values.booking_link}}\" style=\"display:inline-block;background:#1a56db;color:#ffffff;padding:12px 20px;border-radius:6px;text-decoration:none;font-weight:600;\">Rebook my estimate</a></p><p style=\"margin:0;\">{{custom_values.owner_first_name}}<br>{{custom_values.business_name}} · {{custom_values.business_phone}}</p></div>"
481
+ },
482
+ {
483
+ "ref": "email_template.ns_email_2",
484
+ "name": "No-Show 2 - Still Want This?",
485
+ "subject": "Still want the quote?",
486
+ "html": "<div style=\"font-family:Arial,Helvetica,sans-serif;font-size:16px;line-height:1.5;color:#1f2937;max-width:600px;\"><p style=\"margin:0 0 14px;\">Hi {{contact.first_name}},</p><p style=\"margin:0 0 14px;\">Quick and honest: if the timing is wrong, say so and I'll check back next season. If you still want a written quote, the calendar is open and the visit is still free.</p><p style=\"margin:0 0 14px;\"><a href=\"{{custom_values.booking_link}}\" style=\"display:inline-block;background:#1a56db;color:#ffffff;padding:12px 20px;border-radius:6px;text-decoration:none;font-weight:600;\">Pick a new time</a></p><p style=\"margin:0 0 14px;\">Either way, thanks for considering {{custom_values.business_name}}.</p><p style=\"margin:0;\">{{custom_values.owner_first_name}}<br>{{custom_values.business_name}} · {{custom_values.business_phone}}</p></div>"
487
+ }
488
+ ],
489
+ "sms": [
490
+ {
491
+ "ref": "sms_template.stl_sms_1",
492
+ "name": "Speed to Lead - Instant Text",
493
+ "body": "Hi {{contact.first_name}}, it's {{custom_values.owner_first_name}} at {{custom_values.business_name}}. Got your quote request. Quick one: is this urgent today, or can it wait for a scheduled visit? Reply STOP to opt out."
494
+ },
495
+ {
496
+ "ref": "sms_template.stl_sms_2",
497
+ "name": "Speed to Lead - 20 Minute Text",
498
+ "body": "{{contact.first_name}}, if you can text me a photo of the problem I'll tell you what we're likely looking at before anyone drives out. Or book the estimate here: {{custom_values.booking_link}}"
499
+ },
500
+ {
501
+ "ref": "sms_template.stl_sms_3",
502
+ "name": "Speed to Lead - Next Day Text",
503
+ "body": "Hi {{contact.first_name}}, {{custom_values.owner_first_name}} again. Not chasing, I just don't want your request to sit. Want me to hold an estimate time this week, or is it a \"not now\"?"
504
+ },
505
+ {
506
+ "ref": "sms_template.mc_sms_1",
507
+ "name": "Missed Call - Instant Text-Back",
508
+ "body": "Hi, it's {{custom_values.business_name}}. Sorry we missed your call. If it's an emergency, reply URGENT and we'll call right back. Otherwise, what's going on? Reply STOP to opt out."
509
+ },
510
+ {
511
+ "ref": "sms_template.mc_sms_2",
512
+ "name": "Missed Call - 1 Hour Text",
513
+ "body": "Still need us? Reply with a good time to call you back, or book an estimate directly: {{custom_values.booking_link}}. {{custom_values.business_name}}, {{custom_values.business_phone}}"
514
+ },
515
+ {
516
+ "ref": "sms_template.n_sms_1",
517
+ "name": "Nurture Text 1 - One Word",
518
+ "body": "{{contact.first_name}}, quick one from {{custom_values.owner_first_name}} at {{custom_values.business_name}}: are you leaning toward a repair, a replacement, or just pricing it for now? One word is enough. Reply STOP to opt out."
519
+ },
520
+ {
521
+ "ref": "sms_template.n_sms_2",
522
+ "name": "Nurture Text 2 - Free Tip",
523
+ "body": "Tip from {{custom_values.business_name}}: ask every contractor the same question: \"What could add to this quote once you start?\" The honest ones have an answer. Want our 5-question checklist for comparing quotes? Reply YES."
524
+ },
525
+ {
526
+ "ref": "sms_template.n_sms_3",
527
+ "name": "Nurture Text 3 - Midpoint",
528
+ "body": "{{contact.first_name}}, it's {{custom_values.owner_first_name}}. Still thinking about {{offer.summary || the work you asked about}}? I have a couple of estimate slots this week. Want one, or should I check back next month?"
529
+ },
530
+ {
531
+ "ref": "sms_template.n_sms_4",
532
+ "name": "Nurture Text 4 - Any Question",
533
+ "body": "Hi {{contact.first_name}}, {{custom_values.owner_first_name}} here. No pitch, just checking: did you get your question answered? If not, send it here (a photo helps) and I'll reply personally."
534
+ },
535
+ {
536
+ "ref": "sms_template.n_sms_5",
537
+ "name": "Nurture Text 5 - Last Direct Ask",
538
+ "body": "{{contact.first_name}}, last one from me at {{custom_values.business_name}}. If you want a written quote this month, reply YES and I'll send two estimate times. If not, no hard feelings. Reply STOP to opt out."
539
+ },
540
+ {
541
+ "ref": "sms_template.w_sms_1",
542
+ "name": "Win-back Text 1",
543
+ "body": "Hi {{contact.first_name}}, it's {{custom_values.owner_first_name}} at {{custom_values.business_name}}. Is {{offer.summary || the work you asked about}} still on your list this season, or did you get it handled? Reply STOP to opt out."
544
+ },
545
+ {
546
+ "ref": "sms_template.w_sms_2",
547
+ "name": "Win-back Text 2",
548
+ "body": "{{contact.first_name}}, our calendar is open this week before the season picks up. Want a free estimate slot? Reply YES and I'll hold one. {{custom_values.business_name}}"
549
+ },
550
+ {
551
+ "ref": "sms_template.appt_confirm_sms",
552
+ "name": "Estimate Confirmed Text",
553
+ "body": "{{contact.first_name}}, your estimate with {{custom_values.business_name}} is confirmed for {{appointment.start_time}}. We'll text before we arrive. Need to change it? Reply here. Reply STOP to opt out."
554
+ },
555
+ {
556
+ "ref": "sms_template.appt_24h_sms",
557
+ "name": "Estimate Reminder - 24 Hours",
558
+ "body": "Reminder: your estimate with {{custom_values.business_name}} is tomorrow at {{appointment.start_time}}. Please make sure we can get to the area. Reply C to confirm or R to reschedule."
559
+ },
560
+ {
561
+ "ref": "sms_template.appt_2h_sms",
562
+ "name": "Estimate Reminder - 2 Hours",
563
+ "body": "{{contact.first_name}}, {{custom_values.business_name}} here. We'll be at your place around {{appointment.start_time}}, about 2 hours from now. Reply here if anything's changed."
564
+ },
565
+ {
566
+ "ref": "sms_template.ns_sms_1",
567
+ "name": "No-Show Text 1",
568
+ "body": "Hi {{contact.first_name}}, {{custom_values.business_name}} here. We came by for the estimate but couldn't connect today. No problem. Want me to find you a new time this week? Reply YES. Reply STOP to opt out."
569
+ },
570
+ {
571
+ "ref": "sms_template.ns_sms_2",
572
+ "name": "No-Show Text 2",
573
+ "body": "{{contact.first_name}}, {{custom_values.owner_first_name}} here. I kept a couple of estimate slots open for you this week. Want one? Reply with a day that works."
574
+ }
575
+ ]
576
+ },
577
+ "_templatesNote": "Every message is a send-ready template (emails: subject + html; sms: body) written to references/copy-guide.md from this preset's placeholder brief fields. The build stage REWRITES each one from the real brief (offer, audience pain points and objections, price points, lead magnet, follow-up style, voice.threeWords for tone, voice.signatureLine verbatim in the first email and text) keeping the cadence, the single CTA and the merge fields, then writes the result to BOTH the template and the matching email/sms asset body. Never ship an outline-only body. {{contact.*}}, {{custom_values.*}} and {{appointment.*}} are GHL merge fields and pass through the fill untouched; {{offer.*}} / {{audience.*}} / {{business.*}} are preset tokens filled from the brief.",
578
+ "emails": [
579
+ {
580
+ "ref": "email.stl_email_1",
581
+ "name": "Speed to Lead - Instant Email",
582
+ "subject": "Got your quote request, {{contact.first_name}}",
583
+ "templateRef": "email_template.stl_email_1",
584
+ "copyDirection": "Instant. Who I am, what they asked for, what happens next (a text; if urgent reply URGENT), the photo ask, the estimate booking CTA, the written-quote promise. Tone: brief.voice.threeWords. If brief.voice.signatureLine exists, insert it verbatim as its own paragraph right after the greeting.",
585
+ "mergeTags": [
586
+ "contact.first_name",
587
+ "custom_values.business_name",
588
+ "custom_values.owner_first_name",
589
+ "custom_values.booking_link",
590
+ "custom_values.business_phone"
591
+ ]
592
+ },
593
+ {
594
+ "ref": "email.stl_email_2",
595
+ "name": "Speed to Lead - 3 Hour Email",
596
+ "subject": "What a quote from us includes",
597
+ "templateRef": "email_template.stl_email_2",
598
+ "copyDirection": "Second touch, 3 hours in. What a written quote from us includes (itemized, materials, timeline, warranty, what is excluded), why that matters when comparing, CTA, reply invitation.",
599
+ "mergeTags": [
600
+ "contact.first_name",
601
+ "custom_values.business_name",
602
+ "custom_values.booking_link",
603
+ "custom_values.owner_first_name",
604
+ "custom_values.business_phone"
605
+ ]
606
+ },
607
+ {
608
+ "ref": "email.n_email_1",
609
+ "name": "Nurture 1 - The Cheapest Quote",
610
+ "subject": "Why the cheapest quote costs the most",
611
+ "templateRef": "email_template.n_email_1",
612
+ "copyDirection": "Day 1 of nurture. Reframe the price comparison: what gets left out of a low quote, how to read one, what to ask. Give, do not pitch.",
613
+ "mergeTags": [
614
+ "contact.first_name",
615
+ "custom_values.booking_link",
616
+ "custom_values.owner_first_name",
617
+ "custom_values.business_name",
618
+ "custom_values.business_phone"
619
+ ]
620
+ },
621
+ {
622
+ "ref": "email.n_email_2",
623
+ "name": "Nurture 2 - How the Estimate Works",
624
+ "subject": "How the estimate visit actually works",
625
+ "templateRef": "email_template.n_email_2",
626
+ "copyDirection": "Day 5. Remove the unknown: the estimate visit step by step (arrive, look, explain, written quote), how long, no pressure to sign on the spot. Name and answer the top objection. One CTA.",
627
+ "mergeTags": [
628
+ "contact.first_name",
629
+ "custom_values.business_name",
630
+ "custom_values.booking_link",
631
+ "custom_values.owner_first_name",
632
+ "custom_values.business_phone"
633
+ ]
634
+ },
635
+ {
636
+ "ref": "email.n_email_3",
637
+ "name": "Nurture 3 - The Three Questions",
638
+ "subject": "Three questions to ask any contractor",
639
+ "templateRef": "email_template.n_email_3",
640
+ "copyDirection": "Day 10. Pure value: three questions to ask anyone (licensed/insured proof, who actually does the work, what happens if it takes longer) and our answers. Only claim licensing / insurance if the brief confirms it. CTA + reply invite.",
641
+ "mergeTags": [
642
+ "contact.first_name",
643
+ "custom_values.booking_link",
644
+ "custom_values.owner_first_name",
645
+ "custom_values.business_name",
646
+ "custom_values.business_phone"
647
+ ]
648
+ },
649
+ {
650
+ "ref": "email.n_email_4",
651
+ "name": "Nurture 4 - About the Price",
652
+ "subject": "About the price",
653
+ "templateRef": "email_template.n_email_4",
654
+ "copyDirection": "Day 14. Handle cost head-on: itemized, options, deposit and payment terms in writing, financing only if the brief says it exists. No numbers unless the brief has them.",
655
+ "mergeTags": [
656
+ "contact.first_name",
657
+ "custom_values.booking_link",
658
+ "custom_values.owner_first_name",
659
+ "custom_values.business_name",
660
+ "custom_values.business_phone"
661
+ ]
662
+ },
663
+ {
664
+ "ref": "email.n_email_5",
665
+ "name": "Nurture 5 - What a Small Problem Becomes",
666
+ "subject": "What a small problem turns into",
667
+ "templateRef": "email_template.n_email_5",
668
+ "copyDirection": "Day 20. Cost of waiting, honestly: the small-fix stage vs the big-repair stage, no scare tactics. Soft CTA.",
669
+ "mergeTags": [
670
+ "contact.first_name",
671
+ "custom_values.booking_link",
672
+ "custom_values.owner_first_name",
673
+ "custom_values.business_name",
674
+ "custom_values.business_phone"
675
+ ]
676
+ },
677
+ {
678
+ "ref": "email.n_email_6",
679
+ "name": "Nurture 6 - Everything in One Place",
680
+ "subject": "Everything you need to decide, in one place",
681
+ "templateRef": "email_template.n_email_6",
682
+ "copyDirection": "Day 27. The decision sheet: what the estimate is, service area, what the quote includes, our rule, how to book or move. Offer the \"later\" out. One CTA.",
683
+ "mergeTags": [
684
+ "contact.first_name",
685
+ "custom_values.business_name",
686
+ "custom_values.booking_link",
687
+ "custom_values.owner_first_name",
688
+ "custom_values.business_phone"
689
+ ]
690
+ },
691
+ {
692
+ "ref": "email.w_email_1",
693
+ "name": "Win-back 1 - One Question",
694
+ "subject": "One question, {{contact.first_name}}",
695
+ "templateRef": "email_template.w_email_1",
696
+ "copyDirection": "Win-back day 0. Acknowledge the gap without guilt; ask what stopped them (went with someone else, price, timing, fixed it yourself); promise to fix it or leave them alone.",
697
+ "mergeTags": [
698
+ "contact.first_name",
699
+ "custom_values.business_name",
700
+ "custom_values.owner_first_name",
701
+ "custom_values.business_phone"
702
+ ]
703
+ },
704
+ {
705
+ "ref": "email.w_email_2",
706
+ "name": "Win-back 2 - Before the Season Turns",
707
+ "subject": "Before the season turns",
708
+ "templateRef": "email_template.w_email_2",
709
+ "copyDirection": "Win-back day 14. Seasonal reason to act (maintenance before the busy season, off-season availability) from the brief; what has not changed. One CTA.",
710
+ "mergeTags": [
711
+ "contact.first_name",
712
+ "custom_values.business_name",
713
+ "custom_values.booking_link",
714
+ "custom_values.owner_first_name",
715
+ "custom_values.business_phone"
716
+ ]
717
+ },
718
+ {
719
+ "ref": "email.w_email_3",
720
+ "name": "Win-back 3 - The Door Stays Open",
721
+ "subject": "The door stays open",
722
+ "templateRef": "email_template.w_email_3",
723
+ "copyDirection": "Win-back day 30, the last automatic message. Say so plainly, leave the offer open, one CTA, one useful parting tip.",
724
+ "mergeTags": [
725
+ "contact.first_name",
726
+ "custom_values.booking_link",
727
+ "custom_values.owner_first_name",
728
+ "custom_values.business_name",
729
+ "custom_values.business_phone"
730
+ ]
731
+ },
732
+ {
733
+ "ref": "email.appt_confirm_email",
734
+ "name": "Estimate Confirmed",
735
+ "subject": "Confirmed, {{contact.first_name}}: your estimate visit",
736
+ "templateRef": "email_template.appt_confirm_email",
737
+ "copyDirection": "On booking. Date/time via {{appointment.start_time}}, the address on file, what to have ready (access, photos, budget), what they get (written quote), how to move it.",
738
+ "mergeTags": [
739
+ "contact.first_name",
740
+ "custom_values.business_name",
741
+ "appointment.start_time",
742
+ "custom_values.booking_link",
743
+ "custom_values.owner_first_name",
744
+ "custom_values.business_phone"
745
+ ]
746
+ },
747
+ {
748
+ "ref": "email.appt_prep_email",
749
+ "name": "Estimate Prep (day before)",
750
+ "subject": "Tomorrow: 3 things that make the estimate quicker",
751
+ "templateRef": "email_template.appt_prep_email",
752
+ "copyDirection": "Day before. Three concrete prep items (access, photos, one question), the time window, how to reach us.",
753
+ "mergeTags": [
754
+ "contact.first_name",
755
+ "appointment.start_time",
756
+ "custom_values.business_phone",
757
+ "custom_values.owner_first_name",
758
+ "custom_values.business_name"
759
+ ]
760
+ },
761
+ {
762
+ "ref": "email.ns_email_1",
763
+ "name": "No-Show 1 - We Missed You",
764
+ "subject": "We missed you, {{contact.first_name}} (no guilt)",
765
+ "templateRef": "email_template.ns_email_1",
766
+ "copyDirection": "Same day. Zero guilt (nobody home / no access), the estimate is still free, rebook CTA, invite the honest reason.",
767
+ "mergeTags": [
768
+ "contact.first_name",
769
+ "custom_values.booking_link",
770
+ "custom_values.owner_first_name",
771
+ "custom_values.business_name",
772
+ "custom_values.business_phone"
773
+ ]
774
+ },
775
+ {
776
+ "ref": "email.ns_email_2",
777
+ "name": "No-Show 2 - Still Want This?",
778
+ "subject": "Still want the quote?",
779
+ "templateRef": "email_template.ns_email_2",
780
+ "copyDirection": "Day 4 after the no-show. Honest binary: wrong timing or still want it. One CTA, gracious close.",
781
+ "mergeTags": [
782
+ "contact.first_name",
783
+ "custom_values.booking_link",
784
+ "custom_values.business_name",
785
+ "custom_values.owner_first_name",
786
+ "custom_values.business_phone"
787
+ ]
788
+ }
789
+ ],
790
+ "sms": [
791
+ {
792
+ "ref": "sms.stl_sms_1",
793
+ "name": "Speed to Lead - Instant Text",
794
+ "templateRef": "sms_template.stl_sms_1",
795
+ "copyDirection": "Instant. Name + business, acknowledge, the one question that matters (urgent or can it wait), STOP line. No link. If brief.voice.signatureLine is short enough, use it verbatim as the second sentence.",
796
+ "mergeTags": [
797
+ "contact.first_name",
798
+ "custom_values.owner_first_name",
799
+ "custom_values.business_name"
800
+ ],
801
+ "conditionalOn": "channels.sms == true"
802
+ },
803
+ {
804
+ "ref": "sms.stl_sms_2",
805
+ "name": "Speed to Lead - 20 Minute Text",
806
+ "templateRef": "sms_template.stl_sms_2",
807
+ "copyDirection": "20 minutes in. Ask for a photo (saves a trip), offer the estimate link.",
808
+ "mergeTags": [
809
+ "contact.first_name",
810
+ "custom_values.booking_link"
811
+ ],
812
+ "conditionalOn": "channels.sms == true"
813
+ },
814
+ {
815
+ "ref": "sms.stl_sms_3",
816
+ "name": "Speed to Lead - Next Day Text",
817
+ "templateRef": "sms_template.stl_sms_3",
818
+ "copyDirection": "Day 1. Not chasing; binary: hold an estimate time or \"not now\". No link.",
819
+ "mergeTags": [
820
+ "contact.first_name",
821
+ "custom_values.owner_first_name"
822
+ ],
823
+ "conditionalOn": "channels.sms == true"
824
+ },
825
+ {
826
+ "ref": "sms.mc_sms_1",
827
+ "name": "Missed Call - Instant Text-Back",
828
+ "templateRef": "sms_template.mc_sms_1",
829
+ "copyDirection": "Seconds after a missed inbound call. Apologize, URGENT keyword for emergencies, ask what is going on, STOP line.",
830
+ "mergeTags": [
831
+ "custom_values.business_name"
832
+ ],
833
+ "conditionalOn": "channels.sms == true"
834
+ },
835
+ {
836
+ "ref": "sms.mc_sms_2",
837
+ "name": "Missed Call - 1 Hour Text",
838
+ "templateRef": "sms_template.mc_sms_2",
839
+ "copyDirection": "One hour later. Ask for a call-back time or offer the estimate link; include the phone number.",
840
+ "mergeTags": [
841
+ "custom_values.booking_link",
842
+ "custom_values.business_name",
843
+ "custom_values.business_phone"
844
+ ],
845
+ "conditionalOn": "channels.sms == true"
846
+ },
847
+ {
848
+ "ref": "sms.n_sms_1",
849
+ "name": "Nurture Text 1 - One Word",
850
+ "templateRef": "sms_template.n_sms_1",
851
+ "copyDirection": "Day 3. A one-word segmenting question (repair / replace / just pricing). STOP line.",
852
+ "mergeTags": [
853
+ "contact.first_name",
854
+ "custom_values.owner_first_name",
855
+ "custom_values.business_name"
856
+ ],
857
+ "conditionalOn": "channels.sms == true"
858
+ },
859
+ {
860
+ "ref": "sms.n_sms_2",
861
+ "name": "Nurture Text 2 - Free Tip",
862
+ "templateRef": "sms_template.n_sms_2",
863
+ "copyDirection": "Day 8. A genuinely useful tip (ask every contractor what could add to the quote), offer more on YES. No link.",
864
+ "mergeTags": [
865
+ "custom_values.business_name"
866
+ ],
867
+ "conditionalOn": "channels.sms == true"
868
+ },
869
+ {
870
+ "ref": "sms.n_sms_3",
871
+ "name": "Nurture Text 3 - Midpoint",
872
+ "templateRef": "sms_template.n_sms_3",
873
+ "copyDirection": "Day 15. Personal, from the owner; binary: an estimate slot this week or check back next month.",
874
+ "mergeTags": [
875
+ "contact.first_name",
876
+ "custom_values.owner_first_name"
877
+ ],
878
+ "conditionalOn": "channels.sms == true"
879
+ },
880
+ {
881
+ "ref": "sms.n_sms_4",
882
+ "name": "Nurture Text 4 - Any Question",
883
+ "templateRef": "sms_template.n_sms_4",
884
+ "copyDirection": "Day 23. No pitch: did they get their question answered? Invite a photo.",
885
+ "mergeTags": [
886
+ "contact.first_name",
887
+ "custom_values.owner_first_name"
888
+ ],
889
+ "conditionalOn": "channels.sms == true"
890
+ },
891
+ {
892
+ "ref": "sms.n_sms_5",
893
+ "name": "Nurture Text 5 - Last Direct Ask",
894
+ "templateRef": "sms_template.n_sms_5",
895
+ "copyDirection": "Day 30. Last one; YES gets two times; no hard feelings; STOP line.",
896
+ "mergeTags": [
897
+ "contact.first_name",
898
+ "custom_values.business_name"
899
+ ],
900
+ "conditionalOn": "channels.sms == true"
901
+ },
902
+ {
903
+ "ref": "sms.w_sms_1",
904
+ "name": "Win-back Text 1",
905
+ "templateRef": "sms_template.w_sms_1",
906
+ "copyDirection": "Win-back day 7. Binary: still on the list this season, or already handled. STOP line.",
907
+ "mergeTags": [
908
+ "contact.first_name",
909
+ "custom_values.owner_first_name",
910
+ "custom_values.business_name"
911
+ ],
912
+ "conditionalOn": "channels.sms == true"
913
+ },
914
+ {
915
+ "ref": "sms.w_sms_2",
916
+ "name": "Win-back Text 2",
917
+ "templateRef": "sms_template.w_sms_2",
918
+ "copyDirection": "Win-back day 22. Concrete seasonal reason (calendar open now) and a YES ask.",
919
+ "mergeTags": [
920
+ "contact.first_name",
921
+ "custom_values.business_name"
922
+ ],
923
+ "conditionalOn": "channels.sms == true"
924
+ },
925
+ {
926
+ "ref": "sms.appt_confirm_sms",
927
+ "name": "Estimate Confirmed Text",
928
+ "templateRef": "sms_template.appt_confirm_sms",
929
+ "copyDirection": "On booking. Confirm date/time, we text before arriving, how to change it, STOP line.",
930
+ "mergeTags": [
931
+ "contact.first_name",
932
+ "custom_values.business_name",
933
+ "appointment.start_time"
934
+ ],
935
+ "conditionalOn": "channels.sms == true"
936
+ },
937
+ {
938
+ "ref": "sms.appt_24h_sms",
939
+ "name": "Estimate Reminder - 24 Hours",
940
+ "templateRef": "sms_template.appt_24h_sms",
941
+ "copyDirection": "24 hours before. Time, make sure we can access the area, C to confirm / R to reschedule.",
942
+ "mergeTags": [
943
+ "custom_values.business_name",
944
+ "appointment.start_time"
945
+ ],
946
+ "conditionalOn": "channels.sms == true"
947
+ },
948
+ {
949
+ "ref": "sms.appt_2h_sms",
950
+ "name": "Estimate Reminder - 2 Hours",
951
+ "templateRef": "sms_template.appt_2h_sms",
952
+ "copyDirection": "2 hours before. Short: on our way soon, reply if plans changed.",
953
+ "mergeTags": [
954
+ "contact.first_name",
955
+ "custom_values.business_name",
956
+ "appointment.start_time"
957
+ ],
958
+ "conditionalOn": "channels.sms == true"
959
+ },
960
+ {
961
+ "ref": "sms.ns_sms_1",
962
+ "name": "No-Show Text 1",
963
+ "templateRef": "sms_template.ns_sms_1",
964
+ "copyDirection": "Same day. No guilt (could not connect), offer a new time this week, YES ask, STOP line.",
965
+ "mergeTags": [
966
+ "contact.first_name",
967
+ "custom_values.business_name"
968
+ ],
969
+ "conditionalOn": "channels.sms == true"
970
+ },
971
+ {
972
+ "ref": "sms.ns_sms_2",
973
+ "name": "No-Show Text 2",
974
+ "templateRef": "sms_template.ns_sms_2",
975
+ "copyDirection": "Next day. Owner kept slots open; ask for a day.",
976
+ "mergeTags": [
977
+ "contact.first_name",
978
+ "custom_values.owner_first_name"
979
+ ],
980
+ "conditionalOn": "channels.sms == true"
981
+ }
982
+ ],
983
+ "workflows": [
984
+ {
985
+ "ref": "workflow.speed_to_lead",
986
+ "name": "Speed to Lead",
987
+ "trigger": {
988
+ "type": "form_submission",
989
+ "formRef": "form.intake"
990
+ },
991
+ "stopOnResponse": true,
992
+ "_design": "Five touches in the first 24 hours (text + email instantly, the contact assigned to the person who takes calls, a call task and an alert to a real person, an opportunity card, then a 2nd and 3rd touch), then the hand-off tag that starts the 30-day nurture as the LAST action (E_NO_HANDOFF). Ends early only if they reply (stopOnResponse) or book (the booking workflow pulls them out).",
993
+ "actions": [
994
+ {
995
+ "type": "add_contact_tag",
996
+ "tagRef": "tag.lc_new_lead"
997
+ },
998
+ {
999
+ "type": "create_opportunity",
1000
+ "pipelineRef": "pipeline.jobs",
1001
+ "stageRef": "stage.new_lead",
1002
+ "name": "{{contact.name}} - New Lead"
1003
+ },
1004
+ {
1005
+ "type": "assign_user",
1006
+ "userRef": "user.owner"
1007
+ },
1008
+ {
1009
+ "type": "send_sms",
1010
+ "smsRef": "sms.stl_sms_1",
1011
+ "conditionalOn": "channels.sms == true"
1012
+ },
1013
+ {
1014
+ "type": "send_email",
1015
+ "emailRef": "email.stl_email_1"
1016
+ },
1017
+ {
1018
+ "type": "internal_notification",
1019
+ "userRef": "user.owner",
1020
+ "title": "New lead: {{contact.first_name}} {{contact.last_name}}",
1021
+ "body": "{{contact.first_name}} just requested a quote. Phone {{contact.phone}}. Check the urgency field first: if it is an emergency, call NOW; otherwise call within 5 minutes. The first text and email already went out."
1022
+ },
1023
+ {
1024
+ "type": "task_notification",
1025
+ "userRef": "user.owner",
1026
+ "title": "Call {{contact.first_name}} now (new lead)",
1027
+ "body": "Speed-to-lead call. {{contact.first_name}} {{contact.last_name}}, {{contact.phone}}, {{contact.email}}. Ask: what is the problem, how urgent, and can they text a photo. Then hold an estimate time. Log the outcome in the contact notes.",
1028
+ "dueDate": "1"
1029
+ },
1030
+ {
1031
+ "type": "wait",
1032
+ "value": 20,
1033
+ "unit": "minutes"
1034
+ },
1035
+ {
1036
+ "type": "send_sms",
1037
+ "smsRef": "sms.stl_sms_2",
1038
+ "conditionalOn": "channels.sms == true"
1039
+ },
1040
+ {
1041
+ "type": "wait",
1042
+ "value": 3,
1043
+ "unit": "hours"
1044
+ },
1045
+ {
1046
+ "type": "send_email",
1047
+ "emailRef": "email.stl_email_2"
1048
+ },
1049
+ {
1050
+ "type": "wait",
1051
+ "value": 1,
1052
+ "unit": "days"
1053
+ },
1054
+ {
1055
+ "type": "send_sms",
1056
+ "smsRef": "sms.stl_sms_3",
1057
+ "conditionalOn": "channels.sms == true"
1058
+ },
1059
+ {
1060
+ "type": "add_notes",
1061
+ "body": "Speed to Lead finished with no reply (5 touches over 24 hours). Handed off to Lead Nurture (30 days) by the nurture-start tag."
1062
+ },
1063
+ {
1064
+ "type": "add_contact_tag",
1065
+ "tagRef": "tag.nurture_start"
1066
+ }
1067
+ ]
1068
+ },
1069
+ {
1070
+ "ref": "workflow.missed_call",
1071
+ "name": "Missed Call Text-Back",
1072
+ "conditionalOn": "channels.sms == true",
1073
+ "trigger": {
1074
+ "type": "call_status",
1075
+ "callStatuses": [
1076
+ "no-answer",
1077
+ "busy",
1078
+ "voicemail"
1079
+ ],
1080
+ "callDirection": "inbound"
1081
+ },
1082
+ "stopOnResponse": true,
1083
+ "_design": "Fires on an inbound call that was not answered (no-answer / busy / voicemail). Texts back within seconds, tags the contact, alerts a real person and creates a call-back task, then one more text an hour later. Stops the moment they reply.",
1084
+ "actions": [
1085
+ {
1086
+ "type": "send_sms",
1087
+ "smsRef": "sms.mc_sms_1",
1088
+ "conditionalOn": "channels.sms == true"
1089
+ },
1090
+ {
1091
+ "type": "add_contact_tag",
1092
+ "tagRef": "tag.missed_call"
1093
+ },
1094
+ {
1095
+ "type": "add_contact_tag",
1096
+ "tagRef": "tag.source_phone"
1097
+ },
1098
+ {
1099
+ "type": "internal_notification",
1100
+ "userRef": "user.owner",
1101
+ "title": "Missed call: {{contact.first_name}} {{contact.phone}}",
1102
+ "body": "An inbound call from {{contact.phone}} was not answered. A text-back already went out. Call back as soon as you can."
1103
+ },
1104
+ {
1105
+ "type": "task_notification",
1106
+ "userRef": "user.owner",
1107
+ "title": "Call back {{contact.first_name}} (missed call)",
1108
+ "body": "Missed inbound call from {{contact.phone}}. The text-back asked what they need; check the conversation first, then call.",
1109
+ "dueDate": "1"
1110
+ },
1111
+ {
1112
+ "type": "wait",
1113
+ "value": 1,
1114
+ "unit": "hours"
1115
+ },
1116
+ {
1117
+ "type": "send_sms",
1118
+ "smsRef": "sms.mc_sms_2",
1119
+ "conditionalOn": "channels.sms == true"
1120
+ }
1121
+ ]
1122
+ },
1123
+ {
1124
+ "ref": "workflow.lead_nurture",
1125
+ "name": "Lead Nurture (30 days)",
1126
+ "trigger": {
1127
+ "type": "contact_tag",
1128
+ "tagRef": "tag.nurture_start"
1129
+ },
1130
+ "stopOnResponse": true,
1131
+ "_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.",
1132
+ "actions": [
1133
+ {
1134
+ "type": "remove_contact_tag",
1135
+ "tagRef": "tag.nurture_start"
1136
+ },
1137
+ {
1138
+ "type": "add_contact_tag",
1139
+ "tagRef": "tag.lc_nurturing"
1140
+ },
1141
+ {
1142
+ "type": "wait",
1143
+ "value": 1,
1144
+ "unit": "days"
1145
+ },
1146
+ {
1147
+ "type": "send_email",
1148
+ "emailRef": "email.n_email_1"
1149
+ },
1150
+ {
1151
+ "type": "wait",
1152
+ "value": 2,
1153
+ "unit": "days"
1154
+ },
1155
+ {
1156
+ "type": "send_sms",
1157
+ "smsRef": "sms.n_sms_1",
1158
+ "conditionalOn": "channels.sms == true"
1159
+ },
1160
+ {
1161
+ "type": "wait",
1162
+ "value": 2,
1163
+ "unit": "days"
1164
+ },
1165
+ {
1166
+ "type": "send_email",
1167
+ "emailRef": "email.n_email_2"
1168
+ },
1169
+ {
1170
+ "type": "wait",
1171
+ "value": 3,
1172
+ "unit": "days"
1173
+ },
1174
+ {
1175
+ "type": "send_sms",
1176
+ "smsRef": "sms.n_sms_2",
1177
+ "conditionalOn": "channels.sms == true"
1178
+ },
1179
+ {
1180
+ "type": "wait",
1181
+ "value": 2,
1182
+ "unit": "days"
1183
+ },
1184
+ {
1185
+ "type": "send_email",
1186
+ "emailRef": "email.n_email_3"
1187
+ },
1188
+ {
1189
+ "type": "wait",
1190
+ "value": 4,
1191
+ "unit": "days"
1192
+ },
1193
+ {
1194
+ "type": "send_email",
1195
+ "emailRef": "email.n_email_4"
1196
+ },
1197
+ {
1198
+ "type": "wait",
1199
+ "value": 1,
1200
+ "unit": "days"
1201
+ },
1202
+ {
1203
+ "type": "send_sms",
1204
+ "smsRef": "sms.n_sms_3",
1205
+ "conditionalOn": "channels.sms == true"
1206
+ },
1207
+ {
1208
+ "type": "wait",
1209
+ "value": 5,
1210
+ "unit": "days"
1211
+ },
1212
+ {
1213
+ "type": "send_email",
1214
+ "emailRef": "email.n_email_5"
1215
+ },
1216
+ {
1217
+ "type": "wait",
1218
+ "value": 3,
1219
+ "unit": "days"
1220
+ },
1221
+ {
1222
+ "type": "send_sms",
1223
+ "smsRef": "sms.n_sms_4",
1224
+ "conditionalOn": "channels.sms == true"
1225
+ },
1226
+ {
1227
+ "type": "wait",
1228
+ "value": 4,
1229
+ "unit": "days"
1230
+ },
1231
+ {
1232
+ "type": "send_email",
1233
+ "emailRef": "email.n_email_6"
1234
+ },
1235
+ {
1236
+ "type": "wait",
1237
+ "value": 3,
1238
+ "unit": "days"
1239
+ },
1240
+ {
1241
+ "type": "send_sms",
1242
+ "smsRef": "sms.n_sms_5",
1243
+ "conditionalOn": "channels.sms == true"
1244
+ },
1245
+ {
1246
+ "type": "wait",
1247
+ "value": 1,
1248
+ "unit": "days"
1249
+ },
1250
+ {
1251
+ "type": "add_notes",
1252
+ "body": "Lead Nurture (30 days) finished with no reply: 11 touches over 31 days. Tagged lapsed; Win-back starts from the winback-start tag."
1253
+ },
1254
+ {
1255
+ "type": "add_contact_tag",
1256
+ "tagRef": "tag.lc_lapsed"
1257
+ },
1258
+ {
1259
+ "type": "add_contact_tag",
1260
+ "tagRef": "tag.winback_start"
1261
+ }
1262
+ ]
1263
+ },
1264
+ {
1265
+ "ref": "workflow.reply_route",
1266
+ "name": "Replied - Stop & Route",
1267
+ "trigger": {
1268
+ "type": "customer_reply"
1269
+ },
1270
+ "stopOnResponse": false,
1271
+ "_design": "The exit workflow. The moment a contact replies on any channel: pull them out of every sequence, write a note, mark contacted, alert a real person and create a reply task with a 15-minute target.",
1272
+ "actions": [
1273
+ {
1274
+ "type": "remove_from_workflow",
1275
+ "workflowRef": "workflow.speed_to_lead"
1276
+ },
1277
+ {
1278
+ "type": "remove_from_workflow",
1279
+ "workflowRef": "workflow.missed_call",
1280
+ "conditionalOn": "channels.sms == true"
1281
+ },
1282
+ {
1283
+ "type": "remove_from_workflow",
1284
+ "workflowRef": "workflow.lead_nurture"
1285
+ },
1286
+ {
1287
+ "type": "remove_from_workflow",
1288
+ "workflowRef": "workflow.win_back"
1289
+ },
1290
+ {
1291
+ "type": "remove_from_workflow",
1292
+ "workflowRef": "workflow.no_show",
1293
+ "conditionalOn": "goal.bookingNeeded == true"
1294
+ },
1295
+ {
1296
+ "type": "add_notes",
1297
+ "body": "Contact replied. Removed from Speed to Lead, Missed Call Text-Back, Lead Nurture, Win-back and No-Show Rescue. A human owns the conversation from here."
1298
+ },
1299
+ {
1300
+ "type": "add_contact_tag",
1301
+ "tagRef": "tag.lc_contacted"
1302
+ },
1303
+ {
1304
+ "type": "internal_notification",
1305
+ "userRef": "user.owner",
1306
+ "title": "{{contact.first_name}} replied",
1307
+ "body": "{{contact.first_name}} {{contact.last_name}} replied. All automated sequences stopped. Answer within 15 minutes: {{contact.phone}}"
1308
+ },
1309
+ {
1310
+ "type": "task_notification",
1311
+ "userRef": "user.owner",
1312
+ "title": "Reply to {{contact.first_name}} (15-minute target)",
1313
+ "body": "They replied to a message. Read the conversation, answer the question they asked, and offer one concrete time.",
1314
+ "dueDate": "1"
1315
+ }
1316
+ ]
1317
+ },
1318
+ {
1319
+ "ref": "workflow.win_back",
1320
+ "name": "Win-back (30 days)",
1321
+ "trigger": {
1322
+ "type": "contact_tag",
1323
+ "tagRef": "tag.winback_start"
1324
+ },
1325
+ "stopOnResponse": true,
1326
+ "_design": "Starts from the Lead Nurture (or No-Show Rescue) hand-off tag. 5 touches across 31 days of waits, low pressure, one reason to come back per message. Ends by tagging lost. Any reply stops it.",
1327
+ "actions": [
1328
+ {
1329
+ "type": "remove_contact_tag",
1330
+ "tagRef": "tag.winback_start"
1331
+ },
1332
+ {
1333
+ "type": "send_email",
1334
+ "emailRef": "email.w_email_1"
1335
+ },
1336
+ {
1337
+ "type": "wait",
1338
+ "value": 7,
1339
+ "unit": "days"
1340
+ },
1341
+ {
1342
+ "type": "send_sms",
1343
+ "smsRef": "sms.w_sms_1",
1344
+ "conditionalOn": "channels.sms == true"
1345
+ },
1346
+ {
1347
+ "type": "wait",
1348
+ "value": 7,
1349
+ "unit": "days"
1350
+ },
1351
+ {
1352
+ "type": "send_email",
1353
+ "emailRef": "email.w_email_2"
1354
+ },
1355
+ {
1356
+ "type": "wait",
1357
+ "value": 8,
1358
+ "unit": "days"
1359
+ },
1360
+ {
1361
+ "type": "send_sms",
1362
+ "smsRef": "sms.w_sms_2",
1363
+ "conditionalOn": "channels.sms == true"
1364
+ },
1365
+ {
1366
+ "type": "wait",
1367
+ "value": 8,
1368
+ "unit": "days"
1369
+ },
1370
+ {
1371
+ "type": "send_email",
1372
+ "emailRef": "email.w_email_3"
1373
+ },
1374
+ {
1375
+ "type": "wait",
1376
+ "value": 1,
1377
+ "unit": "days"
1378
+ },
1379
+ {
1380
+ "type": "add_notes",
1381
+ "body": "Win-back finished with no reply: 5 touches over 31 days. Tagged lost. The contact stays in the database for future campaigns; nothing else sends automatically."
1382
+ },
1383
+ {
1384
+ "type": "add_contact_tag",
1385
+ "tagRef": "tag.lc_lost"
1386
+ }
1387
+ ]
1388
+ },
1389
+ {
1390
+ "ref": "workflow.appointment_booked",
1391
+ "name": "Estimate Booked - Confirm & Move",
1392
+ "trigger": {
1393
+ "type": "appointment",
1394
+ "appointmentStatus": "confirmed",
1395
+ "calendarRef": "calendar.estimate"
1396
+ },
1397
+ "stopOnResponse": false,
1398
+ "_design": "On a confirmed booking: stop every sequence, tag booked, alert a real person, send the confirmation email and text, then find the opportunity and move it to the booked stage (create it there if none exists).",
1399
+ "actions": [
1400
+ {
1401
+ "type": "remove_from_workflow",
1402
+ "workflowRef": "workflow.speed_to_lead"
1403
+ },
1404
+ {
1405
+ "type": "remove_from_workflow",
1406
+ "workflowRef": "workflow.lead_nurture"
1407
+ },
1408
+ {
1409
+ "type": "remove_from_workflow",
1410
+ "workflowRef": "workflow.win_back"
1411
+ },
1412
+ {
1413
+ "type": "add_contact_tag",
1414
+ "tagRef": "tag.lc_booked"
1415
+ },
1416
+ {
1417
+ "type": "internal_notification",
1418
+ "userRef": "user.owner",
1419
+ "title": "Estimate booked: {{contact.first_name}}",
1420
+ "body": "{{contact.first_name}} {{contact.last_name}} booked an estimate for {{appointment.start_time}}. Confirmation sent."
1421
+ },
1422
+ {
1423
+ "type": "send_email",
1424
+ "emailRef": "email.appt_confirm_email"
1425
+ },
1426
+ {
1427
+ "type": "send_sms",
1428
+ "smsRef": "sms.appt_confirm_sms",
1429
+ "conditionalOn": "channels.sms == true"
1430
+ },
1431
+ {
1432
+ "type": "find_opportunity",
1433
+ "pipelineRef": "pipeline.jobs",
1434
+ "found": [
1435
+ {
1436
+ "type": "update_opportunity",
1437
+ "pipelineRef": "pipeline.jobs",
1438
+ "stageRef": "stage.estimate_scheduled"
1439
+ }
1440
+ ],
1441
+ "notFound": [
1442
+ {
1443
+ "type": "create_opportunity",
1444
+ "pipelineRef": "pipeline.jobs",
1445
+ "stageRef": "stage.estimate_scheduled",
1446
+ "name": "{{contact.name}} - Estimate Booked"
1447
+ }
1448
+ ]
1449
+ }
1450
+ ],
1451
+ "conditionalOn": "goal.bookingNeeded == true"
1452
+ },
1453
+ {
1454
+ "ref": "workflow.appointment_reminders",
1455
+ "name": "Estimate Reminders",
1456
+ "trigger": {
1457
+ "type": "appointment",
1458
+ "appointmentStatus": "confirmed",
1459
+ "calendarRef": "calendar.estimate"
1460
+ },
1461
+ "stopOnResponse": false,
1462
+ "_design": "Same trigger as Confirm & Move, kept separate so the timers stay simple: 24 hours before (text + prep email) and 2 hours before (text). Appointment-relative waits only work under an appointment trigger.",
1463
+ "actions": [
1464
+ {
1465
+ "type": "wait_appointment",
1466
+ "value": 1,
1467
+ "unit": "days"
1468
+ },
1469
+ {
1470
+ "type": "send_sms",
1471
+ "smsRef": "sms.appt_24h_sms",
1472
+ "conditionalOn": "channels.sms == true"
1473
+ },
1474
+ {
1475
+ "type": "send_email",
1476
+ "emailRef": "email.appt_prep_email"
1477
+ },
1478
+ {
1479
+ "type": "wait_appointment",
1480
+ "value": 2,
1481
+ "unit": "hours"
1482
+ },
1483
+ {
1484
+ "type": "send_sms",
1485
+ "smsRef": "sms.appt_2h_sms",
1486
+ "conditionalOn": "channels.sms == true"
1487
+ }
1488
+ ],
1489
+ "conditionalOn": "goal.bookingNeeded == true"
1490
+ },
1491
+ {
1492
+ "ref": "workflow.no_show",
1493
+ "name": "No-Show Rescue",
1494
+ "trigger": {
1495
+ "type": "appointment",
1496
+ "appointmentStatus": "noshow",
1497
+ "calendarRef": "calendar.estimate"
1498
+ },
1499
+ "stopOnResponse": true,
1500
+ "_design": "On a no-show: tag, alert, call-back task, a warm text and email the same day, a second text next day, a last email 3 days later; then tag lapsed, hand off to Win-back, and move the opportunity back to Contacted (find_opportunity is last because its branches do not rejoin).",
1501
+ "actions": [
1502
+ {
1503
+ "type": "add_contact_tag",
1504
+ "tagRef": "tag.no_show"
1505
+ },
1506
+ {
1507
+ "type": "internal_notification",
1508
+ "userRef": "user.owner",
1509
+ "title": "No-show: {{contact.first_name}}",
1510
+ "body": "{{contact.first_name}} {{contact.last_name}} missed their estimate. A rebook text and email went out. Call-back task created."
1511
+ },
1512
+ {
1513
+ "type": "task_notification",
1514
+ "userRef": "user.owner",
1515
+ "title": "Call {{contact.first_name}} (no-show)",
1516
+ "body": "Missed estimate. Call, no guilt, offer two new times.",
1517
+ "dueDate": "1"
1518
+ },
1519
+ {
1520
+ "type": "send_sms",
1521
+ "smsRef": "sms.ns_sms_1",
1522
+ "conditionalOn": "channels.sms == true"
1523
+ },
1524
+ {
1525
+ "type": "send_email",
1526
+ "emailRef": "email.ns_email_1"
1527
+ },
1528
+ {
1529
+ "type": "wait",
1530
+ "value": 1,
1531
+ "unit": "days"
1532
+ },
1533
+ {
1534
+ "type": "send_sms",
1535
+ "smsRef": "sms.ns_sms_2",
1536
+ "conditionalOn": "channels.sms == true"
1537
+ },
1538
+ {
1539
+ "type": "wait",
1540
+ "value": 3,
1541
+ "unit": "days"
1542
+ },
1543
+ {
1544
+ "type": "send_email",
1545
+ "emailRef": "email.ns_email_2"
1546
+ },
1547
+ {
1548
+ "type": "wait",
1549
+ "value": 3,
1550
+ "unit": "days"
1551
+ },
1552
+ {
1553
+ "type": "add_notes",
1554
+ "body": "No-Show Rescue finished with no reply (4 touches over 7 days). Tagged lapsed; Win-back starts from the winback-start tag."
1555
+ },
1556
+ {
1557
+ "type": "add_contact_tag",
1558
+ "tagRef": "tag.lc_lapsed"
1559
+ },
1560
+ {
1561
+ "type": "add_contact_tag",
1562
+ "tagRef": "tag.winback_start"
1563
+ },
1564
+ {
1565
+ "type": "find_opportunity",
1566
+ "pipelineRef": "pipeline.jobs",
1567
+ "found": [
1568
+ {
1569
+ "type": "update_opportunity",
1570
+ "pipelineRef": "pipeline.jobs",
1571
+ "stageRef": "stage.contacted"
1572
+ }
1573
+ ],
1574
+ "notFound": []
1575
+ }
1576
+ ],
1577
+ "conditionalOn": "goal.bookingNeeded == true"
1578
+ }
1579
+ ],
1580
+ "handoffs": [
1581
+ {
1582
+ "ref": "handoff.add_staff",
1583
+ "conditionalOn": "staff is empty",
1584
+ "owner": "OPERATOR-UI",
1585
+ "title": "Add the people who get lead alerts and call tasks",
1586
+ "trigger": "the brief listed no staff",
1587
+ "instruction": "In the sub-account go to Settings > My Staff and add at least one user (yourself is fine): name, email, role. Every \"alert a real person\" and \"call task\" step in the workflows is already built and waiting; the moment a user exists, open each workflow and pick them in the Notify and Task steps (or re-run the build with useSavedPlan:true so the pending steps are re-pointed).",
1588
+ "produces": null,
1589
+ "successCheck": "At least one user exists and no workflow step still shows \"user pending\"",
1590
+ "blocks": [
1591
+ "workflow.speed_to_lead",
1592
+ "workflow.reply_route",
1593
+ "workflow.appointment_booked",
1594
+ "workflow.no_show",
1595
+ "workflow.missed_call"
1596
+ ],
1597
+ "_blocksNote": "Workflows with a notification or task pointed at user.__pending__ stay DRAFT until a real user is assigned. Prune entries whose workflow was dropped by conditional pruning."
1598
+ },
1599
+ {
1600
+ "ref": "handoff.phone_number",
1601
+ "conditionalOn": "flags includes phone_number_needed",
1602
+ "owner": "OPERATOR-UI",
1603
+ "title": "Get a phone number in GoHighLevel (so texts and missed-call text-back can work)",
1604
+ "trigger": "channels.sms == true && channels.hasPhoneNumber != yes",
1605
+ "instruction": "Settings > Phone Numbers > Add Number: buy a local number for the business, then paste it into the business_phone custom value. Every text step and the missed-call text-back are built and wait for this; A2P registration (next) needs the number too.",
1606
+ "produces": "cv.business_phone",
1607
+ "successCheck": "A number shows under Phone Numbers and business_phone matches it",
1608
+ "blocks": [
1609
+ "workflow.speed_to_lead",
1610
+ "workflow.missed_call",
1611
+ "workflow.lead_nurture",
1612
+ "workflow.win_back",
1613
+ "workflow.appointment_booked",
1614
+ "workflow.appointment_reminders",
1615
+ "workflow.no_show"
1616
+ ]
1617
+ },
1618
+ {
1619
+ "ref": "handoff.business_details",
1620
+ "owner": "OPERATOR-UI",
1621
+ "title": "Confirm your business phone and the name messages are signed with",
1622
+ "trigger": "always (30 seconds)",
1623
+ "instruction": "Settings > Custom Values: set business_phone to the number customers should call or text back, and owner_first_name to the first name every email is signed with. The build pre-fills both from the intake when it can.",
1624
+ "produces": "cv.owner_first_name",
1625
+ "successCheck": "business_phone and owner_first_name show real values, not blanks",
1626
+ "blocks": [
1627
+ "cv.owner_first_name"
1628
+ ]
1629
+ },
1630
+ {
1631
+ "ref": "handoff.calendar_oauth",
1632
+ "conditionalOn": "flags includes calendar_oauth_needed && goal.bookingNeeded == true",
1633
+ "owner": "OPERATOR-UI",
1634
+ "title": "Connect your calendar (Google / Outlook)",
1635
+ "trigger": "goal.bookingNeeded == true && channels.calendarConnected == false",
1636
+ "instruction": "Open the Estimate Visit calendar > Connections and authorize your Google or Outlook account. This login can only be approved by you. Then copy the booking link into the booking_link custom value.",
1637
+ "produces": "cv.booking_link",
1638
+ "successCheck": "Calendar shows a connected external account and the booking link resolves",
1639
+ "blocks": [
1640
+ "cv.booking_link"
1641
+ ]
1642
+ },
1643
+ {
1644
+ "ref": "handoff.assign_calendar_staff",
1645
+ "conditionalOn": "business.teamSize != 'Just me' && goal.bookingNeeded == true",
1646
+ "owner": "OPERATOR-UI",
1647
+ "title": "Assign who takes estimates on the calendar",
1648
+ "trigger": "a team account: Blueprint will not guess who books",
1649
+ "instruction": "Open the Estimate Visit calendar > Team and add the people who take these appointments. Blueprint auto-assigns only when the account has exactly one user.",
1650
+ "produces": null,
1651
+ "successCheck": "The calendar has at least one assigned team member",
1652
+ "blocks": [
1653
+ "calendar.estimate"
1654
+ ]
1655
+ },
1656
+ {
1657
+ "ref": "handoff.a2p",
1658
+ "conditionalOn": "flags includes needs_a2p",
1659
+ "owner": "OPERATOR-EXT",
1660
+ "title": "Register A2P 10DLC (so texts can send)",
1661
+ "trigger": "channels.sms == true",
1662
+ "instruction": "Submit your A2P brand and campaign through the GHL Trust Center / your carrier. Every text step is built but the SMS-bearing workflows stay DRAFT and will not send until this is approved (7-14 day review). Save your full submission package before submitting.",
1663
+ "produces": null,
1664
+ "successCheck": "A2P campaign status shows submitted or approved",
1665
+ "blocks": [
1666
+ "workflow.speed_to_lead",
1667
+ "workflow.missed_call",
1668
+ "workflow.lead_nurture",
1669
+ "workflow.win_back",
1670
+ "workflow.appointment_booked",
1671
+ "workflow.appointment_reminders",
1672
+ "workflow.no_show",
1673
+ "sms.stl_sms_1",
1674
+ "sms.stl_sms_2",
1675
+ "sms.stl_sms_3",
1676
+ "sms.mc_sms_1",
1677
+ "sms.mc_sms_2",
1678
+ "sms.n_sms_1",
1679
+ "sms.n_sms_2",
1680
+ "sms.n_sms_3",
1681
+ "sms.n_sms_4",
1682
+ "sms.n_sms_5",
1683
+ "sms.w_sms_1",
1684
+ "sms.w_sms_2",
1685
+ "sms.appt_confirm_sms",
1686
+ "sms.appt_24h_sms",
1687
+ "sms.appt_2h_sms",
1688
+ "sms.ns_sms_1",
1689
+ "sms.ns_sms_2"
1690
+ ],
1691
+ "_blocksNote": "Lists every SMS-bearing WORKFLOW ref (so they stay DRAFT) plus the sms.* asset refs. After conditional pruning, drop any entry whose workflow/asset was removed."
1692
+ },
1693
+ {
1694
+ "ref": "handoff.stripe",
1695
+ "conditionalOn": "flags includes stripe_not_connected",
1696
+ "owner": "OPERATOR-EXT",
1697
+ "title": "Connect Stripe (deposits and invoices)",
1698
+ "trigger": "channels.payment != 'Stripe connected'",
1699
+ "instruction": "Connect Stripe under Payments > Integrations so deposits and invoices can be taken from the quote. The funnel is built; the money connection is yours to authorize.",
1700
+ "produces": null,
1701
+ "successCheck": "Stripe shows connected",
1702
+ "blocks": []
1703
+ },
1704
+ {
1705
+ "ref": "handoff.email_domain",
1706
+ "conditionalOn": "flags includes email_domain_needed",
1707
+ "owner": "OPERATOR-UI",
1708
+ "title": "Set up your sending email",
1709
+ "trigger": "channels.email == false",
1710
+ "instruction": "Connect a sending domain / mailbox in Settings > Email Services so every email below sends from your address instead of a default.",
1711
+ "produces": null,
1712
+ "successCheck": "A verified sending domain / from-address exists",
1713
+ "blocks": [
1714
+ "email.stl_email_1",
1715
+ "email.stl_email_2",
1716
+ "email.n_email_1",
1717
+ "email.n_email_2",
1718
+ "email.n_email_3",
1719
+ "email.n_email_4",
1720
+ "email.n_email_5",
1721
+ "email.n_email_6",
1722
+ "email.w_email_1",
1723
+ "email.w_email_2",
1724
+ "email.w_email_3",
1725
+ "email.appt_confirm_email",
1726
+ "email.appt_prep_email",
1727
+ "email.ns_email_1",
1728
+ "email.ns_email_2"
1729
+ ]
1730
+ }
1731
+ ]
1732
+ }
1733
+ }