# Preset Format & Versioning

STATUS: v2, 2026-08-26 (v1 2026-06-15, owner atlas). Defines how Blueprint presets are authored, versioned, and consumed by the plan-gen skill. Ratifies schema §7 item 4 (preset → plan determinism) and adds the Tier 1 v2 layer: users, templates, `userRef`, the tag hand-off, and the depth rules every preset must meet.

## What a preset is

A preset is a **build-plan template**: it is authored in the exact vocabulary of the §5 build-plan (refs, logical actions, handoffs), with two additions — **template tokens** that the skill fills from the brief, and **`conditionalOn`** flags that the skill evaluates to add or drop items. The preset sets the *skeleton*; the brief fills the *specifics*.

This keeps preset → plan **deterministic** and keeps the preset in the same shape the operator reviews and the phase-2 executor consumes. There is no separate "preset language" to learn beyond §5 + the two additions below.

> Ratification of §7 #4: **the preset fixes the skeleton** (pipeline stage *set*, the workflow *set*, the asset *list*, the handoff *set*, the complete copy). **The brief fills** names, people, copy specifics, toggles, and any list the brief overrides (stages from `goal.salesStages`, users from `team.staff`, calendars from `calendars[]`). **Brief `flags` add or drop conditional items.** The skill does not invent structure the preset does not declare; it only fills, drops, expands, and rewrites copy. Same brief + same preset version → same plan skeleton, every time.

## What v2 depth means (owner review, 2026-08-26)

"Every single WF is so short, they are not doing much for a business." Every preset in the library now ships:

| Workflow | Minimum | What the library ships |
|---|---|---|
| Speed to Lead | 10+ actions: instant text + email, call task to a real user, staff alert, opportunity card, 2nd and 3rd touch, and a hand-off into the nurture as the **last** action | 14 actions, 5 touches in 24 h, `assign_user`, then `add_contact_tag nurture-start` |
| Missed Call Text-Back | `call_status` trigger scoped to no-answer / busy / voicemail | 7 actions: text-back, tag, alert, call-back task, +1 h text |
| Lead Nurture | **never under 30 days of waits**, 8–12 touches, `stopOnResponse` | 28 actions, 11 touches, 31 days |
| Replied – Stop & Route | remove from every sequence → note → tag → notify | 9 actions incl. a 15-minute reply task |
| Win-back | 30+ days, 4–6 touches | 13 actions, 5 touches, 31 days |
| Appointment (when the preset has a calendar) | confirm & move, reminders, no-show rescue | 8 + 5 + 14 actions |

Staff notifications and tasks are **always included**, even when the account has no staff yet: the step stays and is reported as waiting (`user.__pending__`). Copy is complete and written to `references/copy-guide.md`. `examples/validate-plan.cjs` enforces the rules on presets and plans alike.

## File layout

Each preset is two files in `skills/blueprint/presets/`:

- `<preset-id>.preset.json` — the machine-readable template the skill loads.
- `<preset-id>.md` — a human companion: what this preset builds, when it is selected, the workflow table, and the copy notes. (Reviewers read this; the skill reads the JSON.)

Library (v2.0.0): `med-spa` (default), `generic-client` (agency / other fallback), `clinic`, `local-service`, `coach`, `ecommerce`, plus `clinic-launch-a2p` (the launch-event preset, v1 shape, selected explicitly). The industry slugs mirror `src/intake-to-build/customization.ts` and `src/plan-guide.ts` maps each slug to its preset.

## Preset JSON envelope

```jsonc
{
  "presetId": "med_spa",
  "presetVersion": "2.0.0",      // semver; bump on any skeleton change
  "schemaVersion": "0.1",         // the build-plan schema this targets
  "title": "Med Spa",
  "description": "One-paragraph summary: what it builds, how deep.",
  "default": true,                // exactly one preset across the library is the default
  "selectors": {                  // how the skill picks this preset
    "businessTypes": ["Med spa","Medical spa","Medical aesthetics","Aesthetics"],
    "aliases": ["med_spa","medspa","medical_spa","aesthetics","injectables"]
  },
  "source": "where the skeleton was harvested from",
  "conventions": { /* the v2 rules restated for the model reading this preset */ },
  "skeleton": { /* §5 build-plan shape with tokens + conditionalOn, see below */ }
}
```

`selectors.businessTypes` are the `business_type` answers that route to this preset. `selectors.aliases` are the `brief.preset` enum values that resolve to this preset (e.g. `preset: "med_spa"` resolves to the med-spa preset, which claims that alias). Exactly one preset across the library has `"default": true` — **`med_spa`**. `generic` is `default:false`, the neutral fallback for agency / other / unmatched types.

## The `skeleton` object

`skeleton` mirrors the §5 build-plan top level — `users`, `pipelines`, `customFields`, `tags`, `customValues`, `calendars`, `forms`, `funnels`, `templates`, `emails`, `sms`, `workflows`, `handoffs`. Keys starting with `_` are author notes and are dropped at fill time. Every item is a §5 object, optionally annotated with:

### 1. Template tokens — `{{ ... }}`
A `{{path}}` inside any string field is replaced with the brief value at that path. Dotted paths read the brief (`{{business.name}}`, `{{offer.summary}}`); `[n]` indexes an array (`{{audience.painPoints[0]}}`). Supports a fallback with `||`:

```jsonc
{ "ref": "pipeline.main", "name": "{{business.name}} Pipeline || Patient Journey" }
```

If `business.name` is present, it fills; otherwise the literal after `||` is used. A token that resolves to empty with no fallback → the skill omits the field (never emits an empty string) and notes the default it used. Inside copy, a token whose value starts a Capitalized word mid-sentence is lower-cased on fill (`{{offer.leadMagnet}}` = "Free skin assessment" → "your free skin assessment request").

**GHL merge fields pass through untouched.** A `{{...}}` whose first segment is `contact`, `custom_values`, `appointment`, `user`, `location` or `message` is a GoHighLevel merge field, not a preset token; the fill step leaves it exactly as written. The copy guide (§7) lists the allowed set.

### 2. `conditionalOn` — include/drop the whole item
A string boolean expression over the brief. If it evaluates false, the skill **drops the item entirely** (and anything that referenced it — the skill prunes dangling refs and records the prune in the plan summary). Grammar is intentionally tiny:

- `goal.bookingNeeded == true`
- `channels.sms == true`
- `flags includes needs_a2p`
- `channels.payment != "Stripe connected"`
- `staff is empty` (v2: `brief.team.staff` absent or empty)
- combine with `&&` / `||`

`conditionalOn` may sit on any item, including a single workflow action (`send_sms` steps carry `channels.sms == true`) and a single pipeline stage.

```jsonc
{ "ref": "calendar.consult", "conditionalOn": "goal.bookingNeeded == true", "name": "Consultation", ... }
{ "ref": "handoff.a2p",      "conditionalOn": "flags includes needs_a2p", ... }
```

### 3. `fillFrom` — populate a list from the brief, else the preset default
For list-valued structure the brief can override:

```jsonc
"stages": { "fillFrom": "goal.salesStages", "transform": "stageList", "default": [ { "ref": "stage.new_lead", "name": "New Lead" } /* ... */ ] }
"users":  { "fillFrom": "team.staff",       "transform": "userList",  "default": [] }
```

- `stageList`: `["new lead","contacted",...]` → `[{ref: "stage.<slug>", name, position}]`. Workflows that point at `stage.*` refs resolve against whichever set won; a referenced stage not in the chosen set is mapped to the nearest equivalent (new-lead → the first stage; booked → any stage whose name contains "book" / "consult" / "visit" / "estimate" / "call") and the mapping is noted. Never ship a dead ref.
- `userList` (v2): each `brief.team.staff[]` entry → `{ref: "user.<slug of name>", firstName, lastName, email, role, phone?}`. `role` is `admin` for the first person / anyone whose title says owner, admin or director; `user` otherwise. Emails must be unique.

### 4. `templates` and `templateRef` (v2) — every message lives once, complete
`skeleton.templates.emails[]` = `{ref: "email_template.<slug>", name, subject, html}` and `skeleton.templates.sms[]` = `{ref: "sms_template.<slug>", name, body}`. **The copy is complete and send-ready** in the preset, written from placeholder brief fields to `references/copy-guide.md`. The matching `emails[]` / `sms[]` assets carry `templateRef` plus a `copyDirection` (the job of that message in the sequence) and `mergeTags`; send steps reference the asset (`emailRef` / `smsRef`) or the template (`templateRef`) — one of the two is required.

At fill time the skill **rewrites every template from the real brief** (offer, pain points, objections, prices, lead magnet, `voice.threeWords` for tone, `voice.signatureLine` verbatim in the instant email and text) keeping the cadence, the single CTA and the merge fields, then writes the result to **both** the template and the asset `body`. The plan never carries an outline where a body belongs (`needsContent` will not build).

### 5. `userRef` and the role placeholder (v2)
Every `internal_notification`, `task_notification` and `assign_user` carries `userRef`; every calendar carries `teamMemberRefs`. In a preset the ref is the **role placeholder `user.owner`**, never a real user. The fill step resolves it per step:

| Where | Resolves to |
|---|---|
| `internal_notification.userRef` | the user named by `brief.team.notifyName` |
| `task_notification.userRef`, `assign_user.userRef` | the user named by `brief.team.callsName` |
| `calendars[].teamMemberRefs` | that calendar's `staffNames` from `brief.calendars[]` |
| "the owner" / "me" / a name not in the staff list | the first admin in `users[]` |
| no staff in the brief at all | the sentinel `user.__pending__` on every such step; the step is **kept**, the build reports it as waiting for a staff member, and `handoff.add_staff` holds that workflow DRAFT |

Never write a literal GHL user id in a preset (the legacy `to` / `assignedTo` slots).

### 6. Calendars from the brief (v2)
The preset declares one booking calendar (`teamMemberRefs: ["user.owner"]`). The first entry of `brief.calendars[]` maps onto it (name, type `one_on_one` → `event` / `round_robin` → `round_robin` / `class` → `class_booking`, staff, **length**); every further entry is appended as `calendar.<slug>` with its own `teamMemberRefs`. The appointment workflows attach to the first calendar. No calendars in the brief with `goal.bookingNeeded` true → keep the default and note the assumption.

**Slot length (finding 25):** `brief.calendars[].durationMinutes` (or the words in the calendar answer, "15 minutes" / "1 hour") → `calendars[].slotDuration` in minutes with `slotDurationUnit: "mins"`. The preset's own `slotDuration` is the fallback ONLY when the brief gives no length; a calendar with no `slotDuration` is built with GoHighLevel's 30-minute default (Timed Build 04 asked for 15 and 45 and got 30 twice). Never invent a length the brief or the preset does not give.

### 7. Hand-offs between sequences — a tag, never `add_to_workflow`
Speed to Lead ends with `add_contact_tag nurture-start`; Lead Nurture is triggered by `contact_tag nurture-start` and ends with `add_contact_tag winback-start`; Win-back is triggered by it. **Why a tag and not `add_to_workflow`:** both halves of the tag hand-off are live-proven action shapes captured from working GHL UI-built workflows in `templates/action-schemas.json` (the add-tag node and the tag trigger). `add_to_workflow` has no captured native shape there and has not been proven at runtime; a preset may only use proven shapes. Each sequence removes its own trigger tag as its first action so a later hand-off can start it again (GHL fires a tag trigger only when the tag is added, not when it is already present).

The validator enforces: a `/speed/i` workflow must **end** with the hand-off (`E_NO_HANDOFF`); a `/nurture/i` workflow's waits must total ≥ 30 days (`E_NURTURE_TOO_SHORT`); a `/win.?back/i` workflow's waits must total ≥ 30 days (`E_WINBACK_TOO_SHORT`, this library's own rule).

### 8. `copyDirection` — what a message is for
On `emails[]` / `sms[]` / `funnels[].pages`, `copyDirection` is the instruction the rewrite honors (day 8 gives a tip, day 14 handles price, day 27 is the decision sheet). For pages it drives the page outline. It is not the copy; the template is.

## The skill's deterministic fill algorithm (consumes a preset)

1. Resolve preset (brief.preset alias → preset, else business_type → preset, else default).
2. Deep-copy `skeleton`; drop every `_`-prefixed key.
3. Resolve every `fillFrom` (stages from `goal.salesStages`, users from `team.staff`); apply `brief.calendars[]` (§6).
4. Evaluate every `conditionalOn`; drop false items; prune now-dangling refs (record prunes). Re-number stage positions.
5. Substitute every `{{token}}` (with `||` fallback); omit-and-note where empty; leave GHL merge fields untouched.
6. Resolve the `user.owner` placeholder per step (§5); set `cv.owner_first_name` from the notify user.
7. Derive flag-driven handoffs not already present (a2p, stripe, calendar_oauth, add_staff, email_domain, phone_number).
8. Rewrite every template from the brief per `references/copy-guide.md`; write the result to the template and the asset `body`.
9. Map any workflow `stage.*` ref not in the chosen stage set to its nearest equivalent (note it).
10. Set `buildOrder` (`user.*` first, `email_template.*` / `sms_template.*` before `workflow.*`), compute `summary`, leave `idMap` empty.
11. Run `examples/validate-plan.cjs` (and `validate_build_plan`), fix, then render the §5A approval view.

Steps 2–7 and 9–10 are pure and order-independent of the model's discretion — that is what makes the skeleton deterministic. Step 8 (copy) is the only generative step, and it is grounded in the brief, gated by the copy guide, and editable at review.

## Enum discipline (must pass the mcp validators)

Presets must use GHL-correct enum values so the emitted plan passes ghl-command-mcp's Zod / JSON-Schema validators (their §7 #1 ratification):

- `customFields[].dataType` ∈ `TEXT`, `LARGE_TEXT`, `NUMERICAL`, `PHONE`, **`MONETORY`** (GHL's own spelling — NOT "MONETARY"), `CHECKBOX`, `SINGLE_OPTIONS`, `MULTIPLE_OPTIONS`, `FLOAT`, `DATE`, `TEXTBOX_LIST`, `FILE_UPLOAD`, `SIGNATURE`. Choice types (`SINGLE_OPTIONS`/`MULTIPLE_OPTIONS`/`CHECKBOX`) MUST carry an `options` array.
- `calendars[].calendarType` ∈ `round_robin`, `event`, `class_booking`, `collective`, `service_booking`. **A calendar that carries `teamMemberRefs` must NOT be `event`** *(finding 36)*: `event` calendars have no team-member concept — GoHighLevel answers 200 to the create and silently drops the people. Use `round_robin` even for a single person (a round-robin with one member behaves exactly like a one-on-one calendar and keeps them). `event` is only for a calendar nobody is assigned to.
- `customFields[].model` ∈ `contact`, `opportunity`.
- `users[].role` ∈ `admin`, `user`.
- **Trigger `type`** ∈ the executor's native set so triggers auto-build: `contact_tag`, `form_submission`, `appointment` (+ `appointmentStatus`), `customer_reply`, `pipeline_stage_updated`, `call_status` (+ `callStatuses`, `callDirection`), `inbound_webhook`, `payment_received`. `form_submitted`/`contact_replied` are accepted aliases; `tag_added`/`appointment_status`/`appointment_booked` are NOT recognized (they surface as a manual step) — never use them in a preset.
- handoff `owner` ∈ **`OPERATOR-UI`**, **`OPERATOR-EXT`**, **`TEAM`** (customer-facing, generic). The MCP still accepts + normalizes the legacy `JERRY-UI`/`JERRY-EXT`/`SASHA`, but presets EMIT the canonical labels.
- `handoff.a2p.blocks` MUST list every SMS-bearing **workflow** ref (the validator checks it) — only an explicit `workflow.*` ref keeps a workflow DRAFT.
- Max 40 actions per workflow (a `find_opportunity` counts head + 2 transitions + children); `find_opportunity` must be the last action.

> The schema §5 contract and the live Zod schema (`src/intake-to-build/plan.ts`) are the sources of truth. If the doc and the Zod schema ever disagree, the Zod schema wins.

## Validating a preset

```
node skills/blueprint/examples/validate-plan.cjs skills/blueprint/presets/<id>.preset.json
```

A file with a top-level `skeleton` is validated as the **maximal plan**: every `conditionalOn` taken as true, every `fillFrom` resolved to its default, `user.owner` accepted as the role placeholder. The same script validates a filled plan (the worked examples) and applies the same v2 rules, plus copy hygiene (SMS length, subject length, template namespaces, custom values referenced by copy exist).

## Versioning & distribution

- **Version:** semver in `presetVersion`. Patch = copy/wording; minor = additive items; major = removed/renamed refs or changed skeleton shape. v2.0.0 (2026-08-26) is a major: the workflow set, the templates block and `userRef` are new, and `internal_notification.to` is gone. The generated plan records which preset+version produced it in its `summary` line so a build is reproducible.
- **Distribution:** presets ship **alongside the skill** (`skills/blueprint/presets/` travels with `SKILL.md`), and `src/plan-guide.ts` hands the matching preset, the worked example and the copy guide to the build stage's prompt. Adding a preset = drop two files in `presets/`, give it a `selector`, and map its industry slug in `plan-guide.ts`.
- **Authoring a new preset:** copy the closest `*.preset.json`, change `presetId`/`title`/`selectors`/`default:false`, edit the skeleton, rewrite every template for the vertical to the copy guide, write the `.md` companion, and run the validator. Keep the workflow set; the depth table above is the floor.
