# Brief Schema (§4) — distribution mirror

> **Mirror of the canonical contract.** Canonical source: `command-center/shared/intake-to-build-schema.md` §4 (co-owned by atlas + ghl-command-mcp). This copy travels with the distributed skill so it is self-contained for subscribers who do not have the command center. Kept in lockstep — `schemaVersion` must match. If they ever differ, the canonical wins; open a contract note, do not edit divergently.

`schemaVersion`: **0.1**

The Brief is the normalized business profile the plan-gen skill consumes. The MCP produces it from a form submission (`briefSource: "intake_form"`); the skill produces it from Agency OS artifacts (`briefSource: "agency_os"` / `"hybrid"`).

## Fields
| Field | Type | Notes |
|---|---|---|
| `schemaVersion` | string | "0.1" |
| `briefId` | string | submission id or generated |
| `preset` | enum | `generic` \| `med_spa` \| `clinic_launch_a2p` \| `coach` \| `ecom` \| `agency` (skill resolves via preset selectors) |
| `briefSource` | enum | `agency_os` \| `business_os` \| `intake_form` \| `hybrid` |
| `extended` | object | optional; verbatim deep ICA / offer / brand-DNA structures when partner-OS-sourced (see `agency-os-detection.md` §5) |
| `business.name` | string | |
| `business.type` | string | drives preset |
| `business.website` | string | |
| `business.location` | string | |
| `business.timezone` | string | drives calendar + send windows |
| `offer.summary` | string | |
| `offer.pricePoints` | array | `[{name, price}]` |
| `offer.leadMagnet` | string | |
| `offer.avgDealValue` | string | |
| `audience.ideal` | string | |
| `audience.painPoints` | array | |
| `audience.objections` | array | |
| `goal.primary` | enum | book appointments / capture + nurture / direct sales / re-engage / other |
| `goal.salesStages` | array | raw stage names → pipeline design |
| `goal.bookingNeeded` | bool | |
| `goal.followUpStyle` | enum | high-touch / light / single confirmation |
| `channels.email` | bool | |
| `channels.sms` | bool | |
| `channels.a2pStatus` | enum | not started / in progress / approved / not needed |
| `channels.payment` | enum | Stripe connected / Stripe not connected / other / none |
| `channels.calendarConnected` | bool | |
| `channels.social` | array | |
| `channels.hasPhoneNumber` | enum | `yes` \| `no` \| `unsure` — "Do you already have a phone number in GoHighLevel?" *(v2)* |
| `team.staff` | array | `[{name, email, role?, mobile?}]` — EVERY staff member to set up; becomes plan `users[]` *(v2)* |
| `team.notifyName` | string | who is notified about new leads: a staff name or "the owner" *(v2)* |
| `team.callsName` | string | who takes booking / follow-up calls: a staff name or "the owner" *(v2)* |
| `calendars` | array | `[{name, type, staffNames, durationMinutes?}]`, `type` ∈ `one_on_one` \| `round_robin` \| `class` — one entry per booking calendar the client asked for *(v2)*; `durationMinutes` = the appointment length when the answer gave one ("45 minutes" → 45), which the plan carries as `calendars[].slotDuration` *(finding 25)* |
| `voice.threeWords` | string | brand voice in three words — the copywriter's tone brief *(v2)* |
| `voice.signatureLine` | string | a line the client always says to new clients, verbatim — reused in the first email / text *(v2)* |
| `assets.existingPipeline` | string | |
| `assets.existingWorkflows` | string | do-not-clobber list |
| `assets.brand` | string | |
| `assets.notes` | string | |
| `flags` | array | derived: `needs_a2p`, `stripe_not_connected`, `calendar_oauth_needed`, `email_domain_needed`, `phone_number_needed` *(v2: SMS wanted and no confirmed number)* |
| `warnings` | array | parse-time notes from the normalizer (a staff line it could not read, a calendar type it assumed). Never fatal. *(v2)* |

A canonical worked example is in `examples/sample-brief.json`. Omit unknown fields; never emit `null`.

## v2 additions (2026-08-26) — the pieces a good build needs

The owner inspected a real build: it had created one user and one calendar because the intake never asked who the staff are or how many calendars they need. `team`, `calendars`, `voice` and `channels.hasPhoneNumber` carry that. All optional — a 0.1 brief still validates.

Worked example (the v2 block only):
```jsonc
{
  "team": {
    "staff": [
      { "name": "Jane Smith", "email": "jane@glow.com", "role": "Front desk", "mobile": "555-123-4567" },
      { "name": "Dr. Mark Lee", "email": "mark@glow.com", "role": "Provider" }
    ],
    "notifyName": "Jane Smith",
    "callsName": "the owner"
  },
  "calendars": [
    { "name": "New Patient Consult", "type": "round_robin", "staffNames": ["Jane Smith", "Dr. Mark Lee"], "durationMinutes": 45 },
    { "name": "Follow-up Call", "type": "one_on_one", "staffNames": ["Jane Smith"], "durationMinutes": 15 }
  ],
  "channels": { "sms": true, "hasPhoneNumber": "no" },
  "voice": { "threeWords": "warm, direct, unhurried", "signatureLine": "You will never look overdone here." },
  "flags": ["needs_a2p", "phone_number_needed"]
}
```

How it lands in the plan: each `team.staff[]` entry → a `users[].ref` (`user.jane_smith`); `notifyName` → the `userRef` on the new-lead notification; `callsName` → the task assignee / `assign_user`; each `calendars[]` entry → a `calendars[]` object with `calendarType` (`one_on_one` → `event`, `round_robin` → `round_robin`, `class` → `class_booking`; *finding 36*: a `one_on_one` that NAMES someone becomes `round_robin` instead, because `event` calendars cannot hold a team member — GoHighLevel accepts them and drops the person), `teamMemberRefs` and `slotDuration` (from `durationMinutes`; omitted → GoHighLevel's 30-minute default); `voice` → the tone and the reused line in every template. No staff listed → those steps use `user.__pending__` and are reported as waiting.

### `validate_brief` — what it returns
`{valid, errors, warnings, brief}`. `errors` are schema failures (blocking). `warnings` are the gaps a schema-valid brief can still carry — advisory, never fatal; the skill asks or marks the step as waiting:

| Warning | When |
|---|---|
| No staff listed — notifications will be marked as waiting for a staff member | `team.staff` empty or absent |
| Calendar "X" names no staff | a `calendars[]` entry with empty `staffNames` |
| Calendar "X" lists "Y", who is not in the staff list | a name on a calendar that is not in `team.staff` (owner aliases accepted) |
| Booking is wanted but no calendar was described | `goal.bookingNeeded` (or a "book…" primary goal) and no `calendars[]` |
| "Y" (notified about new leads / taking calls) is not in the staff list | `notifyName` / `callsName` not a staff member nor "the owner" |
| *(passthrough)* | every entry of `brief.warnings` from the normalizer |
