← Design HQ · HTML on GitHub · PRD — prd-as-workshop-booking.md
Workshop Booking — Discover, set it up, go live, then the guest thread.
The Workshop Booking flow, every state, in journey order — the build contract; ACs cited by id, never restated. Companion: assistant-lifecycle.html (master) · on-brand-support.html (token/component source). This file is the canonical structural reference for per-assistant flow mockups (canon §“Assistant-flow mockup structure”).
8 sections: overview → scope → content → journey → desktop → mobile → components → future Discover → Evaluate → Activate → Monitor (founder) Trigger → Interact (forks on booking_surface) → Outcome → Fallback (guest) amber = pending/action-needed · emerald = fixed/healthy/resolved Flow / Components / Future (canon 2026-08-01)Widths here sample a fluid continuum — content
scales continuously; structure steps only at sm 640 · md 768 · lg 1024. Kit:
design-canon.md U8.
Workshop Booking (TMPL-WSBK01) turns a workshop host's WhatsApp DMs into confirmed, paid seats. A guest taps the founder's booking link — or just texts — and the assistant captures the booking right in the chat (name, session, headcount), relays the founder's own payment link or QR exactly as she wrote it, and once payment is confirmed — her one-tap Mark paid, or a tick in her own roster sheet — sends the Meta-approved confirmation and reminds the guest 1 day + 1 hour before class. $1.75 per confirmed booking; MystFlo relays payment asks but never touches the money. Bookings live in MystFlo's own ledger and surface on Pulse (metrics, attention rows, the Attendees list); when the assistant can't answer, the founder takes over in the same WhatsApp thread — no second app.
full_prepay · free_rsvp) × two booking surfaces (in_chat — WhatsApp Flow form fronting typed capture · own_form — founder's form link relayed, read-only roster sheet).deposit_balance radio removal (#3192) · Connect two-role rows — StorageRoleRow/AiModelPicker (#3198, PR #3209) · MystFlo Storage provider (#3178, closed).details.benefits/details.flow have drifted (sec-2 PARTIAL); the build PR ships the migration + PRD together (check-catalog-copy.sh).
┌ CONFIG (Personalize) ────────────┐ ┌ BRAIN ──────────────┐ ┌ CATALOG COPY (DB row) ────────┐
│ name · start_date* · price · │ │ voice · policies · │ │ description · tldr · │
│ seats · payment_mode · │ │ contact sign-off │ │ benefits[4] · details.flow │
│ booking_surface · link/keywords ·│ └──────────┬──────────┘ │ (5 nodes) — sec-1/sec-2 only │
│ copy fields · booking_form_url │ │ └───────────────────────────────┘
└────────────────┬─────────────────┘ │ grounds answers
reads ▼ ▼
guest WhatsApp thread — capture (Flow form / typed / own-form relay)
│ name · contact · seats
▼
cohort_bookings row ◄─── one-way roster read (own_form: her paid tick = billable event)
│ booked → paid → confirmed
├─► locked templates: confirmation (<30s) + reminder (1d + 1h)
└─► Pulse metrics · attention rows · Attendees columns · activity feed
start_date required — gates activation, AC-WSBK-16 ↗) · payment_mode · booking_surface · generated wa.me link + conditional keywords · founder-editable copy (booking invite, payment ask) · booking_form_url required on all save paths under own_form (AC-WSBK-15 ↗).workshop-templates.ts, Meta-approved, never editable (C1). Every sec-9 ledger tags each message's source with one vocabulary: template founder field ai-composed native.cohort_bookings): name · contact · seats · status · paid-source (Mark paid / roster tick). Read + verdict, never edit; the read-back enum is closed in code (_shared/flow-patterns.ts), and the Attendees columns come from the same per-assistant records contract (sec-13).own_form only): the founder's own columns, mapped by header name (_shared/cohort-roster.ts) — read one way, never written; a missing required column pauses reminders with a named-column message, never a silent failure.Founder vocabulary (Discover → Evaluate →
Activate → Monitor) and guest vocabulary (Trigger → Interact → Outcome → Fallback) are the same pair
every MystFlo assistant's Scope Council uses — reproduced here, not invented for this file. The one real
branch worth drawing is at Interact, where booking_surface splits the guest experience in two;
everywhere else is one direction.
┌ DISCOVER ────────────────────────────────────────────────────────┐
│ Founder browses Explore — sees the card, its price, and the │
│ one required app (WhatsApp Business). │
└──────────────────────────┬─────────────────────────────────────────┘
▼
┌ EVALUATE ──────────────────────────────────────────────────────────┐
│ Opens the Overview tab — what it does, how it works, key benefits. │
└──────────────────────────┬─────────────────────────────────────────┘
▼
┌ ACTIVATE ──────────────────────────────────────────────────────────────┐
│ Personalize (workshop date/price/seats · payment + booking-surface │
│ mode · locked message copy) → Connect (WA number · conditional Sheets │
│ pick · template-approval notice) → Try (read-back) │
└──────────────────────────┬─────────────────────────────────────────────┘
▼
┌ MONITOR — founder, ongoing ─────────────────────────────────────────────┐
│ Activating (loader) → Activated (live, honest about pending templates) │
│ → Pulse: bookings roll in, attention rows, live feed │
└──────────────────────────┬─────────────────────────────────────────────┘
▼ once live, per guest DM
▼ the guest side runs across every scenario below
One waterfall could only draw one path. This reads left-to-right along the same six beats every guest passes through, with one lane per scenario — so the differences between modes, and the places where a lane simply stops, are visible in one look. Amber lanes are not built. Full detail per lane: sec-9.
Column 6 is the honest column. Four of seven lanes end in ✗ — see sec-11. Founder-side fallback (a template Meta rejects) is unchanged and lives at sec-6b; bookings and payment relay keep working throughout that.
start_date gates Try and activation — no real date, no activation (AC-WSBK-16 ↗, closing the silent-failure gap where reminders never scheduled and no one was told).booking_surface='own_form' gates two things at once: Connect's roster-sheet pick becomes required, and Personalize's booking_form_url becomes required on all three save paths — activation, update-config, and reactivation (AC-WSBK-15 ↗).wa.me link can't open a Flow form directly — it only pre-fills the guest's “book” message. The form is always sent BY the assistant, as an interactive flow message, once intent is caught. Link-tap and direct-DM converge on the identical mechanism.AC-* ↗) live in prd-as-workshop-booking.md §Test Plan.description — catalog copy is untouched, and no price is written into the line. It shows even when Support is already live (the card doesn’t know the number yet). Activation: 3s.Workshop Booking captures the seat straight from your WhatsApp DMs — name, session, headcount — then relays your own payment link or bank details exactly as you wrote them. Once you mark it paid (or your roster shows it), the guest gets an instant confirmation and a reminder before class. You decide every payment; MystFlo never touches the money.
payment link, policy, voice — from your Brain
picks a session, gives headcount
one tap, or your sheet ticks it
Confirms + reminds
instant confirm, then 1 day + 1 hour before
long_description; ships via migration + PRD in the same PR (guard-railed, check-catalog-copy.sh) — the build PR must carry it. Exception (#5311, provisional): benefit 1’s body is Chief Product’s 2026-09-25 draft (Support answers, booking books), and the header pairing line is UI from the booking-family rule, not a DB field — both pending Peyton’s copy approval.HowItWorksFlow idiom, nodes/subs/branch VERBATIM from the seeded details.flow (20260710080000_2012_b4_wsbk01_card.sql, 5 nodes · 1 branch); skeleton + ✦ only on the seed's one ai:true node; outcome tile = the single lit object. Panel at 720 for the ≥768px horizontal row; <768px renders the vertical rail (not drawn).Edit what you sell in your Brain →
Same class, new date? Just set when and where.
Recurring, so this starts empty — set this run’s start, or a range — 10:00 or 10:00–11:15. An end at or before the start is flagged under the field, never blocks Continue.
So reminders fire at the right moment for a nomad host — never changes the time guests see.
Where guests show up — fills the confirmation, the reminders, and the invite. Starts as the offering’s venue from your Brain, or your business address and Maps link when it has none; change either here if this run is somewhere else. With a map link guests get a one-tap “Open in Maps” button; without it they still see the address.
Edits here change only this workshop — your Brain stays as it is.
Field names match your Brain’s offering fields one-to-one — Name · Price · Date · Time · Where · Spots (Peyton 2026-09-23). Date always starts empty after a pick — same class, new run; Timezone is always yours.
Retreat-style deposits live on Retreat Booking, not here.
Tick one or both. Whatever you tick goes out with your payment ask.
Both are sent together — guests scan or tap, whichever suits them. With both ticked, the link and the QR are both required before you can activate.
Add your QR to activate — or untick it.
We couldn't convert this image — if the QR doesn't reach guests, re-upload from a different browser.
Upload succeeded; this is a warn, not a block; founder can Replace from another browser.
The card never hides — it lights back up the moment Connect finishes.
The AI reads intent — “any spots left tuesday?” books a seat just like “book” does. Nothing to configure.
Only if more than one assistant shares this number — these tell your assistants apart. Nothing to do with reading intent; most founders never see this field.
However they arrive — link tap or a direct message — it's one path in. What they see next is your call:
Shows the form once WhatsApp approves it; guests text back and forth until then — nothing to set up on your end.
Shown above the invite button. Fills in your workshop name, date, time, and location automatically. No date set yet → the name-only line, never a blank.
Sent right after the booking form, with your QR and/or link. The default follows what you ticked in 02 — QR only: “Scan the QR to pay.” · link only: “Use the link below to pay.” — and your edits stay. Lives here with the invite because it is your copy, not a setting.
Which channels the class reminder can use — connect the app for each in Connect. WhatsApp on by default; the booking flow itself always runs in chat regardless.
Only shown when you have On-Brand Support running on this number.
Works the same whether they tapped your wa.me link or typed this directly — the AI reads intent, not just the word.
brain.html 13.4-sell-open.cohort_name + price the pick fills are exactly what the Flow form’s title and date · price line render.location + map_url are answerable mid-chat and outrank the Brain’s business location, so both always render here — never hidden because the Brain has an address.brain_profile.services[] (name · price · spots per row, then a separator, then “+ Something new”) tops 01; before a pick NOTHING else in 01 renders — no empty form. Picking an offering reveals the fields, filled by the offering’s one-time / recurring answer (above): one-time fills everything the Brain holds, recurring leaves Date + Time empty, unanswered fills whatever is there; “+ Something new” reveals the same fields empty (Where still starts as the business address). All stay editable, grouped Name · Price · Date · Time · Where · Spots (Peyton 2026-09-23 — the shared Brain offering vocabulary, one-to-one; supersedes the #4478 field order); retires COHORT_PICKER_FIELD_KEYS filtering (ConfigureStep.tsx:267).contact_info.address · maps_url) when known; today Location hides when the Brain has an address. Start time + End time collapse to ONE Time field (“10:00” or “10:00–11:15”) with Timezone beside it.configuration only; the Brain is never written from Personalize. Timezone is never quick-filled; Date and Time fill only for a one-time offering (or an unanswered one that holds them).Select (src/components/ui/select.tsx), not the laid-out OfferingPicker.tsx radio rows (Peyton 2026-09-25): bordered trigger with ChevronDown and a 2px white ring when open; the menu is surface-elevated glass, rows ≥44px with name + a white/40 price · spots sub-line (“price on ask” when empty), the picked row gets the left ✓, one separator, then “+ Something new” last. The closed trigger shows the picked name (desktop adds the price · spots meta). “Edit what you sell in your Brain →” sits under the trigger. Still vertical, no modal (kit 2026-06-12 lock). The long list became one line, so the page opens short.currency, type currency); the old bare $42 field was stale.In own_form mode the Storage row above is withheld entirely — no row renders, not even grayed (shipped: internalStorageWithheldForOwnForm, ConnectStep.tsx, #3178 CTO/Design review + #3184). The roster lives in her own Sheet, so “MystFlo Storage · Connected” would be a false claim in that mode. The own-sheet requirement stays backend-enforced (CONDITIONAL_REQUIRED_STORAGE, four paths) with no Connect-tab pick UI yet — separately tracked; per canon U9 that future roster-pick row is not drawn in this Active frame (its contract lives only in the F4 duplicated frame below).
.panel — no true 1180-width realization existed. Re-drawn here using the exact wrapper this file already established for a real desktop frame (sec-3b's .flowgrid > .flowstep > .pageframe > .desktop-scroll > .desktop, reduced-zoom chrome) plus the master's two-column split (apps+wallet left flex-1, Brain Check right 400px). Same rows/copy as before (including this session's Storage-row and caret fixes), nothing invented. The "Continue to Try" button is omitted from this crop.stripe_meter_id and a stripe_price_id exist on this template's row; the Wallet row above surfaces the same wallet that guard protects.own_form requires booking_form_url; the roster-sheet requirement itself is backend-enforced (CONDITIONAL_REQUIRED_STORAGE) — its Connect-side face is not yet built, so this frame documents only the withheld Storage row (cond-block above); the future pick contract is drawn in F4.ROLE_IDS, required-roles.ts): “AI Model” → MystFlo AI · Connected (AiModelPicker.tsx shape) and “Storage” → MystFlo Storage · Connected — one Storage role, drawn ONLY in the default in-chat mode; under own_form it disappears entirely (shipped internalStorageWithheldForOwnForm, #3178 CTO/Design review — “Connected” would be a false claim when the roster lives in her own Sheet). MystFlo Storage is a real, shipped provider value (#3178, closed — STORAGE_PROVIDER_KEYS includes mystflo_storage, status pill via the shared ConnectionStatusButton primitive, never hand-rolled text); what remains not built is the Attendees destination it points at (sec-13, M1).design-decisions.md 2026-08-05, caret scope amended 2026-08-07, Peyton-locked; code #3198/PR #3209): label = the role (“Storage”), badge = the connected app carrying the ▾/▴, and tapping it expands the picker group in place as a hairline list. The DestinationPicker expansion (Create a new one — default, Template-first 2026-05-22 / Link a sheet I already have, with the how-this-works tip living under the Link option only) belongs to the FUTURE own_form roster-pick row — no longer drawn in this Active frame (the shipped code withholds the Storage role entirely under own_form; see the cond-block note above); its contract row lives in F4 only, per canon U9. Deviation from the master's generic three-group default, flagged not silently resolved: the master's 2026-08-07 ruling defines the Storage picker as three groups (MystFlo Storage / Google Sheet / Your Supabase) everywhere, but that future row's real gate is provider-specific (CONDITIONAL_REQUIRED_STORAGE names google_sheets only for own_form) — drawing a Supabase group there would promise a destination the backend validator would reject. This mockup keeps the two-group contract (Google Sheet only) rather than fabricate a third group the code doesn't support; reconciling this file's provider-specific gate with the master's generic default is an open follow-up, not decided in this pass. Caret rule, corrected: the default-mode MystFlo Storage row now carries the caret too (fixed in this pass — it was previously missing even the badge/chip wrapper). AI Model remains the one exception — no caret, no coming-soon row, deliberate product gate (BYOK_AI_HIDDEN). Master contract: assistant-lifecycle.html B9/frame 5b.+1 555-0142 ▾ control — a real, already-functioning picker for a different sub-choice (which number) than the label-badge caret's job (which channel/provider). It now lives on the WhatsApp pick-row rather than the outer conn-row, since the outer badge's caret has a real second group (Email) to open.confirmed, fires the $1.75 charge), so the read can never be optional. When the own_form Connect-side pick ships (separately tracked), it adopts the shipped DestinationPicker (T472, “Template-first, never blank” 2026-05-22): Create a new one (default — the MystFlo template roster cloned into her Drive at activation, zero structure concerns) vs Link a sheet I already have — the ONLY path where column mapping applies; the how-this-works tip (no data until read · header-name mapping per _shared/cohort-roster.ts · missing column pauses honestly) lives under that option, never as a blanket. Until then the requirement is backend-only — an own_form founder meets it at the activation gate, not on this tab.Off unless you switch it on. Shows only when 01 used “+ Something new” — hidden when the name is already in your Brain.
Runs on your WhatsApp from the moment you activate. Pause anytime.
assistant-lifecycle.html frame 6) covers this once for every assistant type instead of restating it here. The read-back's "Messages" row already flags the pending Meta approval, so activating isn't a surprise when the success screen repeats it (sec-6).brain_profile.services[] or an existing offering was picked.services[] once, at activation; OFF → the Brain is untouched. Re-activation never re-asks for a name that now exists.Edit what you sell in your Brain →
The rest of the tab is unchanged — see “Scrolled” below.
The rest of the tab renders as usual — only 01 waits.
Edits here change only this workshop — your Brain stays as it is.
Attention tone, never red. Retry is 44px; ↻ is decorative. Timezone and the map link sit where they always do — cropped here.
Each row shows your Brain’s answer — Recurring, or One-time with its date; Wheel Throwing Class hasn’t been answered yet, so it shows price · spots only. Rows are 44px tall; the list opens under the field and pushes nothing. Long lists scroll inside the menu; + Something new stays last.
Edit what you sell in your Brain →
Same class, new date? Just set when and where.
A start, or a range.
The offering’s venue from your Brain, or your business address and Maps link — change either if this run is somewhere else.
Edits here change only this workshop — your Brain stays as it is.
Field names match your Brain’s offering fields one-to-one — Name · Price · Date · Time · Where · Spots.
Works the same whether they tapped your wa.me link or typed this directly — the AI reads intent, not just the word.
Edit what you sell in your Brain →
Your Brain says “price on ask” — add one here, or leave it and guests ask you.
Edits here change only this workshop — your Brain stays as it is.
One-time: Date and Time come straight from the Brain, so there is no “new date” hint. Anything the Brain doesn’t hold stays blank.
It stays with this workshop — on Try you can add it to what you sell.
Still starts as your business address — you can change it.
Edits here change only this workshop — your Brain stays as it is.
Type it below — or add what you sell to your Brain →
Still starts as your business address — the one thing the Brain can fill without an offering.
Nothing to pick from, so the fields show straight away. The Try tab asks whether to add it to what you sell (5m).
Retreat-style deposits live on Retreat Booking, not here.
Tick one or both. Whatever you tick goes out with your payment ask.
Both are sent together — guests scan or tap, whichever suits them. With both ticked, the link and the QR are both required before you can activate.
Add your QR to activate — or untick it.
We couldn't convert this image — if the QR doesn't reach guests, re-upload from a different browser.
Upload succeeded; this is a warn, not a block; founder can Replace from another browser.
The card never hides — it lights back up the moment Connect finishes.
The AI reads intent — “any spots left tuesday?” books a seat just like “book” does. Nothing to configure.
Only if more than one assistant shares this number — these tell your assistants apart. Nothing to do with reading intent; most founders never see this field.
However they arrive — link tap or a direct message — it's one path in. What they see next is your call:
Shows the form once WhatsApp approves it; guests text back and forth until then — nothing to set up on your end.
Shown above the invite button. Fills in your workshop name, date, time, and location automatically. No date set yet → the name-only line, never a blank.
Sent right after the booking form, with your QR and/or link. The default follows what you ticked in 02 — QR only: “Scan the QR to pay.” · link only: “Use the link below to pay.” — and your edits stay. Lives here with the invite because it is your copy, not a setting.
One preview, not two — the 1-day and 1-hour sends use the identical body.
Only shown when you have On-Brand Support running on this number.
start_date gates Try and activation.free_rsvp skips the payment-ask leg entirely, straight to confirmed.payment_mode and booking_surface render as real radio controls the founder can select.wa.me link drops straight into the booking flow.cond-blocks; 05’s holding line + ping rows always render. Dead controls omitted outright: five no-runtime-reader toggles, deposit_balance (a Retreat Booking concept)..beat above the bubble it produces in the phone rail, so a founder reads the form top-to-bottom and watches the chat happen in that order. The old “Here’s how to pay…” default is superseded in these frames only — sec-7/sec-9 still draw today’s shipped copy until the PRD reseeds it.locked-tpl previews, copy verbatim (C1; ONE reminder body covers both offsets); Payment vehicles (#4703, spec change — not built) live in 02 · How guests pay, not here — Peyton (preview review 2026-09-10): one section answers “how guests pay” end to end, so the vehicle group sits directly under the Pay in full / Free RSVP radio, no separate 04b title; Free RSVP folds the group away. Payment ask stays here in 04a, right after the invite in chat order — it is founder copy, and founder copy lives together under Messages (Peyton 2026-08-04). The group = a checkbox GROUP of two vehicles, not a radio: Payment link (URL field nested under it) · QR code (nested upload — saved row: thumbnail · Replace · Remove; empty = dashed drop box). Tick one → that one is sent; tick both → both expand, both required, one helper line. The ask is the caption/body in every case, only the delivery vehicle changes (AC-WSBK-34 ↗ · AC-WSBK-35 ↗; gap #6b bridge must be verified in the same PR).required_when). Schema implication for the build: today’s three-value payment_method (ticketing_page / stripe_link / qr) collapses to two vehicles — the PRD owns the field shape. 768: no separate frame — the control is a single column at every width; 768 draws the 375 stack at wider measure inside the desktop shell (canon U8 sm 640 step, same rule as the time pair).pv-chat (invite → Flow form → payment ask with the QR and QR + link variants as one dashed call-out (the rail shows whichever matches 02) → Mark-paid beat → confirmation → reminder placeholder; scrolls in the ~460px shell, position:sticky in the build), plus a second dimmed phone for the own_form state (relay is the whole guest experience — AC-WSBK-14 ↗). start_date required, date-only, gates Try/activation (AC-WSBK-16 ↗); repeating sessions = #2798.sm 640 pair rule as the time pair) and stay side by side even at 375 because both values are short; the quiet line follows the Spots field; Time and Timezone stack at 375 (the shipped sm 640 pair rule) and the map link sits under Where as a second field. The one-offering and picked-then-edited states differ only by row count / one field value, so they ride the desktop call-out.Payment · Messages · Brain rows and the two “Trying it” notices are unchanged from sec-5 — cropped here.
Runs on your WhatsApp from the moment you activate. Pause anytime.
Runs on your WhatsApp from the moment you activate. Pause anytime.
Copy on these frames is provisional — Chief Product draft 2026-09-25, pending Peyton’s approval (#5311). Placement and structure are the contract; strings may still change.
Answers your guests’ questions from your Brain · a booking link or QR tap goes straight to the form.
Booking runs with Support — each is charged only when used.
… template-approval notice, 02 · Wallet and 03 · Brain Check are unchanged — frame 4.
Unchanged — the two notices in frame 5 (real messages only · send yourself the first one).
Both run on your WhatsApp from the moment you activate. Each is charged only when used.
Answers your guests’ questions from your Brain · a booking link or QR tap goes straight to the form.
Already live on this number as “Ember Clay support” — nothing to switch on.
… template-approval notice, 02 · Wallet and 03 · Brain Check are unchanged — frame 4.
Unchanged — the two notices in frame 5 (real messages only · send yourself the first one).
Runs on your WhatsApp from the moment you activate. Pause anytime.
AddOnCard in its enabled state (stronger hairline, switch on), with the switch locked and a reason line. Build delta: AddOnCard needs a locked + lockReason prop, a price suffix read from the template’s unit (“/session”, not a hard-coded “/run”), and a mount in ConnectStep (today it only mounts in Personalize)..notice is a headline plus a pointer, with no lantern because it informs rather than asks. The full reason lives on the Connect row, and the card and Overview carry the pairing line one step earlier.price_per_run (the add-on price, the Wallet row and the CTA). They never appear in notice or reason copy.Copy on these frames is provisional — Chief Product draft 2026-09-25, pending Peyton’s approval (#5311). Placement and structure are the contract; strings may still change.
768 samples the U8 continuum; it isn’t a third layout. The notice, the Connect rows with the add-on, and the read-back are all single-column at every width, so tablet is the 375 stack at a wider measure inside the desktop sheet chrome (24px gutters, column capped at 720). A to C are drawn here; D and E follow the same rule.
Answers your guests’ questions from your Brain · a booking link or QR tap goes straight to the form.
Booking runs with Support — each is charged only when used.
… template-approval notice, 02 · Wallet and 03 · Brain Check are unchanged — frame 4.
Unchanged — the two notices in frame 5 (real messages only · send yourself the first one).
Both run on your WhatsApp from the moment you activate. Each is charged only when used.
Copy on these frames is provisional — Chief Product draft 2026-09-25, pending Peyton’s approval (#5311). Placement and structure are the contract; strings may still change.
Answers your guests’ questions from your Brain · a booking link or QR tap goes straight to the form.
Booking runs with Support — each is charged only when used.
… template-approval notice, 02 · Wallet and 03 · Brain Check are unchanged — frame 4.
Unchanged — the two notices in frame 5 (real messages only · send yourself the first one).
Both run on your WhatsApp from the moment you activate. Each is charged only when used.
Answers your guests’ questions from your Brain · a booking link or QR tap goes straight to the form.
Already live on this number as “Ember Clay support” — nothing to switch on.
… template-approval notice, 02 · Wallet and 03 · Brain Check are unchanged — frame 4.
Unchanged — the two notices in frame 5 (real messages only · send yourself the first one).
Runs on your WhatsApp from the moment you activate. Pause anytime.
Copy on these frames is provisional — Chief Product draft 2026-09-25, pending Peyton’s approval (#5311). Placement and structure are the contract; strings may still change.
About 30 seconds. Stay on this screen.
Support answers your guests’ questions; a booking link or QR tap goes straight to the form. Watch both in Pulse.
assistant-lifecycle.html) — not restated. NEW (PRD debt): one added step, “Switching on On-Brand Support”, only when Support wasn’t live.Loader and success are a centered dialog column at every width (U8): 768 draws the same 420 / 460 column centered over the dimmed page.
Support answers your guests’ questions; a booking link or QR tap goes straight to the form. Watch both in Pulse.
About 30 seconds. Stay on this screen.
Support answers your guests’ questions; a booking link or QR tap goes straight to the form. Watch both in Pulse.
About 20 seconds. Stay on this screen.
assistant-lifecycle.html / the activation-wizard PRD), not restated here — this card contributes no new criterion for the generic loader.assistant-lifecycle.html frame 7a) — no assistant-specific rewording, same 5 grouped steps.It's capturing seats on your WhatsApp now. Watch bookings fill in Pulse.
assistant-lifecycle.html) — same shell as every Pulse card, only the metrics line and feed content are Workshop-Booking-specific. Support paused or off while this card is live (#5311) is drawn once in the master: assistant-lifecycle.html → 8f.Two things sit behind “route to the founder”: how the handover physically works (settled — it happens in the same WhatsApp thread, no second app needed) and what the founder can decide about it (nothing today — and one of those decisions costs her money, so it can't stay silent).
ONE number — the founder's own. Two things talk on it at once.
┌───────────────────────────────┐ ┌──────────────────────────────┐
│ WhatsApp Business APP │◄──────►│ Cloud API (MystFlo) │
│ on Priya's phone │ mirror │ the assistants │
│ she types here, by hand │ in │ they answer here │
└───────────────┬───────────────┘ real └───────────────┬──────────────┘
│ time │
└────────────────────┬────────────────────┘
▼
┌──────────────────┐
│ the guest's │ one thread.
│ WhatsApp chat │ one number.
└──────────────────┘ no handoff seam.
escalation fires ──► assistant goes SILENT (human mode)
├──► guest told "I'm connecting you with Priya"
├──► Priya opens WhatsApp and replies herself
└──► Slack (optional): a copy lands in her channel
── one-way. She CANNOT reply from Slack. ──
Every booking lands in MystFlo's own ledger. Nothing flows outward — not to Sheets, not to Airtable, not to Notion, not to a file. And the founder's only view of that ledger shows bookings that are still unpaid — the moment one is paid, it leaves her screen; the Attendees block + sheet below fix that (re-cut 2026-09-02 — Peyton: the list lives IN the Pulse tab and rises as a nested sheet, no longer a standalone page).
IN-CHAT MODES (A · B) OWN-FORM MODE (C)
Flow form submitted founder's own form
│ name · email · seats │ (MystFlo never sees it)
▼ ▼
┌───────────────────┐ ┌──────────────────────┐
│ MystFlo ledger │◄────── reads ───────│ her roster Sheet — │
│ (cohort_bookings)│ one way │ MystFlo TEMPLATE in │
└─────────┬─────────┘ │ her Drive (default) │
│ │ or a linked sheet │
│ └──────────────────────┘
▼ she ticks paid — the
tick is the billable
event
┌───────────────────────────────┐
│ Pulse · unpaid bookings only │ paid → disappears from view
└───────────────────────────────┘
✗ no write-back to her Sheet ✗ no Airtable / Notion
✗ no export
Every field is already stored on the booking, and all three verdict actions already
ship in the booking EF. Re-cut 2026-09-02 (Peyton ruling), amended 2026-09-03 (R37 — "right column as principle"): Attendees is a block that is the FIRST item in the Pulse tab's right slot
(noun-titled, ≤5 rows, "See all N ›"), the booking link/share/flyer sit under it, and the full list rises as a nested, scrollable
sheet over that tab — not a new tab (R30/R31 hold), not a full-width band, and no longer a routed page. The way in from the
wall is a slot inside the Pulse card, not a row hanging under it. This supersedes the 2026-07-28
"Pulse row → full-screen destination" contract and the #2924 route lock; the shipped
AssistantRecordsPage + RecordsEntryRow are now drift, not the contract. Deep link survives
as a URL param on the sheet: ?assistant=<id>&records=open (param name = build decision, CTO);
the old route becomes a redirect. Naming is per-assistant: Workshop shows Attendees, Checkout
Orders, Waitlist Signups — one component, one records contract. "Who's coming" survives as the
group heading inside. Event Booking (TMPL-EVBK01) rides these frames (design decision 2026-08-26) — this
re-cut IS the Event Booking update; only the program nouns change.
nestedBackdrop · ✕ / Escape close THIS sheet only · ?assistant=4L9&records=open · same sheet as master assistant-lifecycle.html #sec-9esortable — hidden at ≤1 page (20 rows/page); drawn here for shape only‹ PrevPage 1 of 1Next ›ModalWrapper presentation="sheet", contentWidth="wide", nestedBackdrop, broadcastCloseAll={false}.?assistant=…&records=open), not a route.FilterChipGroup, PaginationBar), never a third style.The frame above draws one populated table at one width. A founder meets six other
screens before or instead of it: the in-card slot and the in-tab block that open it, the same rows on a phone (as a bottom sheet), the day-one
empty list, the fetch in flight, the fetch that failed, and the deep link opened by someone who isn't
signed in — or isn't the owner. Mobile leads. The 6-column grid is a lg 1024
structure only; below it the identical rows render stacked, and no column is dropped — every field
stays visible, the layout changes, the data does not.
?records=openone component, noun is a proprow geometry identical in every state4 of 6 · 2 on the waitlist · needs-verdict first, then newest. Full context: the Pulse tab in 8b.
Your booking link is live. Share it and the first name lands here — usually within a day of posting.
Your booking link is live. Share it and the first name lands here — usually within a day of posting.
aria-busy="true" on the table; the shimmer is suppressed under prefers-reduced-motion.Nothing's lost — this is a connection problem on our end. Your bookings are safe.
Or ping us if it keeps happening.
?assistant=…&records=open), opened by the wrong personWe'll bring you straight back to this list.
<ProtectedRoute>; sign-in first. Return-to back to the sheet is a build gap (no platform-wide return-to exists — named, not invented). Never a guest preview: a roster has no public shape.?assistant= resolves nothing she owns (RLS returns no row), so both params are dropped and Pulse renders her own wall. No sheet, no count, no name, no "permission" wording — byte-identical to a deleted assistant. Replaces the old in-surface "This list isn't here" state: there is no surface without the assistant.?assistant=<id>&records=open / When resolved / Then the records sheet opens stacked over the post detail sheet's Pulse tab; ✕ and Escape close only the sheet; refresh, bookmark, second device reopen it. The post-activation tab set stays Pulse · Overview · Personalize · Connect. (AC-WSBK-23's "same full-screen destination" wording no longer matches — never edited here.)?assistant= resolves no owned assistant / Then Pulse renders their own wall with no sheet, no count, no name, no permission wording — byte-identical to a deleted assistant. (AC-WSBK-31's "This list isn't here" state no longer exists as a surface.)ModalWrapper sheet (nestedBackdrop, broadcastCloseAll={false}, contentWidth="wide"), so the founder never loses the Pulse context — ✕ returns her to the tab. The deep-link reason that justified the route survives as a URL param on the sheet (?assistant=<id>&records=open; name = build decision, CTO): bookmark, refresh, second device all reopen it. The old /pulse/:assistantId/records route becomes a redirect. Still not a PageSlideOver — a data-review-and-decide panel is a sibling sheet (2026-07-01 lock).AssistantRecordsPage · in-card slot in AssistantSummaryCardPost + retire the RecordsEntryRow mount · records block in the post modal's Pulse tab · RecordsSheet on ModalWrapper over AssistantModalShell (z-index above the shell) · warm-stale flag shared by block + sheet · ?records=open persisted like ?assistant=.email: null bug — discarding the one channel not rate-limited by WhatsApp's 24h window.Bookings and payment relay keep working. Only the reminder is blocked until the wording is fixed.
Resubmitting goes back into review — same one-to-two-day window.
assistant-lifecycle.html frame 7e), adapted to this assistant's two templates.Your assistant is live. Bookings and reminders are paused until your workshop details save.
Pauses the assistant and opens Personalize so you can fix the details there. Bookings and reminders start the moment they save.
activate-assistant treats a failed seedWorkshopCohort as non-blocking (index.ts:1915-1918) and the founder sees the ordinary Frame 6 success screen with no cohorts row underneath — no bookings, no reminders, no signal. This frame is the target state once #4634 (B2) forwards a cohortSeedFailed flag the same way workshopTemplatesPending/workshopTemplatesFailed already ride the activation response..fail-badge, same .notice block ("What happened" instead of "What they flagged"), same stacked primary/outline buttons, same fine-print line — no new card style, colour, or icon introduced. Red badge because this blocks the founder's actual capability (bookings), not just one channel — a stricter case than 6b's reminder-only block.Message us link (wa.me/<MYSTFLO_SUPPORT_NUMBER>, config value, never hardcoded, run reference prefilled — same pattern as the master's assistant-lifecycle.html sec-7 "(b) Failed" sub-frame); no new panel, the card just updates in place..att-row grammar (! icon · title · outline button) this file's own sec-8 already draws for the pending-templates row; tapping Retry reopens 6c.Your assistant is live. Bookings and reminders are paused until your workshop details save.
Pauses the assistant and opens Personalize so you can fix the details there. Bookings and reminders start the moment they save.
01 · Your workshop / 02 · How guests pay / 03a / 03b / 04a / 04b categories), every field showing its saved state, nothing clickable — with booking_keywords hidden. Not redrawn here on purpose: one drawing, one contract.Every bubble below draws the in_chat
branch (the default) — a guest DMs, the assistant captures name/session/headcount, and relays the
founder's payment link. When the founder's Flow is published, this typed capture is replaced by the
native in-chat form — drawn in the Secondary sub-frame below (payment still relays outside the
form; the US has no in-Flow payment). The own_form branch (guest is relayed the founder's own booking-form link
and books there instead — AC-WSBK-14) has no equivalent bubble frame drawn yet; see the note
at the end of this section rather than a screen invented for this pass.
pending_payment booking row this reply represents.shouldCaptureCohortBooking, shared by this booking engine) requires name AND email before a booking is persisted or the payment ask is shared; only the retreat-vs-class wording changes — the three-field capture contract itself is unchanged and correct.copy_payment_ask is bridged to cohorts.payment_instruction only at activation, and only the WA-Flows path reads it at runtime — the default in-chat path never does, so the AI falls back to a generic hand-off line instead of relaying the real link. The proposed fix makes the in-chat runtime read the same field, closing the gap.The two locked, Meta-approved template bodies (Booking confirmation · Class reminder) live at full size in Part II · Components — seated in the flow at Personalize 04a (sec-3/3b) and in every sec-9 thread.
{{program}} aliases {{workshop_name}} (the Event card's seeded token now resolves), and a missing date degrades to the name-only line, never a literal token or a blank.booking_surface='own_form', in-chat capture never fires; the assistant relays the booking-form link and sets intent FORM_RELAY instead.booking_form_url is required the moment own_form is picked, enforced on all three save paths (activation, update-config, reactivation).Section 7 shows individual messages being fixed. This section shows the whole thread, unbroken, for every mode a founder can pick — and, beside each thread, exactly where every message's words come from and where the founder meets them in Personalize and the live preview. One card per mode; nothing here is invented — every bubble traces to a named template, config field, or the real runtime string, and anything not built lives in Part III · Future, never here.
Payment structure and where guests book are two independent settings, not one menu. A founder picks one of each — so the five things that feel like five options are really four shipped combinations plus one unbuilt entry path. Drawing them as one list would imply choices that don't exist (and hide ones that do).
where guests book ──► in_chat (default) own_form
payment structure ┌──────────────────┐ ┌──────────────────┐
│ │ MODE A │ │ │
├── full_prepay (default) ────────│ shipped │ │ MODE C │
│ └──────────────────┘ │ shipped │
│ ┌──────────────────┐ │ (payment shape │
├── free_rsvp ─────────────────────│ MODE B │ │ changes only │
│ │ shipped │ │ one word) │
│ └──────────────────┘ └──────────────────┘
│
└── deposit_balance ── hidden on this card — a retreat concept, lives on Retreat Booking
┌ MODE D — 2+ booking cards live on ONE WhatsApp number. Supported product usage ──┐
│ (a founder running concurrent programs), not a misconfiguration. "what's on?" │
│ or a bare "book" → deterministic name+date list; a unique full program name │
│ routes straight to that card's form. SHIPPED (#3789) — drawn below. │
└─────────────────────────────────────────────────────────────────────────────────┘
┌ MODE E — guest DMs and the AI already reads booking intent. What's missing is ───┐
│ the CHOICE within ONE assistant: offering its several open workshops to pick │
│ from. NOT BUILT — two real blockers, drawn in Part III · Future. │
└─────────────────────────────────────────────────────────────────────────────────┘
The default, and the one most founders will run. The guest never leaves WhatsApp; the money does — payment happens on the founder's own link, because WhatsApp's in-form payment is India/Singapore only.
pending_payment row and the assistant relays the founder's payment ask.wa.me only pre-fills "book") · 2 invite = copy_flow_intro founder field (default names the program + date, #3789) over WhatsApp's native form (per-founder Flow from flow-json/workshop-booking.ts; unpublished → typed capture, nothing waits) · 4 = ONE message, thank-you first (runtime string, composePaidBookingThankYou — "seat is held, confirmed the moment your payment lands", P2: pay AFTER the form), then the founder's Payment ask (Brain policies.payment wins when set); a single URL in the ask is lifted onto a native Pay now button (sendWhatsAppCtaUrl) · 5 confirmation = workshop_booking_form_confirmation template, ≤30s after Mark paid · 6/7 = ONE class_reminder body, both offsets.shouldCaptureCohortBooking) — no row without both; seats re-checked at submit, not send. Mark paid (or a roster tick) is the ONLY door to confirmed — the assistant never takes the guest's word.Same thread as Mode A with the money taken out — and taking the money out removes two steps, not one. There is no payment ask, and no Mark paid: the booking confirms itself the moment the form comes back.
free_rsvp workshop skips the payment-ask leg entirely and goes straight to confirmed.captureCohortBooking calls learnPaid on submit, same meter, same exactly-once; no "awaiting payment" state ever appears in Pulse.composeFreeBookingConfirm — "you're in / free / see you {date}"), NOT the locked Meta template. learnPaid runs with skipWhatsApp (honored only for free_rsvp) — it still meters, emails, and confirms; the locked workshop_booking_form_confirmation template is reserved for Mark paid on the paid card. Typed capture sends the same session confirm; an already-confirmed retry never re-texts the guest.cohorts.start_date, not payment. Free RSVP is the one mode needing no nudge machinery (no unpaid state to chase).The assistant hands over the founder's own form link and steps aside — no capture, no payment ask, no in-chat form. Everything that happens next happens somewhere MystFlo cannot see, and comes back only through the roster sheet.
FORM_RELAY. AC-WSBK-15 ↗ — booking_form_url required on all three save paths.booking_form_url, its sentence = copy_booking_form_link (or AI-written around the real link when blank); confirmation + reminders = the same Meta templates, with the confirmation landing hours later because it waits on the founder.booking-roster-sync) is the ONLY door a booking enters through, manual by design — and it is the billable event (confirms the booking, fires the $1.75 charge). Reminders are the part that stays automatic.full_prepay vs free_rsvp only shapes wording here ("pick your spot and pay there" → "pick your spot there") — MystFlo never asks for or learns payment independently in this mode.Several booking cards Active on one WhatsApp number is supported product usage — a founder running concurrent programs — not a misconfiguration. Every booking link prefills the same generic keyword by design, so when the message doesn't uniquely name a program, the assistant answers with a deterministic list instead of guessing — never the wrong Flow, never Brain improvisation.
composeBookingCatalogReply — sorted by start date then name, one bullet per live card. Deterministic, not AI-composed: two cards can't be told apart by the shared "book" prefill, and guessing sent an Event guest into a Workshop Flow (live test, 2026-08-20).resolveBookingPhase0, whatsapp-routing.ts).This is already built, and the answer to "route to the owner or swap in the on-brand assistant?" is both, in that order — the handover is automatic and the founder never configures it.
guest asks something off the booking script
│
▼
┌ can Workshop Booking answer it? ┐
│ from the workshop details or │──── yes ──► answers, thread continues
│ the Brain │
└────────────┬────────────────────┘
│ no (intent = UNSURE)
▼
┌ is On-Brand Support live on ────┐
│ this same WhatsApp number? │──── no ───┐
└────────────┬────────────────────┘ │
│ yes │
▼ │
┌ Support takes one more shot with ┐ │
│ the full Brain + FAQ, and the │── fails ─┤
│ answer is fact-checked before │ │
│ it is allowed out │ │
└────────────┬─────────────────────┘ │
│ passes ▼
▼ ┌ founder gets the thread ─────┐
guest gets a real │ holding line to the guest · │
answer — no handoff │ human mode on · host pinged │
└──────────────────────────────┘
"get me a human" / refund / date move ─────────────────────────► straight to founder,
no Support attempt
assistant-flows/assistant-lifecycle.html is the primary structural contract; assistant-flows/on-brand-support.html is the sibling exemplar this file borrows its token block and component classes from verbatim.prd-as-workshop-booking.md §Test Plan (AC-WSBK-* / AC-BILL-*) · DB migration 20260710080000_2012_b4_wsbk01_card.sql · ConfigureStep.tsx isBookingFamily gate · tests/e2e/assistants/tmpl-wsbk01.spec.ts.Business name ("Ember Clay Studio"), guest names, and phone numbers are illustrative. This file draws the proposed target.
workshop-templates.ts copy with example vars resolved; nothing invented.Only shown when you have On-Brand Support running on this number.
The guest hasn't tapped a link and hasn't said "book" — they just message the business, and want to choose between what's on offer. More than half of this already works: the assistant reads booking intent from plain English today (sec-9, Mode A design notes), and since #3789 a founder running 2+ booking cards on one number gets the deterministic name+date list — shipped, drawn as sec-9 · Mode D. What is still missing is the choice within one assistant: a single Workshop Booking card knows about exactly one workshop, so it cannot offer its own list.
Today, none of this exists. Every mode above ends its automatic behaviour the moment the assistant sends the form or the payment link. If the guest reads it and does nothing, nothing happens — no nudge to them, no signal to the founder, ever. The three toggles that sound like they'd cover this ("notify me on unpaid timeout" and friends) are dead controls with no runtime reader, which is why this rescope removed them from Personalize rather than leave them lying.
Whether the guest should see a WhatsApp Flow waitlist form depends on which of three sold-out paths they hit — they behave nothing alike, and only one of them is missing anything a form could fix. Paths 1 and 3 below are covered in sec-13 and are fixed in code, not redesigned here — this section is scoped to path 2 only.
email: null, discarding data validated two
lines earlier (sec-13). No second form
belongs here. Asking someone to fill another form immediately after “sorry, sold out”
is friction with nothing to show for it — the fix is the one-line email bug, not new UI.email: null, because nothing was ever collected. This is the
only path with no name and no email to lose — they were never asked for. That's what a form
would add here, and why it belongs on this path and no other. Proposed form below.Same shape as the booking Flow (sec-9, Mode A) — a native in-chat form instead of typed capture — fired the moment the sold-out reply goes out, so joining the waitlist costs the guest one tap, not a typed back-and-forth.
provisionWorkshopFlow, sec-9) — not a reuse of the booking Flow, since it asks
for less and fires on a different trigger. Two fields only: name + email.email: null bug (sec-13).The Connect tab (sec-4) duplicated with ONE tweak: a deferred Stripe row. CPO-ruled post-beta (#3152, ADR-019's phase-2 door) — per canon U9 it must NOT appear in the Active Connect frame, so it lives only here.
FUTURE own_form roster-pick row (separately tracked — the shipped Connect tab withholds the Storage row entirely under own_form and has no roster-pick UI yet; see sec-4's cond-block note). Per canon U9 it is drawn only here, never in the Active frame — collapsed; this frame otherwise exists for the Stripe row.
Wallet · Brain Check · template notice unchanged from sec-4 — omitted here, this frame exists for the one new row.
learnPaid settle path (host Mark-paid · roster tick · Stripe webhook), all metering identically — never a second billing path. Deferred post-beta by CPO ruling; tracked as #3152.The recurring-schedule question (sec-3) is resolved, not deferred — single-session ships now; the lightweight repeating-session increment lives in #2798, tracked separately.