Design preview · Workshop Booking (TMPL-WSBK01) · 8-section structure · updated 2026-08-06

← Design HQ  ·  HTML on GitHub  ·  PRD — prd-as-workshop-booking.md

Workshop Booking — the flow, organized by journey

Workshop Booking — Discover, set it up, go live, then the guest thread.

SizePortrait scrolls 375 · sideways is a slide deckNotes
Design brief — overview · scope · journey (closed)

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.

In this build sections 4–7 = the Active contract
  • Full founder journey: Explore card → Overview → Personalize (375 + desktop split) → Connect → Try → Activating / Activated → Pulse + Attendees destination.
  • Two payment structures (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).
  • Payment relay only: link today; QR-upload picker is specified-not-built (#3153, drawn in place in Personalize 04b). Confirmation <30s of payment learned; reminders 1 day + 1 hour (AC-WSBK-02/03 ↗).
  • Template-approval honesty states (sec-4 banner, sec-6 amber notice) and the rejected-template fix path (sec-6b — drawn as contract, not built in code; the shipped rejected screen draws a different lifecycle moment — see parity ledger).
  • Handover in the same thread (WhatsApp Coexistence) + one-way Slack mirror (sec-12); the “05 · When I need to step in” Personalize category is proposed, not built.
  • Attendees records destination (M1, approved 2026-07-28, frontend-only, not built — sec-13 is its build contract).
Explicitly out drawn only in Future design (Part III), never a build target
  • F1 workshop picker (Mode E) · F2 quiet-guest follow-ups · F3 waitlist capture promise · F4 Stripe auto-settle (#3152, post-beta CPO ruling).
  • Not this assistant at all: deposit + balance is Retreat Booking (TMPL-RTBK01); one workshop per activation — two workshops = two assistants; no write-back to founder sheets, no Airtable/Notion.
Build status Beta · code-verified in parity/parity-workshop-booking.md
  • Shipped: capture engine + Modes A/B/C end to end (sec-9 MATCHES) · payment-relay fix #2796 · entry-link fields #2821 · 03a reach-you regroup (#3205, render-verified) · deposit_balance radio removal (#3192) · Connect two-role rows — StorageRoleRow/AiModelPicker (#3198, PR #3209) · MystFlo Storage provider (#3178, closed).
  • Drawn ahead of code: sec-6b rejected-template path · Nudge/attention actions (sec-8) · Attendees M1 (sec-13) · step-in category 05 · QR picker #3153.
  • Catalog copy is FINAL here — live 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
The four data shapes
  • Config schema (drawn in sec-3/3b, categories 01–05): workshop facts (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 ↗).
  • Locked message copy: confirmation + reminder bodies from 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.
  • Booking record (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).
  • Roster sheet (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
The guest side — every scenario across one spine

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.

Scenario
1 · Trigger
2 · Interact
3 · Payment
4 · Confirm
5 · Remind
6 · If they go quiet
A · Full payment, in chatfull_prepay · in_chat
the default
Taps the link, or just DMs. The AI reads intent — no keyword needed.
Assistant sends the native Flow form. Not approved yet → typed capture instead.
Founder's own link, relayed verbatim. Never inside the form.
Founder taps Mark paid → template lands <30s.
1 day + 1 hour before. One template, both times.
✗ Nothing. Seat held forever, founder never told.
B · Free RSVP, in chatfree_rsvp · in_chat
same as A
same as A — form drops the price/date field
— no payment leg at all
Automatic on submit. No founder tap.
same as A
Nothing to chase — the only clean lane.
C · Their own booking formeither payment · own_form
roster sheet required
same as A
Assistant relays the founder's form link and steps aside. No capture.
On the founder's own form — MystFlo never sees it.
Waits on a manual roster tick. Hours, not seconds.
same as A — roster bookings are reminder-eligible
✗ Nothing — and MystFlo can't even tell paid from unpaid.
Sold out → waitlistany mode · seats re-checked
at submit, not at send
same as A
Form submitted, but the last seat went. Guest is added to the waitlist and told so.
— no charge
✗ No confirmation — there's no seat
✗ No reminders
Promise broken — "you'll be first to know" is never sent by anything.
Asks something elseany mode · already built
zero founder setup
mid-thread, any time
Answers from workshop details + Brain. Can't → On-Brand Support tries once, fact-checked.
unchanged
unchanged
unchanged
Still fails → founder takes over in the same thread. See sec-12.
E · Picks from a listNOT BUILT · 2 blockers
see sec-10
Just DMs — no link, no keyword. Intent reading already works.
Offers the open workshops; guest taps one. This is the missing piece.
then exactly A or B
then exactly A or B
then exactly A or B
inherits whatever A/B does

Column 6 is the honest column. Four of six 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.

Sequencing principle BINDING
  • Personalize's 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 ↗).
How the booking form is sent
  • Both entry points are one trigger path, not two. A 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.
  • The Flow fronts the typed capture — it doesn't replace it. The native form sends when the founder's Flow is published; otherwise the assistant falls back to typed capture, unchanged. A Workshop Booking instance behaves as typed-capture until its Flow is Meta-approved & published.
  • Payment is never inside the form. WhatsApp in-Flow payment is India/Singapore only — not the US. After the form submits, the assistant relays the founder's own external payment link, exactly like the typed path.
  • The form is auto-built per founder — no Meta Business Suite step. It's created, uploaded, and published on the founder's own WABA at activation. One published Flow per founder, version-swapped on config edits. Founders never open Flow Builder.
Part I — The flow every journey state, desktop then mobile, grouped by the PRD's phases — the 1:1 build contract, matched pixel for pixel. Parts live at full size in Part II · Components; deferred material in Part III · Future design, never a build target. Acceptance ids (AC-* ↗) live in prd-as-workshop-booking.md §Test Plan.
Discover
Discover “is this worth a tap?” · sec-1 the Explore card
Workshop Booking
Takes the booking, relays your payment link, and reminds them before class — so seats don't go empty and DMs don't pile up.
$1.75 per bookingBeta WhatsApp Business
Personalize Connect Try
tap for detailsActivate
Frame 1 · Explore card [Discover]
  • Story — As a founder scrolling Explore, I see the real price and the one required app before I ever tap in, so there's no surprise once I start.
Design notes
  • Price is $1.75 per confirmed booking, flat fee. Only WhatsApp Business is a required app — the own_form Google Sheet is a conditional, backend-enforced requirement (sec-4 note / F4), not a card-level chip.
768 not yet drawn for this flow. Desktop and 375 are the contract.
This step is 1180. No 375 drawing — use the Desktop size.
Evaluate
Evaluate “what does it actually do?” · sec-2 the Overview tab
Workshop Booking
Takes the booking, relays your payment link, and reminds them before class — so seats don't go empty and DMs don't pile up.
$1.75 per booking
Overview Personalize Connect Try
What it does

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.

How it works
01
You
Teach it once

payment link, policy, voice — from your Brain

02
Customer
DMs to book a seat

picks a session, gives headcount

03
Assistant
Captures + relays
AItakes the booking, sends your payment ask
free RSVP mode -> skips the payment ask, straight to confirms + reminds
04
You
Marks it paid

one tap, or your sheet ticks it

05
Assistant

Confirms + reminds

instant confirm, then 1 day + 1 hour before

Key benefits
Captures the booking while you're mid-class
Guests get an answer at the moment they're deciding — not whenever you next check DMs.
Relays your real payment link — every time
The same link or bank details you wrote, sent exactly as you wrote them. You decide every payment.
Reminds them before class
One reminder template, sent before the session — fewer no-shows without you typing a word.
Never invents a date or price
If it isn't in your setup, it says so and brings you in.
Frame 2 · Overview tab [Evaluate]
  • Story — As a founder, I read exactly what it does and how it works before I configure anything, so Personalize never asks me to decide something I don't understand yet.
Design notes
  • "Relays your payment link — every time" is the promise this Overview tab makes — see sec-7 for the payment relay fix that makes it true in the default in-chat mode.
  • Catalog copy is FINAL here — "a reminder before class" replaces the live long_description; ships via migration + PRD in the same PR (guard-railed, check-catalog-copy.sh) — the build PR must carry it.
  • "How it works" = production 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).
768 not yet drawn for this flow. Desktop and 375 are the contract.
This step is 1180. No 375 drawing — use the Desktop size.
Activate
Activate “set it up once” · sec-3/3b Personalize · sec-4 Connect · sec-5 Try
dimmed Explore page peeks — revealGap · app header stays visible above
Workshop Booking
Takes the booking, relays your payment link, and reminds them before class — so seats don't go empty and DMs don't pile up.
$1.75 per booking
Overview Personalize Connect Try
01 · Your workshop
Workshop name *
Saturday Pottery Workshop
Workshop date *
2026-07-28
Price per seat
$42
Total seats
10
02 · How guests pay
Payment structure
Full prepay
Default. Guest pays in full to hold the seat.
Free RSVP
No payment — just a headcount.

Retreat-style deposits live on Retreat Booking, not here.

03a · How guests reach you — two ways in
1
Your booking link — share it anywhere
Bio, launch post, story, DM — one tap opens the chat.
Your booking link — generated, read-only
wa.me/15551234567?text=Book — Wheel Throwing Class

Tapping it opens WhatsApp with “Book — Wheel Throwing Class” pre-filled and sent straight to your WhatsApp — routes to this workshop's form, even with several assistants on the number.

2
They just text you — no link needed
Guests who already have your number, or reply to a story.

The AI reads intent — “any spots left tuesday?” books a seat just like “book” does. Nothing to configure.

Tell assistants apart conditional
book · count me in

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.

03b · Where guests book — what they see next

However they arrive — link tap or a direct message — it's one path in. What they see next is your call:

In this chat
Default. WhatsApp's own tap-through form — no typing back and forth.
Your booking list lives in Attendees — on this assistant, in Pulse.
Your own booking form
The assistant shares your form link; bookings land in your sheet, you tick paid there.
Your roster sheet is the record — MystFlo creates a pre-structured one in your Drive (default), or links the one you already have. Either way, we only read it.
Linking your own sheet? Any table works — you map your columns in Connect, and MystFlo has no information about a booking until it next reads your sheet.

Shows the form once WhatsApp approves it; guests text back and forth until then — nothing to set up on your end.

04a · Booking messages
Booking invite — one field, same for every entry path · sent as a regular message, no approval needed
Tap below to grab your spot for Wheel Throwing Class:
Booking confirmation locked
Meta-approved · not editable
Header: "You're booked — Wheel Throwing Class"
Hi Maya, it's Priya — your seat for the Wheel Throwing class is confirmed. We'll send a reminder before class. Reply here with any questions.
Class reminder locked
Meta-approved · not editable one preview, not two
Hi Maya, it's Priya — just a warm reminder about your upcoming session. Your spot's all set and I'm looking forward to seeing you. If something's come up and you can't make it, reply here so I can offer the place to someone else. See you soon!
04b · Payment message
Payment method not built · #3153
Payment link
Default. Stripe, PayPal, or any URL you paste.
https://buy.stripe.com/8wM4kb…
QR code
Upload your bank or PayNow QR once — sent as an image, guest scans and pays directly.
paynow-qr.png
Saved — sent with every payment ask
Replace Remove

Shown when “QR code” is picked. No QR yet → a dashed drop box (upload once, it stays); Remove reverts to it.

Payment ask — one field, Brain-prefilled · sent as a regular message, no approval needed
Here's how to pay for your spot — reply once you've sent it and I'll confirm.

Same words in both modes — the phone preview shows the link message and the QR variant in sequence. Only the delivery vehicle changes.

04c · Reminder channels — required
WhatsAppEmail

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.

05 · When I need to step in not built · proposed
If the assistant can't answer conditional
Let On-Brand Support try first
Uses your full knowledge base. Costs one extra AI run per question — only when it's actually used.
Come straight to me
No second attempt, no extra cost.

Only shown when you have On-Brand Support running on this number.

What guests hear while they wait — editable
I'm connecting you with Priya — she'll come back to you shortly.
Where you get pinged
WhatsApp — this thread
Always on. Reply from your phone as usual.
Connected
Slack
A copy in your channel. On the app? You reply in WhatsApp. API-only number? Reply right in the thread — it reaches your guest.
Connect
·· preview — “In this chat” · once your Flow is live ··
hi! any spots left for tuesday's class?
Tap below to grab your spot for Wheel Throwing Class:
Wheel Throwing Class
Sat 2 Aug · $42 per seat · 3 seats left
Name
Email
Party size
2026-07-28
Confirm booking
Real form fields after activation — never faked here
Here's how to pay for your spot — reply once you've sent it and I'll confirm.
https://buy.stripe.com/8wM4kb…
When “QR code” is picked (04b), this bubble sends instead:
your QR as an image — the same payment-ask words as its caption
done — just sent it!
you tap Mark paid — or your roster shows it
You're booked — Wheel Throwing Class
Hi Maya, it's Priya — your seat for the Wheel Throwing class is confirmed. We'll send a reminder before class. Reply here with any questions.
Then: the Class reminder (04a) — same thread, 1 day + 1 hour before class

Works the same whether they tapped your wa.me link or typed this directly — the AI reads intent, not just the word.

·· preview — “Your own booking form” (03b) ··
hi! any spots left for tuesday's class?
Yes! Here's where to book: forms.emberclay.com/wheel — you'll pick your spot and pay there. Shout if anything's unclear.
That relay is the whole guest experience — no capture, no payment ask (AC-WSBK-14). Confirmation + reminders fire from your roster tick.
Frame 3b · Personalize (desktop split) [Activate]
  • Story — As a founder on desktop, I fill the same Personalize fields while watching a live preview of how the assistant will actually reply — same fixes, wider canvas.
Acceptance
  • Same criteria as sec-3 — AC-WSBK-16 ↗, AC-WSBK-05 ↗, AC-WSBK-17 ↗ — this frame draws the desktop width of the identical fix, not a second behavior.
Design notes
  • See sec-3 for the full rationale on every fix shown here (recurring-schedule resolution, required date, dead-toggle removal, pay/book split, radio bug, entry-link parity #2821, the 03a/03b reach-vs-book split, the conditional “Tell assistants apart” block, the cancellation-policy removal, the 04a/04b Messages split, the #3153 payment-method picker with dual guest-sees previews, and the 05 step-in category pulled in from sec-12 — Peyton 2026-08-04). This frame exists only to show the same content at the desktop split-pane width, alongside the live preview column.
dimmed Explore page peeks — revealGap · app header stays visible above
Workshop Booking
Takes the booking, relays your payment link, and reminds them before class — so seats don't go empty and DMs don't pile up.
$1.75 per booking
Overview Personalize Connect Try
01 · Connect apps
Communication WhatsApp Business
Matches your Reminder channels from Personalize — WhatsApp always required for booking chat
Connected
WhatsApp
WhatsApp Business
Which number should this assistant answer on?
+1 555-0142 ▾
Email — if picked in Personalize
Resend
Not required — Email wasn't picked as a reminder channel
Not connected
AI Model MystFlo AI
Connected by default. Choosing your own AI model is coming later.
Connected
Storage MystFlo Storage
Your Attendees list on Pulse — every booking lands there, captured in chat or read from your roster. Nothing external to connect.
Connected
Storage — when “Your own booking form” is picked (03b) conditional

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).

Two message templates are waiting on approval Your booking confirmation and class reminder need one-time approval from Meta before they can send — usually resolved within a day or two of activation. Bookings still capture and your payment link still relays in the meantime.
02 · Wallet
Wallet balanceEach confirmed booking draws $1.75 from your prepaid balance $86.40
03 · Brain Check
Business Basics
Business name
Confirmed
Edit in Brain →
Brand Voice
Brand voice
Confirmed
Edit in Brain →
FAQs & Policies
Cancellation policy
Confirmed
Edit in Brain →
Founder & Story
Contact info for sign-off
Confirmed
Edit in Brain →
4 of 4 ready — it can answer guests
Gap closed (2026-08-07)
  • This frame was TOC'd under the desktop Activate group (alongside sec-3b/sec-5) but only ever drawn as a 560px .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.
Frame 4 · Connect tab [Activate]
  • Story — As a founder, I connect the WhatsApp number this assistant should answer on, see that my bookings' home is already connected with nothing to set up, and see plainly that two templates are waiting on Meta before I activate.
Acceptance
  • AC-BILL-04 ↗ — activation is blocked unless both a 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.
  • AC-WSBK-15 ↗ — 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.
Design notes
  • A tethered WhatsApp-number picker is required when the connected account has more than one number. The dimmed cond-block documents the own_form state — the Storage row is withheld there, not re-badged; with "In this chat" (the default) the lit row above is what renders. Every row carries its ROLE label (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, closedSTORAGE_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).
  • The badge is the picker, and the caret is now structural on every row but AI Model (B9, kit 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.
  • Communication now carries the caret too (2026-08-07, supersedes the "keeps its own control" reading below) — Personalize's new "04c · Reminder channels" field is a DECISION only (WhatsApp always required, Email optional), and the caret expands into one pick-group per channel: WhatsApp (the existing which-number control now nests INSIDE this group, unchanged in function — see next bullet) and Email (Resend). Drawn NOT-CONNECTED here deliberately — Workshop's booking confirmation and class reminder are locked, Meta-approved WhatsApp templates (04a) with no Resend-side equivalent copy yet; picking "Email" in Personalize without that copy existing is a real build gap, not a design decision, so the Email group renders dimmed/optional rather than pretending parity. Building real Email reminders here means writing a second, non-WhatsApp version of the class-reminder copy — flagged, not solved in this pass.
  • The WhatsApp-number picker keeps its own control, now nested inside the WhatsApp group. When the connected account has more than one number, the founder picks which one this assistant answers on via the right-aligned +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.
  • One connect list, no badge tier (Peyton 2026-08-04): MystFlo AI and MystFlo Storage are real rows in 01 · Connect apps, not an “Included” group. AI is Connected by default (a future model choice is named, future-tense only — never “bring your own key” today). MystFlo Storage is Connected with zero clicks in the default in-chat mode. Superseded in part (2026-08-05, #3178 CTO/Design review + #3184): under own_form the Connect-tab row is WITHHELD entirely — “Connected” would be a false claim when the roster lives in her own Sheet — and the conditional Google Sheets roster row is not built yet (backend-only gate; future contract in F4). Attendees as the in-app data home in both modes (roster-read bookings land there too) remains true at the data layer (sec-13, M1) — it just isn't claimed as a Connect row under own_form.
  • The roster sheet is READ-only (re-affirmed Peyton 2026-08-04): MystFlo reads one way — the roster tick IS the billable event (marks the booking 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.
  • Template-approval banner: plain, factual, no internal names — repeated at success (sec-6). Brain check reads four sections: business name + brand voice, cancellation policy, founder contact info.
Overview Personalize Connect Try
What you're activating
Workshop date2026-07-28
Price / seats$42 · 10 total
PaymentFull prepay · in this chat
Messagesconfirmation + reminder — locked, pending Meta
Brain 4 of 4 ready
Wallet$86.40 · $1.75 per booking
Trying it
Replies are triggered by real customer messages Message your WhatsApp after activation to see it live. There's no way to fake a guest message before then — so we don't pretend to.
Send yourself the first one Right after you activate, message your own business number and go through the booking flow yourself — pick a spot, see the confirmation, the real thing.
Name this one
Weekly wheel-throwing class
Activate — $1.75 per booking

Runs on your WhatsApp from the moment you activate. Pause anytime.

Frame 5 · Try tab [Activate]
  • Story — As a founder, I see exactly what I'm about to turn on — date, price, payment/booking mode, message status, Brain readiness, wallet — in one read-back before I commit.
Acceptance
  • AC-WSBK-16 ↗ — the required date shown in the read-back is what Try/activation actually gates on.
  • AC-BILL-04 ↗ — the wallet balance shown here is what the same-name guard checks before activation succeeds.
Design notes
  • Honest test — same pattern as every WhatsApp assistant: no pre-activation test run exists for chat-triggered assistants; the master contract (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).
  • #3138 — self-test nudge is booking-specific: "Send yourself the first one" tells the founder to go through the actual booking flow (pick a spot, see the confirmation) rather than the master contract's generic "message your own number" line — a single reply doesn't prove booking capture works, walking the flow does. First-time activation must never show a real "Run test" button or an "activate without a test run?" confirm dialog for this template — both notices above are the whole Try experience, on every activation path.
768 not yet drawn for this flow. Desktop and 375 are the contract.
Closed — the form keeps the full working height
dimmed Explore peeks — tap to return
Overview Personalize Connect Try
01 · Your workshop
Workshop name * required
Saturday Pottery Workshop
Workshop date * required
2026-07-28
Price per seat
$42
Total seats
10
·· preview — “In this chat” · once your Flow is live ··
hi! any spots left for tuesday's class?
Tap below to grab your spot for Wheel Throwing Class:
Wheel Throwing Class
Sat 2 Aug · $42 per seat · 3 seats left
Name
Email
Party size
2026-07-28
Confirm booking
Real form fields after activation — never faked here
Here's how to pay for your spot — reply once you've sent it and I'll confirm.
https://buy.stripe.com/8wM4kb…
When “QR code” is picked (04b), this bubble sends instead:
your QR as an image — the same payment-ask words as its caption
done — just sent it!
you tap Mark paid — or your roster shows it
You're booked — Wheel Throwing Class
Hi Maya, it's Priya — your seat for the Wheel Throwing class is confirmed. We'll send a reminder before class. Reply here with any questions.
Then: the Class reminder (04a) — same thread, 1 day + 1 hour before class

Works the same whether they tapped your wa.me link or typed this directly — the AI reads intent, not just the word.

·· preview — “Your own booking form” (03b) ··
hi! any spots left for tuesday's class?
Yes! Here's where to book: forms.emberclay.com/wheel — you'll pick your spot and pay there. Shout if anything's unclear.
That relay is the whole guest experience — no capture, no payment ask (AC-WSBK-14). Confirmation + reminders fire from your roster tick.
Scrolled — payment, booking surface, message copy
dimmed Explore peeks — tap to return
02 · How guests pay
Payment structure
Full prepay
Guest pays in full to hold the seat. Default.
Free RSVP
No payment — just a headcount.

Retreat-style deposits live on Retreat Booking, not here.

Scrolled further — how guests reach & book you, message copy, when to step in
dimmed Explore peeks — tap to return
03a · How guests reach you — two ways in
1
Your booking link — share it anywhere
Bio, launch post, story, DM — one tap opens the chat.
Your booking link — generated, read-only
wa.me/15551234567?text=Book — Wheel Throwing Class

Tapping it opens WhatsApp with “Book — Wheel Throwing Class” pre-filled and sent straight to your WhatsApp, so it routes to this workshop's form even if you run several.

2
They just text you — no link needed
Guests who already have your number, or reply to a story.

The AI reads intent — “any spots left tuesday?” books a seat just like “book” does. Nothing to configure.

Tell assistants apart conditional
book · count me in

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.

03b · Where guests book — what they see next

However they arrive — link tap or a direct message — it's one path in. What they see next is your call:

In this chat
WhatsApp's own tap-through form — no typing back and forth, no separate app. Default.
Your booking list lives in Attendees — on this assistant, in Pulse.
Your own booking form
The assistant shares your form link; bookings land in your sheet, you tick paid there.
Your roster sheet is the record — MystFlo creates a pre-structured one in your Drive (default), or links the one you already have. Either way, we only read it.
Linking your own sheet? Any table works — you map your columns in Connect, and MystFlo has no information about a booking until it next reads your sheet.

Shows the form once WhatsApp approves it; guests text back and forth until then — nothing to set up on your end.

04a · Booking messages
Booking invite — one field, same for every entry path · sent as a regular message, no approval needed
Tap below to grab your spot for Wheel Throwing Class:
Booking confirmation locked
Meta-approved · not editable
Header: "You're booked — Wheel Throwing Class"
Hi Maya, it's Priya — your seat for the Wheel Throwing class is confirmed. We'll send a reminder before class. Reply here with any questions.
Class reminder locked
Meta-approved · not editable
Hi Maya, it's Priya — just a warm reminder about your upcoming session. Your spot's all set and I'm looking forward to seeing you. If something's come up and you can't make it, reply here so I can offer the place to someone else. See you soon!

One preview, not two — the 1-day and 1-hour sends use the identical body.

04b · Payment message
Payment method not built · #3153
Payment link
Default. Stripe, PayPal, or any URL you paste.
https://buy.stripe.com/8wM4kb…
QR code
Upload your bank or PayNow QR once — sent as an image, guest scans and pays directly.
paynow-qr.png
Saved — sent with every payment ask
Replace Remove

Shown when “QR code” is picked. No QR yet → a dashed drop box (upload once, it stays); Remove reverts to it.

Payment ask — one field, Brain-prefilled · sent as a regular message, no approval needed
Here's how to pay for your spot — reply once you've sent it and I'll confirm.

Same words in both modes — the phone preview shows the link message and the QR variant in sequence. Only the delivery vehicle changes.

05 · When I need to step in not built · proposed
If the assistant can't answer conditional
Let On-Brand Support try first
Uses your full knowledge base. Costs one extra AI run per question — only when it's actually used.
Come straight to me
No second attempt, no extra cost.

Only shown when you have On-Brand Support running on this number.

What guests hear while they wait — editable
I'm connecting you with Priya — she'll come back to you shortly.
Where you get pinged
WhatsApp — this thread
Always on. Reply from your phone as usual.
Connected
Slack
A copy in your channel. On the app? You reply in WhatsApp. API-only number? Reply right in the thread — it reaches your guest.
Connect
Frame 3 · Personalize (375) [Activate]
  • Story — As a founder, I set my workshop's date, price, seats, how guests pay and book, and my message copy in one place — the assistant needs nothing else to run.
Acceptance
  • AC-WSBK-16 ↗ — required start_date gates Try and activation.
  • AC-WSBK-05 ↗ — a workshop configured free_rsvp skips the payment-ask leg entirely, straight to confirmed.
  • AC-WSBK-17 ↗ — payment_mode and booking_surface render as real radio controls the founder can select.
  • AC-WSBK-01 ↗ — a guest tapping the workshop's shareable wa.me link drops straight into the booking flow.
Design notes
  • Categories 01 · 02 · 03a/03b · 04a/04b · 05: 03a “how guests reach you” = two equal entry cards (booking link · they-just-text, AI reads intent — nothing to configure); 03b “where guests book” radios = WhatsApp’s own tap-through Flow form (default) vs the founder’s own form, each with its destination line (Attendees on Pulse · a template roster created in her Drive — or a linked sheet — read-only); cancellation policy holds NO slot (Brain read, Connect owns it); 05 step-in is proposed/not built (why: sec-12 · parts: c-stepin).
  • Conditional grammar: “Tell assistants apart” keywords (routing disambiguation ONLY — never intent; renders only with 2+ assistants on the number) and 05’s On-Brand-Support radio pair draw as dimmed 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).
  • Messages: Booking invite + Payment ask = regular session messages, stated in their fine print (never submitted to Meta); Booking confirmation + Class reminder = Meta-approved locked-tpl previews, copy verbatim (C1; ONE reminder body covers both offsets); #3153 payment method (not built) = link default (URL field) vs QR (saved stage-row: thumbnail · Replace · Remove; empty = dashed drop box) — the ask is the caption/body in BOTH modes, only the delivery vehicle changes (AC-WSBK-34 ↗ · AC-WSBK-35 ↗; gap #6b bridge must be verified in the same PR).
  • Preview = the full conversation in ONE pv-chat (invite → Flow form → payment ask with the QR variant as a dashed call-out → 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.
Live
Monitor “is it working?” · sec-6a Activating · sec-6 Activated · sec-8 Pulse
Activating "Weekly wheel-throwing class"

About 20 seconds. Stay on this screen.

Checking your setup
Confirming billing & terms
Setting up your assistant
Running a live test~15s
Going live
Frame 6a · Activating loader [Monitor]
  • Story — As a founder, I watch real setup steps complete in order — not a fake spinner — so waiting feels like something is actually happening.
Acceptance
  • Covered by the master lifecycle contract's own criteria (assistant-lifecycle.html / the activation-wizard PRD), not restated here — this card contributes no new criterion for the generic loader.
Design notes
  • Same C2 pipeline as every assistant: wording ported verbatim from the master loader (assistant-lifecycle.html frame 7a) — no assistant-specific rewording, same 5 grouped steps.
Workshop Booking is live

It's capturing seats on your WhatsApp now. Watch bookings fill in Pulse.

WhatsApp connected Brain confirmed — 4 of 4 sections
Confirmations & reminders — pending Meta approval Two message templates are still waiting on Meta's one-time approval — usually a day or two. Bookings capture and your payment link relays right now; confirmations and reminders will start sending once approval comes through. If either is rejected instead, you'll get a clear fix step here — never a silent stall (see sec-6b below).
View in Pulse Done
Frame 6 · Activated / success [Monitor]
  • Story — As a founder, the moment activation finishes I know it's live, what's already true (WhatsApp connected, Brain confirmed), and exactly what's still pending — never a surprise later.
Acceptance
  • AC-WSBK-18 ↗ — when templates are pending Meta approval at activation success, the screen states that honestly and never claims confirmations/reminders already send; bookings and payment relay keep working underneath.
Design notes
  • Checklist carries only always-true facts (WhatsApp connected, Brain confirmed); the pending-Meta wait is its own amber notice, never folded into the green list. Headline hedges; "usually a day or two" stays in sub-copy (matches sec-4's banner).
Active Details →
Weekly wheel-throwing class ASST-4L9
Workshop Booking
1 connected · reminders pending Meta approval
Bookings
9 confirmed · 2 awaiting payment · $15.75 · 2h ago
Needs your attention
!
+1 ••• ••• 7742 — booked, not yet paid
Booked 44 min ago · payment ask sent, no reply yet
Nudge
!
2 templates pending Meta approval
Confirmations & reminders hold until approved
Details
Today
Booking confirmed — Priya M.+1 ••• ••• 2210 · paid $42 6:14 PM
Payment link relayed+1 ••• ••• 7742 · awaiting payment 5:30 PM
HeldClass reminder queued — pending template approvalWill send once Meta approves tonight 6 PM
Booking confirmed — Daniel R.+44 •• •• 9031 · paid $42 2:05 PM
Frame 8 · Pulse [Monitor]
  • Story — As a founder, I watch bookings and payments roll in and see exactly who still needs a nudge, without depending on push notifications that don't exist yet.
Acceptance
  • AC-WSBK-06 ↗ — a payment signal arriving after the session's start time is flagged for the founder to confirm/decline, never auto-confirmed — this is what an "attention" row like the unpaid booking represents.
  • AC-WSBK-07 ↗ — duplicate captures from the same phone don't create a second row, so the feed never double-counts one guest.
Design notes
  • Honest fallback while notifications aren't built: even though the push-notification toggles are hidden (sec-3 — no runtime reader exists), Pulse itself always shows unpaid/pending bookings the moment the founder opens it. Real detection, not the broken notify system standing in for it.
  • Card geometry, controls, and feed pattern reuse the master's fixed zone map (assistant-lifecycle.html) — same shell as every Pulse card, only the metrics line and feed content are Workshop-Booking-specific.
  • Meta line uses the platform's D-3 convention: amber lives on the meta line only, never the status badge — "Active" stays green because bookings and payment relay already work; only the outbound reminder send is waiting.

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).

MechanismIt happens in the same chat — Slack is only a mirror
shippedWhatsApp Coexistence · Meta, May 2025
   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. ──
Design notes
  • Since May 2025, WhatsApp Coexistence lets the founder's own number run the Business App and the Cloud API at once, mirroring in real time — the founder keeps her phone and replies by hand in the same thread the assistant was just in. No second app is needed.
  • Slack is a one-way mirror today: escalations and guest messages post into the founder's channel, and she cannot answer from there.
  • A founder whose number is Cloud-API-only (migrated before coexistence existed, or onboarded API-only) has no Business App to reply from — for that class of founder, “route to the founder” currently has no way for her to actually answer.
  • The holding line — “I'm connecting you with Priya” — should degrade to what's true for founders who have no reply path yet, rather than promise a human who can't currently speak.
Gap → placedThe founder can't set any of this — now a real Personalize category (05)
NOT BUILT · drawn in place as Personalize 05 (sec-3 / sec-3b)
Design notes
  • Category drawn in the real Personalize frames — sec-3b / sec-3, 05 · When I need to step in; full-size reference: Part II · c-stepin.
  • OBS second attempt is a PAID AI run that fires automatically today with no opt-in — the radio makes it a founder choice; default ON, cost stated per-question, never monthly.
  • Holding line is hard-coded today ("I'm connecting you with {founder} who can assist you further. They'll respond shortly!") — becomes an editable field.
  • Slack ping already wired, optional, ONE-WAY — presented as a choice for the first time; the WhatsApp-thread reply path is always on.

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 destination below fixes that.

  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 today (Attendees M1 adds client-side CSV)
Design notes
  • Bookings flow straight into MystFlo's own ledger — name, email, seats, amount, status. It does not reach the founder's own tools, and today she cannot get it out.
  • Sheets read into MystFlo today but never write back; Airtable, Notion, and a plain export don't exist yet.
  • Own-form: her form collects the booking off-platform, her sheet is the record; ticking a row paid is the billable event — it confirms the booking, fires the $1.75 charge, and triggers the same confirmation + 1d/1h reminders (AC-WSBK-11 ↗). No payment integration on this path.
  • A founder who picks own-form has a full attendee list — her own sheet. A founder who picks the default in-chat mode has no list until Attendees (M1) ships: Pulse shows unpaid bookings until they're paid, then nothing.
Approved 2026-07-28Attendees — a Pulse destination, not a tab (M1 build contract)

Every field is already stored on the booking, and all three verdict actions already ship in the booking EF — M1 is frontend-only. Attendees is a row inside the existing Pulse tab ("7 booked · 3 left ›", the same pattern as the "Waiting on you" flag) that opens a full-screen destination page — not a new tab. The post-tab set stays Pulse · Overview · Personalize · Connect. Naming is per-assistant: Workshop shows Attendees, Checkout shows Orders, Waitlist shows Signups — one component, one records contract. "Who's coming" survives as the section heading inside.

NOT BUILTM1 · zero migrationsread + verdict, never editPulse row → full-screen page, R30/R31 unamended
← Back to Pulse
Attendees
Desktop ≥1024 · 10 seats · 7 booked · 3 left · synced 2 min ago
NameContactSeatsBookedStatus
Maya Chenmaya@studio.co226 Julpaid
Tom Reyestom@reyes.co126 Julpaid
Aisha Koneaisha@studio.co3link sent 27 Julawaiting paymentMark paid
Devi Raodevi@rao.studio1roster tickpaid
Waitlist · 2
Sam Oyelaranphone onlyjoined 27 Julwaiting
Columns come from the per-assistant records contract — the same declaration that names the destination. The read-only columns match the mirror sheet 1:1, so in-app and her spreadsheet are the same picture. Read + verdict only — no cell is ever editable.
Story
  • As a founder on the morning of my workshop, I open the assistant and see exactly who is coming, who still owes me, and who's waiting for a spot — without opening a spreadsheet or scrolling WhatsApp.
Design notes
  • Lives on the assistant, never a global list — cross-assistant merged tables and phone/email identity joins are banned; deep-link entry required (workshop morning is repeat-entry).
  • Every status stays listed — unpaid is called out by dot + verdict, never by being the only rows shown; waitlist rows group beneath (a spot opens → "who's next" is human-answerable).
  • Read + verdict, never edit — contact fixes happen in the founder's own sheet, returning through the narrow read-back enum.
  • Export CSV is client-side from rows already loaded (no endpoint); the live sheet mirror is M2, opt-in at activation, never auto-created.
Approved 2026-07-28Attendees — the way in, and every state the populated frame doesn't draw

The frame above draws one populated table at one width. A founder meets six other screens before or instead of it: the Pulse row that opens it, the same rows on a phone, 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.

NOT BUILTrouted page · deep-linkableone component, noun is a proprow geometry identical in every state
13a · The way in — a row on the Pulse card, not a tab
Active Details →
Weekly wheel-throwing class ASST-4L9
Workshop Booking
1 connected · next class Sat 10:00
Bookings
9 confirmed · 2 awaiting payment · $15.75 · 2h ago
Attendeeswho's coming, who still owes you 7 booked · 3 left
Needs your attention
!
+1 ••• ••• 7742 — booked, not yet paid
Booked 44 min ago · payment ask sent, no reply yet
Nudge
375 · Pulse tab — the row sits under the card it belongs to, above the episodic attention list
Desktop ≥1024 · same row, two seat states
Attendeeswho's coming, who still owes you 7 booked · 3 left
Attendeeswho's coming, who still owes you sold out · 2 waiting
Never amber-tinted as a whole row — sold-out is a state, not a fault. Status is a dot; the row stays monochrome.
13b · Populated — the tiers below 1024 the table frame skips
‹ Pulse
Attendees
10 seats · 7 booked · 3 left
⤓ Export CSVsynced 2 min ago
Who's coming · 4
Maya Chenpaid
maya@studio.co · 2 seats · booked 26 Jul
Tom Reyespaid
tom@reyes.co · 1 seat · booked 26 Jul
Aisha Koneawaiting payment
aisha@studio.co · 3 seats · link sent 27 Jul
Mark paid
Waitlist · 2
Sam Oyelaranwaiting
phone only · — seats · joined 27 Jul
375 · stacked rows — every column survives, none is dropped
‹ Pulse
Attendees
10 seats · 7 booked · 3 left · synced 2 min ago
⤓ Export CSV768 · sidebar rail collapsed to 64px · content still stacked — the grid engages at 1024, not here
Who's coming · 4
Maya Chenmaya@studio.copaid
2 seats · booked 26 Jul
Aisha Koneaisha@studio.coawaiting paymentMark paid
3 seats · link sent 27 Jul
Tablet takes its SHELL from desktop (rail present, full header) and its CONTENT REFLOW from mobile (rows stay stacked) — the only thing extra width buys is the contact moving up onto line 1 and the verdicts moving inline.
13c · Empty — activation day, nobody has booked yet
‹ Pulse
Attendees
10 seats · 0 booked · 10 left
No one's booked yet

Your booking link is live. Share it and the first name lands here — usually within a day of posting.

Copy your booking link
375 · no Export button — there is nothing to export, so the control isn't drawn disabled, it isn't drawn
‹ Pulse
Attendees
Desktop ≥1024 · 10 seats · 0 booked · 10 left
No one's booked yet

Your booking link is live. Share it and the first name lands here — usually within a day of posting.

Copy your booking link
Column headers are NOT drawn over an empty body — a header row with nothing under it reads as a broken table, not as an empty one.
13d · Loading — the fetch in flight
‹ Pulse
Attendees
375 · four skeleton rows at the real row height — never a centred spinner, never a height jump on resolve
‹ Pulse
Attendees
NameContactSeatsBookedStatus
Column headers ARE drawn during load — they're structure the fetch can't change, and drawing them stops the header sliding in late. Screen readers get aria-busy="true" on the table; the shimmer is suppressed under prefers-reduced-motion.
13e · Error — the fetch failed
‹ Pulse
Attendees
10 seats
!
Couldn't load your attendees

Nothing's lost — this is a connection problem on our end. Your bookings are safe.

Try again Back to Pulse

Or ping us if it keeps happening.

375 · red is earned here — a fetch failure is a genuine failure, not a pending state
‹ Pulse
Attendees
Desktop ≥1024 · 10 seats
last loaded 6 min ago
Couldn't refresh — showing the list from 6 minutes agoRows below may be out of date. Try again
Maya Chenpaid
Tom Reyespaid
Two error shapes, one rule: cold failure (no rows ever loaded) takes the full-surface state on the left; warm failure (a refresh failed over rows already on screen) keeps the stale rows, dims them, and names their age — never blanks a list the founder is reading. Verdict buttons are removed from stale rows: acting on data we know is stale is how a seat gets double-released.
13f · The deep link, opened by the wrong person
Sign in to see your attendees

We'll bring you straight back to this list.

Sign in
375 · NOT signed in — <ProtectedRoute> redirect with return-to. Never a guest preview: a roster has no public shape.
‹ Pulse
Attendees
This list isn't here

It may have been deleted, or the link belongs to a different account.

Back to Pulse
375 · signed in, NOT the owner — identical to a deleted assistant. No count, no seat number, no name, no "permission" wording.
Story
  • As a founder, whatever state the list is in — empty, still loading, broken, or not mine — I always know what happened and what to do next, and I never see a blank screen with no way forward.
Acceptance
  • AC-WSBK-23 ↗ — the Pulse row ("Attendees · N booked · M left") and the deep link both open the same full-screen destination; the post-activation tab set stays Pulse · Overview · Personalize · Connect.
  • AC-WSBK-24 ↗ — paid rows stay listed; unpaid are called out by dot + verdict, never by being the only rows present.
  • AC-WSBK-25 ↗ — the only controls on a row are mark paid · extend · release; no state adds an editable cell, and the stale-rows error state removes verdicts rather than adding any.
  • AC-WSBK-26 ↗ — Export CSV renders only when rows are in memory, so the empty and cold-error states carry no export affordance at all.
  • AC-WSBK-27 ↗ — waitlist rows group beneath attendees in every tier; on 375 they keep the same stacked row shape, Seats reads "—".
  • AC-WSBK-28 ↗ — a paused assistant renders these states unchanged; pausing is not an error state.
  • AC-WSBK-31 ↗ — a non-owner sees the "This list isn't here" state, visually identical to a deleted assistant: zero rows, zero counts, no permission wording that would confirm the list exists.
  • NEW (PRD debt)Given the attendee fetch fails after rows have already rendered / When the founder is looking at the list / Then the stale rows stay on screen, dimmed and labelled with their age, with verdict controls removed — the list is never blanked and a stale row is never actionable.
  • NEW (PRD debt)Given a signed-out visitor opens the Attendees deep link / When the route resolves / Then they are sent to sign-in with a return-to back to this list — the surface has no guest or preview mode.
Design notes
  • Routed page, not a slide-over. Own URL (/pulse/:assistantId/attendees) because AC-WSBK-23 requires the deep link and workshop morning is repeat-entry — a slide-over can't be bookmarked, shared to a second device, or reopened after a refresh. Trade-off accepted: the founder loses the Pulse context behind the surface, bought back by a persistent "‹ Pulse" affordance.
  • Structure steps at lg 1024 only (6-column grid; below it the same fields stack, nothing dropped); row geometry identical across all four data states (56px min mobile / 40px grid) so the surface never jumps height on resolve. Fluid per U8; radius/hairlines constant.
  • Status is a dot, the row stays monochrome — emerald paid (C12), amber awaiting/sold-out, white/35 waiting. Red appears only on a genuine load failure, never on an unpaid booking.
  • One component, three nouns. Attendees / Orders / Signups is a prop plus a per-assistant column contract; every state above is authored once and reused. The empty-state copy is the only per-assistant string.
What the waitlist actually stores
  • ✓ A real row IS written — name, phone, joined-at, subscribed status.
  • ✗ Email is collected and validated, then written email: null bug — discarding the one channel not rate-limited by WhatsApp's 24h window.
  • ✗ Rows are tagged as campaign overflow — a different assistant's concept reporting.
  • ✗ Seat cap exists ONLY on the Flow-form path overbooking — typed capture (what every instance runs until Meta approves its Flow) has no seat check; a 10-seat workshop can confirm 15.
Fallback “what happens when Meta says no?” · sec-6b template rejected
!
WhatsApp turned down your class reminder

Bookings and payment relay keep working. Only the reminder is blocked until the wording is fixed.

What they flagged "Too promotional for a utility message." WhatsApp is strict about wording on automated reminders sent outside the customer's active reply window.
Rewrite and resubmit
Use our approved wording

Resubmitting goes back into review — same one-to-two-day window.

Frame 6b · Template rejected [Fallback]
  • Story — As a founder, if Meta rejects a template I get the exact reason and two ways to fix it — while bookings and payment relay keep running underneath.
Acceptance
  • AC-WSBK-19 ↗ — a rejected template shows a named, actionable fix path (rewrite & resubmit, or use MystFlo's approved wording), never a dead end; other capabilities (capture, payment relay) stay unaffected.
Design notes
  • A real state Workshop Booking can hit, unlike On-Brand Support: On-Brand Support has no WhatsApp template dependency and correctly draws no rejected-template state. Workshop Booking depends on Meta approving two templates (confirmation + reminder), so rejection is a real outcome — design-canon's D-3 rule requires red-status failure states to always show a named fix path, never a dead end. Modeled directly on the master's own rejected-template twin (assistant-lifecycle.html frame 7e), adapted to this assistant's two templates.
  • Deliberately NOT drawn here: the generic live-test rollback (B6 — WhatsApp connection failure, unrelated to template wording) already covered once by the master contract. This frame is the template-specific twin only.
768 not yet drawn for this flow. Desktop and 375 are the contract.
This step is 1180. No 375 drawing — use the Desktop size.
Guest
This step is 375. No 1180 drawing — use the Mobile size.
768 not yet drawn for this flow. Desktop and 375 are the contract.
Guest journey — Trigger · Interact · Outcome what the guest sees on WhatsApp · sec-7, forked on booking_surface

Tapping the Pulse card (sec-8) — its Details → control — opens the full detail sheet: AssistantDetailModalPost on AssistantModalShell, the visual TWIN of the pre-activation sheet (same shell, same tab family, same language — shell-language #7; twin reference: retreat-booking.html sec-6c). Tabs are PLAIN NAV, not a stepper: Pulse · Overview · Personalize · Connect, Pulse first and default, no Try tab (a live assistant's real runs are the proof — R30). This is the frame Peyton asked to see and didn't have — sec-8 only ever drew the small Pulse-page card tile, never what opening it actually shows. Four states below, one instance throughout (Weekly wheel-throwing class · ASST-4L9 — same nickname as sec-5's “Name this one” and sec-8's card).

Weekly wheel-throwing class ASST-4L9
Workshop Booking · Active since Jul 21
Pulse Overview Personalize Connect
This workshop
StatusActive
Bookings9 confirmed · 2 awaiting payment · $15.75 · 2h ago
Your booking link
https://wa.me/15551234567?text=Hi! I'd like to book a seat for Wheel Throwing Class Copy link

Same link as Personalize — share it anywhere without digging back into setup. Opens the in-chat booking form.

Needs your attention
!
+1 ••• ••• 7742 — booked, not yet paid
Booked 44 min ago · payment ask sent, no reply yet
Nudge
!
2 templates pending Meta approval
Confirmations & reminders hold until approved
Details
Recent activity
Booking confirmed — Priya M.+1 ••• ••• 2210 · paid $42 6:14 PM
Payment link relayed+1 ••• ••• 7742 · awaiting payment 5:30 PM
HeldClass reminder queued — pending template approvalWill send once Meta approves tonight 6 PM
Booking confirmed — Daniel R.+44 •• •• 9031 · paid $42 2:05 PM
Weekly wheel-throwing class ASST-4L9
Workshop Booking · Active since Jul 21
Pulse Overview Personalize Connect
What it does

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.

How it works
01
You
Teach it once

payment link, policy, voice — from your Brain

02
Customer
DMs to book a seat

picks a session, gives headcount

03
Assistant
Captures + relays
AItakes the booking, sends your payment ask
free RSVP mode -> skips the payment ask, straight to confirms + reminds
04
You
Marks it paid

one tap, or your sheet ticks it

05
Assistant

Confirms + reminds

instant confirm, then 1 day + 1 hour before

Word-for-word the pre-activation Overview (sec-2 — R28, one product description, pre and post): same "What it does", same five HowItWorksFlow nodes. No "Key benefits" persuasion block here (matches the retreat-booking twin's precedent) and no "Your setup" block — that lives on Personalize (R29).

Weekly wheel-throwing class ASST-4L9
Workshop Booking · Active since Jul 21
Pulse Overview Personalize Connect
Your setup
Workshop date2026-07-28
Price / seats$42 · 10 total
PaymentFull prepay · in this chat
Booking linkwa.me link — same as Pulse
Messagesconfirmation + reminder — locked, pending Meta
Reminder channelsWhatsApp
Changes take effect on the next run — nothing already sent gets rewritten.
Weekly wheel-throwing class ASST-4L9
Workshop Booking · Active since Jul 21
Pulse Overview Personalize Connect
Connect apps
Communication WhatsApp Business
Matches your Reminder channels from Personalize — WhatsApp always required for booking chat
Connected
AI Model MystFlo AI
Connected by default. Choosing your own AI model is coming later.
Connected
Storage MystFlo Storage
Your Attendees list on Pulse — every booking lands there, captured in chat or read from your roster. Nothing external to connect.
Connected
Wallet
Wallet balanceEach confirmed booking draws $1.75 from your prepaid balance$86.40
Brain check
Business name
Confirmed
Edit in Brain →
Cancellation policy
Confirmed
Edit in Brain →
4 of 4 ready — it can answer guests
Frame 8b · Post detail sheet — four tab states [Monitor]
  • Story — Weeks after activating, I tap into the live assistant and land on what matters NOW (Pulse) — bookings, the attention row, my booking link — with everything I set one tab away, and it feels like the same room I set it up in.
  • Gap this frame closes: sec-8 only ever drew the small Pulse-page card tile plus a bare "Today" activity panel — never what tapping Details → on that card actually opens. This frame is that missing sheet.
  • Twins rule (shell-language #7): same shell (AssistantModalShell), same tab family, same language as the pre-activation sheet (sec-2/3b/4/5) — plain nav here vs stepper there is the ONE sanctioned difference (T1147). Modeled directly on retreat-booking.html sec-6c, the sibling Program Booking Engine assistant's already-shipped version of this exact frame.
  • R30: tabs are Pulse · Overview · Personalize · Connect — Pulse FIRST and selected on open; NO Try tab, no "Activity" label.
  • R28: Overview copy is word-for-word sec-2's Overview, including the same seeded details.flow five-node HowItWorksFlow — Key benefits dropped (persuasion copy, not needed once already active; matches the retreat twin's precedent).
  • R29: "Your setup" read-back lives on Personalize, never Overview — values are sec-5's exact read-back row set (Workshop date · Price/seats · Payment · Messages · Brain · Wallet), plus the booking-link row, presented as a read-only box with edit pencils rather than the live form fields from sec-3b.
  • Pulse tab's "Your booking link" card and "Needs your attention"/"Recent activity" rows are verbatim from sec-8 — same copy, same link, same rows — this sheet is the destination sec-8's card opens into, not a new source of truth for any of it.
  • Connect tab condenses sec-4's full two-column 1180-width layout into the panel-width Connect apps + Wallet + Brain check rows (same pattern retreat-booking's twin uses) — sec-4 remains the canonical full detail for every row/copy/caret rule; nothing here contradicts it.
  • Mobile tier — explicitly scoped out, matching the sibling precedent: retreat-booking.html sec-6c is desktop-only (it sits in Part I ahead of that file's own § Mobile journey section, with no 375 twin drawn). This frame matches that scope decision rather than inventing a new mobile-tier obligation the sibling doesn't carry — and sec-8's own frame-note already establishes that its ~375-width Pulse card "stands in for both tiers," so the Pulse tab's content is already covered at phone width one frame up.

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.

Booking capture — before/after the copy-mismatch fix [Trigger → Interact, in_chat]
Current — retreat-flavored (wrong)
Proposed — class-flavored (fixed)
Sub-frame · Booking capture
  • Story — As a guest, I ask about a class in the founder's own voice and get an answer about MY class — not a retreat cohort I never signed up for.
Acceptance
  • AC-WSBK-01 ↗ — the capture step (name, session pick, headcount) is what creates the pending_payment booking row this reply represents.
Design notes
  • Confirmed, not hypothetical: today's actual AI prompt is worded for retreats, not workshops/classes: it says "our retreat," asks for "name, email, how many seats," and treats the guest as "joining a retreat cohort." Verified against the live prompt — this is real, shipped, user-visible copy damage, not a guess.
  • Email stays in the fixed bubble too — it isn't a stylistic choice. The real capture gate (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.
Payment relay — before/after #2796 [Interact, in_chat]
Current — broken (#2796)
Proposed — fixed
Sub-frame · Payment relay
  • Story — As a guest, once I say I'm in, I get the founder's real payment link right away — not a hand-off promise that goes nowhere.
Acceptance
  • AC-WSBK-01 ↗ — "the assistant relays the host's payment ask" is this criterion's own language; #2796 was this exact promise silently failing in the in-chat runtime.
Design notes
  • Root cause of #2796: 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.

Secondary — the WhatsApp Flow seat-booking form [Interact, alternate rendering]
Sub-frame · Secondary WhatsApp Flow form
  • Story — As a guest, when the founder's Flow is published, I book through a native in-chat form instead of typing my details one message at a time.
Design notes
  • When the Flow is published, guests get this native in-chat form instead of typed capture; when not published, capture falls back cleanly to plain chat.
  • The intro line is a Personalize field (default: "Tap below to grab your spot for {{workshop_name}}:"), editable, same mechanism as Payment ask — not the runtime's hardcoded string. Approved 2026-07-28.
  • What triggers this form is not literal keyword matching — it's the AI's own classification of the message's intent, so free-form text ("any spots left tuesday?") triggers it exactly like a link-tap's pre-filled "book" does. The conditional "Tell assistants apart" keywords field (Personalize 03a) is a separate, earlier-stage mechanism: it only disambiguates which assistant an inbound message routes to when a founder runs more than one WhatsApp assistant on the same number.
own_form branch — cited, not drawn [Interact]
  • AC-WSBK-14 ↗ — once booking_surface='own_form', in-chat capture never fires; the assistant relays the booking-form link and sets intent FORM_RELAY instead.
  • AC-WSBK-11 ↗ — a roster-sourced booking (no chat thread ever existed for that guest) is still eligible for the 1d/1h reminder exactly like a chat-captured booking.
  • AC-WSBK-15 ↗ — booking_form_url is required the moment own_form is picked, enforced on all three save paths (activation, update-config, reactivation).
  • No WhatsApp bubble existed for what the guest actually sees in this mode when this frame was written — the founder's booking-form link relay. That gap is closed by sec-9 · Mode C, which draws the own_form thread end to end, and by the Personalize preview's own second state (sec-3/3b, round-4): a dimmed phone under the main preview draws the relay as the whole guest experience, keyed to the “Your own booking form” radio.

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.

First: this is a 2×2, not a list of five

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 E — guest DMs and the AI already reads booking intent. What's missing is ───┐
  │ the CHOICE: offering a list of open workshops to pick from. NOT BUILT —          │
  │ two real blockers, drawn in Part III · Future.                                     │
  └─────────────────────────────────────────────────────────────────────────────────┘
meta template locked, Meta-approved — founder reviews, never edits founder field a real Personalize input, sent verbatim ai-composed written live in the founder's voice from the Brain native form WhatsApp's own UI, not text
Mode AFull payment, booked in this chat

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.

payment_mode = full_prepay booking_surface = in_chat shipped
Story
  • As a guest, I ask about a class, fill a short form, pay on the founder's own link, and get a confirmation and two reminders — all in one thread, without ever being handed off.
Acceptance
  • AC-WSBK-01 ↗ — capture creates the pending_payment row and the assistant relays the founder's payment ask.
  • AC-WSBK-02 ↗ — confirmation within 30s of payment being learned. AC-WSBK-03 ↗ — reminder fires at both offsets.
Design notes
  • Message sources, 1→7: 1 guest (intent-classified — wa.me only pre-fills "book") · 2 invite = copy_flow_intro founder field over WhatsApp's native form (per-founder Flow from flow-json/workshop-booking.ts; unpublished → typed capture, nothing waits) · 4 payment ask = founder field sent verbatim (Brain policies.payment wins when set) · 5 confirmation = workshop_booking_form_confirmation template, ≤30s after Mark paid · 6/7 = ONE class_reminder body, both offsets.
  • Capture gate: name + email (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.
  • Four kinds of words in one thread (runtime string · founder field · Meta template · live AI) — the source-chip vocabulary is proposed to carry INTO Personalize so "why can't I edit this one?" answers itself.
  • The only unbounded gap is 3 → 4 → Mark paid — nothing chases an unanswered payment link; see Part III · F2. Not drawn as solved because it isn't.
Mode BFree RSVP, booked in this chat

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.

payment_mode = free_rsvp booking_surface = in_chat shipped
Story
  • As a guest at a free session, I say I'm coming, fill in three fields, and I'm confirmed on the spot — nobody asks me for money and nobody makes me wait.
Acceptance
  • AC-WSBK-05 ↗ — a free_rsvp workshop skips the payment-ask leg entirely and goes straight to confirmed.
Design notes
  • Delta from Mode A: trigger/form/submit identical (form softens to "Save your place", price/date field hidden — same Flow JSON, one conditional field); payment ask AND Mark paid legs removed — captureCohortBooking calls learnPaid on submit, same meter, same exactly-once; no "awaiting payment" state ever appears in Pulse.
  • Proposed proposed: the "Payment ask" field hides when Free RSVP is picked — an editable sentence that never sends is the dead-control problem this rescope removed five toggles for.
  • Confirmation/reminders unchanged — reminders key off cohorts.start_date, not payment. Free RSVP is the one mode needing no nudge machinery (no unpaid state to chase).
Mode CThe founder's own booking form

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.

payment_mode = full_prepay or free_rsvp booking_surface = own_form booking_form_url · required Google Sheets roster · required shipped
Story
  • As a founder who already has a booking form I trust, I keep it — the assistant sends people to it, and once I tick them off as paid it takes the confirming and reminding off my plate.
Acceptance
  • AC-WSBK-14 ↗ — in-chat capture never fires; the assistant relays the link and sets FORM_RELAY. AC-WSBK-15 ↗ — booking_form_url required on all three save paths.
  • AC-WSBK-11 ↗ — roster-sourced bookings get the 1d/1h reminders like any other.
Design notes
  • Message sources: same entry/intent classification as every mode; bubble 2's link = 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.
  • The blind stretch is the honest limit: booking + payment happen on the founder's form — no webhook, no way to tell bookmarked from paid; the roster tick (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.
Every modeWhen the guest asks something else

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.

shippedchat-handler.ts · fall-through gate
  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
Design notes
  • The ladder, A→E: answerable → answered in the founder's voice, thread continues · unanswerable → On-Brand Support gets ONE attempt, fact-checked before it can reach the guest · refunds / date moves / "get me a human" → straight to the founder, honest holding line, human mode (the assistant never tells a paid guest their booking is cancelled) · "I already paid!" → acknowledged, never believed — confirmed only when payment is learned · sold out → hard-coded waitlist line; seats re-counted at submit.
  • Nothing here needs building — it needs showing — the ladder is live and invisible; a Try-step / Explore-copy opportunity, not engineering.
  • The real gap: the holding line and the sold-out line are hard-coded — the two messages a founder most wants in her own words, and neither appears in Personalize (holding line now proposed as Personalize 05).
Master contract + sources
  • 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.
  • Sources: 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.

Part II — Components the parts library at full size — reference only, nothing here is the ONLY place a thing appears (kit lock 2026-07-30).
C1 · Locked, Meta-approved templates ↑ top in the flow ↑
Meta-approved · lockedBooking confirmation
Header: "You're booked — Wheel Throwing Class"
Hi Maya, it's Priya — your seat for the Wheel Throwing class is confirmed. We'll send a reminder before class. Reply here with any questions.
Meta-approved · lockedClass reminder — one body, both offsets
Hi Maya, it's Priya — just a warm reminder about your upcoming session. Your spot's all set and I'm looking forward to seeing you. If something's come up and you can't make it, reply here so I can offer the place to someone else. See you soon!
C1 · Locked templates seated: Personalize 04a (sec-3/3b) · every sec-9 thread
  • Story — As a guest, once I've paid I get a real confirmation, then a warm reminder before class — sent in the founder's own name.
Acceptance
  • AC-WSBK-02 ↗ — confirmation within 30s of payment learned (Mark paid or roster tick). AC-WSBK-03 ↗ — reminder fires at both offsets.
Design notes
  • Read-only previews, never editable — bodies are the approved workshop-templates.ts copy with example vars resolved; nothing invented.
  • Both deliberately link-free UTILITY templates (Peyton 2026-07-10); the reminder is time/location-agnostic, so ONE preview honestly covers both sends.
C2 · “When I need to step in” — the 05 category at full size not built ↑ top in the flow ↑
05 · When I need to step in proposed
If the assistant can't answer
Let On-Brand Support try first
Uses your full knowledge base. Costs one extra AI run per question — only when it's actually used.
Come straight to me
No second attempt, no extra cost.

Only shown when you have On-Brand Support running on this number.

What guests hear while they wait — editable
I'm connecting you with Priya — she'll come back to you shortly.
Where you get pinged
WhatsApp — this thread
Always on. Reply from your phone as usual.
Connected
Slack
A copy in your channel. On the app? You reply in WhatsApp. API-only number? Reply right in the thread — it reaches your guest.
Connect
C2 · Step-in category seated: Personalize 05 (sec-3/3b) · why: sec-12
  • Escalation radio pair renders only with On-Brand Support on the number (conditional block in the flow frames); holding line + ping rows always render.
Part III — Future design deferred — never a build target. Everything below is drawn so the shape can be judged, NOT so it can be built from. Nothing here is wired today; each item names what actually blocks it.
F1 · Mode E — the workshop picker (formerly 10) not built ↑ top

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. Half of this already works: the assistant reads booking intent from plain English today (sec-9, Mode A design notes) — a guest saying "I'd love to learn pottery" already reaches the booking flow when Workshop Booking is the assistant on that number. What is missing is the choice: the assistant knows about exactly one workshop, so it cannot offer a list.

Mode EGuest just DMs — assistant offers what's open
any payment_mode booking_surface = in_chat NOT BUILT — 2 blockers
What it would take — two named blockers
Reading intent without a keyword ai-composed already works
The assistant classifies the message's intent, so "do you run classes?" already lands in the booking flow. The one caveat: on multi-assistant numbers, the keyword router can send a keyword-free message to On-Brand Support instead, which can answer but cannot book.
1
Knowing what's open ai-composed blocker 1
A live "what's coming up" block — real dates, prices, remaining seats — is built and shipping for On-Brand Support, but the booking assistant never receives it.
2
One workshop per assistant structural blocker 2
An activated Workshop Booking assistant is bound to a single workshop — one date, one price, one seat count, seeded at activation. A picker needs one assistant that holds several workshops, which a single-workshop assistant can't do.
·
The picker widget itself native list
Not a blocker. Up to three options use WhatsApp's reply buttons, already built; more than three needs an interactive list message.
Direction — the picker is On-Brand Support's job, not this card's
  • When a founder runs several workshops, On-Brand Support — the assistant that holds the whole business — answers "what do you run?", presents the list, and routes the guest into the right booking flow. Workshop Booking stays one workshop, booked well.
  • "Two workshops = two activated assistants" (sec-3) stays true and untouched — each workshop keeps its own assistant, date, seat count, and billing. On-Brand Support already holds the business knowledge and the live dates-and-seats block that Workshop Booking is missing in blocker 1.
  • This makes On-Brand Support a recommended companion for any founder running more than one offering, and a real second charge (see sec-12).
  • Still worth doing on Workshop Booking regardless: letting it see its own live seat count, so "is Tuesday full?" is answered from data instead of guessed.
F2 · When the guest goes quiet (formerly 11) not built ↑ top

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.

The constraint that shapes every option below Meta policy, not a preference
  • Inside 24 hours of the guest's last message, the assistant can write a nudge freely, in the founder's own voice.
  • After 24 hours, only a pre-approved template can be sent — which means every distinct nudge below is a separate submission to Meta, with its own review wait and its own chance of rejection (sec-6b). Each nudge has a real cost, so the set should be small.
  • There is a working precedent to copy rather than invent: On-Brand Support already sends exactly one gentle nudge to a guest who goes quiet for 24h, capped at one per quiet stretch, and backs off automatically when the founder's number is near its sending limit.
CasesFour silences, and what each one deserves
NOT BUILT — proposed shape only
F1
Form sent, opened, never submitted Mode A · B
Today: nothing. Proposed — to the guest, once, at 24h: "Hi Maya, it's Priya — your spot for Tuesday's class isn't saved yet. The form's still open if you'd like it." To the founder: nothing individually — this is normal browsing, not a problem. Roll it into Pulse as a count, not an alert.
F2
Booked, payment link sent, never paid Mode A
Today: nothing — the seat sits held forever and the founder is never told; the chase machinery exists but is switched off for full-payment workshops by design. Proposed — to the guest, once, at 24h: "Hi Maya, it's Priya — your spot for Tuesday's class is still held. Here's the link again if you'd like to lock it in: …" To the founder: a Pulse attention row — a held seat is a seat not sold, and the founder is the only one who can release it.
F3
Own-form link sent, nothing came back Mode C
Today: nothing — and this one is different in kind. MystFlo cannot see the founder's form, so it cannot tell "never opened it" from "booked and paid but the founder hasn't ticked the roster yet." Nudging the guest is genuinely risky here — chasing someone who already paid is worse than staying silent. Proposed — say nothing to the guest, and tell the founder instead: "6 people got your booking link this week. 2 are on your roster." That's a true statement MystFlo can make, and it puts the judgement with the only person who has the missing half.
F4
Waitlisted, a spot opens all modes
Today: the guest is told they're on the waitlist, and then nothing ever happens — there is no sweep that notices a cancellation and tells them. The assistant makes a promise it cannot keep ("you'll be the first to know if a spot opens").
Where this lands in Personalize
  • One new category — "06 · Chasing" — with two toggles: nudge unpaid bookings (on by default), nudge unfinished forms (off by default), each with editable copy. This replaces the three dead "notify me" toggles removed elsewhere in this rescope.
  • No nudge toggle for Mode C — the assistant cannot tell paid from unpaid there, so Mode C gets the founder-facing count (F3) instead.
Design notes
  • The smallest honest version is F2 alone — one nudge, one template, one Pulse row, aimed at the only silence that costs the founder money. F1 is polish, F3 is a reporting line rather than a message, and F4 is a bug fix; they don't need to ship together.
  • Every nudge is one per quiet stretch, capped, never stacked with a reminder on the same day — matching the precedent On-Brand Support already runs.
F3 · The waitlist — what we actually capture (formerly 14) not built ↑ top

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.

1
Sold out at submit — Mode A/B form fix in code, not a form
The guest already filled the booking form — name, email, party size — before the seat check ran. A waitlist row IS written, but with 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.
2
Sold out at send — guest just DMs, no form ever sent this is where a form earns its place
The workshop is already full when the guest asks, so no form is ever sent. A waitlist row is written — but it carries only the phone number, the WhatsApp display name if one is exposed, and 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.
3
Typed capture — every instance until Meta approves its Flow overbooks, doesn't waitlist
No seat check exists on this path at all (sec-13 — “the seat cap only exists on ONE path”) — sold-out is never detected, so it overbooks instead of waitlisting. A form is irrelevant here until the seat check ships; that's the real fix, and it's already in the parallel lane.
ProposedA native waitlist form — path 2 only
NOT BUILT same Flow mechanism as booking · new template/Flow

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.

Where each message comes from
1
Guest asks guest
Same intent classification as every other mode (sec-9) — no keyword needed.
2
Sold-out reply ai-composed
Already shipped, hard-coded, verbatim (sec-9, mode-q row E). Not editable today — same as now.
Waitlist form native form proposed
A second Flow, published the same way as the booking form (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.
3
Guest submits guest
Writes the waitlist row with a real name and email this time — the same row that exists today, minus the email: null bug (sec-13).
4
Join confirmation ai-composed proposed
Written live, inside the 24h window the guest just opened by messaging — no template needed for this turn.
Story
  • As a guest who DMs about a sold-out class, I get told I'm on the waitlist and I'm asked, in the same breath, for my name and email — one tap, not a typed back-and-forth.
Design call — why no party size
  • Left out, deliberately. The booking form (sec-9) asks for party size because it's committing a seat right now. A waitlist join isn't committing anything — the number that matters is whatever's true when a spot actually opens, which could be weeks later, so a party size collected today is stale by then. When the founder promotes someone off the waitlist she's messaging them directly anyway (sec-13's attendee-list proposal already shows waitlisted rows for exactly this) — that's the moment to ask, not now. Two fields, not three.
The honest limit
  • Better data collected here does not mean anyone gets told. Without the notify-sweep proposed as F4 in sec-11, a cleanly-filled waitlist is still a list nobody is ever messaged from. This form is the prerequisite for F4, not a substitute for it — email is the one channel that isn't rate-limited by WhatsApp's 24-hour window, so capturing it is what makes F4 reachable for this path.
Where it lands in Personalize
  • No new Personalize control. The sold-out reply is already not editable today; this form is the same class of message — structural, fired automatically, nothing for the founder to configure.
The cost
  • A second per-founder Flow means a second Meta review-and-publish cycle on top of the booking Flow's, with the same fallback-to-typed-capture safety net and the same named recovery path if Meta says no (sec-6b).
F4 · Stripe — auto-mark paid when their payment lands (formerly 15) not built ↑ top

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.

Overview Personalize Connect Try
01 · Connect apps
Communication WhatsApp Business
Which number should this assistant answer on?
+1 555-0142 ▾
Payment Stripe
Auto-mark paid when their payment lands. Nothing confirms until payment is real — MystFlo never touches the money.
Future · #3152
AI Model MystFlo AI
Connected by default. Choosing your own AI model is coming later.
Connected
Storage MystFlo Storage
Your Attendees list on Pulse — every booking lands there, captured in chat or read from your roster. Nothing external to connect.
Connected
Storage — when “Your own booking form” is picked (03b) conditional

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.

Storage Google Sheets
Your booking roster — MystFlo reads it one way; the tick in your paid column is what confirms a booking
Connected

Wallet · Brain Check · template notice unchanged from sec-4 — omitted here, this frame exists for the one new row.

Frame F4 · Stripe auto-settle [Future]
  • Story — As a founder whose guests pay through my Stripe link, a landed payment marks the booking paid on its own — I stop being the human webhook.
Design notes
  • ADR-019 phase-2 door: a Stripe webhook becomes a THIRD door into the same 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.
  • Copy is locked ("Nothing confirms until payment is real — MystFlo never touches the money") — the row detects the founder's own Stripe payment landing; MystFlo still processes no money (ADR-019's payment-relay decision is unchanged).
  • Canon U9: this row never appears in the Active Connect frame (sec-4) until #3152 ships — this duplicated frame is the only place it's drawn.

The recurring-schedule question (sec-3) is resolved, not deferred — single-session ships now; the lightweight repeating-session increment lives in #2798, tracked separately.