← Design HQ · HTML on GitHub · PRD — prd-as-workshop-booking.md
Workshop Booking — Discover, set it up, go live, then the guest thread.
The Workshop Booking flow, every state, in journey order — the build contract; ACs cited by id, never restated. Companion: assistant-lifecycle.html (master) · on-brand-support.html (token/component source). This file is the canonical structural reference for per-assistant flow mockups (canon §“Assistant-flow mockup structure”).
8 sections: overview → scope → content → journey → desktop → mobile → components → future Discover → Evaluate → Activate → Monitor (founder) Trigger → Interact (forks on booking_surface) → Outcome → Fallback (guest) amber = pending/action-needed · emerald = fixed/healthy/resolved Flow / Components / Future (canon 2026-08-01)Widths here sample a fluid continuum — content
scales continuously; structure steps only at sm 640 · md 768 · lg 1024. Kit:
design-canon.md U8.
Workshop Booking (TMPL-WSBK01) turns a workshop host's WhatsApp DMs into confirmed, paid seats. A guest taps the founder's booking link — or just texts — and the assistant captures the booking right in the chat (name, session, headcount), relays the founder's own payment link or QR exactly as she wrote it, and once payment is confirmed — her one-tap Mark paid, or a tick in her own roster sheet — sends the Meta-approved confirmation and reminds the guest 1 day + 1 hour before class. $1.75 per confirmed booking; MystFlo relays payment asks but never touches the money. Bookings live in MystFlo's own ledger and surface on Pulse (metrics, attention rows, the Attendees list); when the assistant can't answer, the founder takes over in the same WhatsApp thread — no second app.
full_prepay · free_rsvp) × two booking surfaces (in_chat — WhatsApp Flow form fronting typed capture · own_form — founder's form link relayed, read-only roster sheet).deposit_balance radio removal (#3192) · Connect two-role rows — StorageRoleRow/AiModelPicker (#3198, PR #3209) · MystFlo Storage provider (#3178, closed).details.benefits/details.flow have drifted (sec-2 PARTIAL); the build PR ships the migration + PRD together (check-catalog-copy.sh).
┌ CONFIG (Personalize) ────────────┐ ┌ BRAIN ──────────────┐ ┌ CATALOG COPY (DB row) ────────┐
│ name · start_date* · price · │ │ voice · policies · │ │ description · tldr · │
│ seats · payment_mode · │ │ contact sign-off │ │ benefits[4] · details.flow │
│ booking_surface · link/keywords ·│ └──────────┬──────────┘ │ (5 nodes) — sec-1/sec-2 only │
│ copy fields · booking_form_url │ │ └───────────────────────────────┘
└────────────────┬─────────────────┘ │ grounds answers
reads ▼ ▼
guest WhatsApp thread — capture (Flow form / typed / own-form relay)
│ name · contact · seats
▼
cohort_bookings row ◄─── one-way roster read (own_form: her paid tick = billable event)
│ booked → paid → confirmed
├─► locked templates: confirmation (<30s) + reminder (1d + 1h)
└─► Pulse metrics · attention rows · Attendees columns · activity feed
start_date required — gates activation, AC-WSBK-16 ↗) · payment_mode · booking_surface · generated wa.me link + conditional keywords · founder-editable copy (booking invite, payment ask) · booking_form_url required on all save paths under own_form (AC-WSBK-15 ↗).workshop-templates.ts, Meta-approved, never editable (C1). Every sec-9 ledger tags each message's source with one vocabulary: template founder field ai-composed native.cohort_bookings): name · contact · seats · status · paid-source (Mark paid / roster tick). Read + verdict, never edit; the read-back enum is closed in code (_shared/flow-patterns.ts), and the Attendees columns come from the same per-assistant records contract (sec-13).own_form only): the founder's own columns, mapped by header name (_shared/cohort-roster.ts) — read one way, never written; a missing required column pauses reminders with a named-column message, never a silent failure.Founder vocabulary (Discover → Evaluate →
Activate → Monitor) and guest vocabulary (Trigger → Interact → Outcome → Fallback) are the same pair
every MystFlo assistant's Scope Council uses — reproduced here, not invented for this file. The one real
branch worth drawing is at Interact, where booking_surface splits the guest experience in two;
everywhere else is one direction.
┌ DISCOVER ────────────────────────────────────────────────────────┐
│ Founder browses Explore — sees the card, its price, and the │
│ one required app (WhatsApp Business). │
└──────────────────────────┬─────────────────────────────────────────┘
▼
┌ EVALUATE ──────────────────────────────────────────────────────────┐
│ Opens the Overview tab — what it does, how it works, key benefits. │
└──────────────────────────┬─────────────────────────────────────────┘
▼
┌ ACTIVATE ──────────────────────────────────────────────────────────────┐
│ Personalize (workshop date/price/seats · payment + booking-surface │
│ mode · locked message copy) → Connect (WA number · conditional Sheets │
│ pick · template-approval notice) → Try (read-back) │
└──────────────────────────┬─────────────────────────────────────────────┘
▼
┌ MONITOR — founder, ongoing ─────────────────────────────────────────────┐
│ Activating (loader) → Activated (live, honest about pending templates) │
│ → Pulse: bookings roll in, attention rows, live feed │
└──────────────────────────┬─────────────────────────────────────────────┘
▼ once live, per guest DM
▼ the guest side runs across every scenario below
One waterfall could only draw one path. This reads left-to-right along the same six beats every guest passes through, with one lane per scenario — so the differences between modes, and the places where a lane simply stops, are visible in one look. Amber lanes are not built. Full detail per lane: sec-9.
Column 6 is the honest column. Four of 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.
start_date gates Try and activation — no real date, no activation (AC-WSBK-16 ↗, closing the silent-failure gap where reminders never scheduled and no one was told).booking_surface='own_form' gates two things at once: Connect's roster-sheet pick becomes required, and Personalize's booking_form_url becomes required on all three save paths — activation, update-config, and reactivation (AC-WSBK-15 ↗).wa.me link can't open a Flow form directly — it only pre-fills the guest's “book” message. The form is always sent BY the assistant, as an interactive flow message, once intent is caught. Link-tap and direct-DM converge on the identical mechanism.AC-* ↗) live in prd-as-workshop-booking.md §Test Plan.Workshop Booking captures the seat straight from your WhatsApp DMs — name, session, headcount — then relays your own payment link or bank details exactly as you wrote them. Once you mark it paid (or your roster shows it), the guest gets an instant confirmation and a reminder before class. You decide every payment; MystFlo never touches the money.
payment link, policy, voice — from your Brain
picks a session, gives headcount
one tap, or your sheet ticks it
Confirms + reminds
instant confirm, then 1 day + 1 hour before
long_description; ships via migration + PRD in the same PR (guard-railed, check-catalog-copy.sh) — the build PR must carry it.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).Retreat-style deposits live on Retreat Booking, not here.
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.
The AI reads intent — “any spots left tuesday?” books a seat just like “book” does. Nothing to configure.
Only if more than one assistant shares this number — these tell your assistants apart. Nothing to do with reading intent; most founders never see this field.
However they arrive — link tap or a direct message — it's one path in. What they see next is your call:
Shows the form once WhatsApp approves it; guests text back and forth until then — nothing to set up on your end.
Shown when “QR code” is picked. No QR yet → a dashed drop box (upload once, it stays); Remove reverts to it.
Same words in both modes — the phone preview shows the link message and the QR variant in sequence. Only the delivery vehicle changes.
Which channels the class reminder can use — connect the app for each in Connect. WhatsApp on by default; the booking flow itself always runs in chat regardless.
Only shown when you have On-Brand Support running on this number.
Works the same whether they tapped your wa.me link or typed this directly — the AI reads intent, not just the word.
In own_form mode the Storage row above is withheld entirely — no row renders, not even grayed (shipped: internalStorageWithheldForOwnForm, ConnectStep.tsx, #3178 CTO/Design review + #3184). The roster lives in her own Sheet, so “MystFlo Storage · Connected” would be a false claim in that mode. The own-sheet requirement stays backend-enforced (CONDITIONAL_REQUIRED_STORAGE, four paths) with no Connect-tab pick UI yet — separately tracked; per canon U9 that future roster-pick row is not drawn in this Active frame (its contract lives only in the F4 duplicated frame below).
.panel — no true 1180-width realization existed. Re-drawn here using the exact wrapper this file already established for a real desktop frame (sec-3b's .flowgrid > .flowstep > .pageframe > .desktop-scroll > .desktop, reduced-zoom chrome) plus the master's two-column split (apps+wallet left flex-1, Brain Check right 400px). Same rows/copy as before (including this session's Storage-row and caret fixes), nothing invented. The "Continue to Try" button is omitted from this crop.stripe_meter_id and a stripe_price_id exist on this template's row; the Wallet row above surfaces the same wallet that guard protects.own_form requires booking_form_url; the roster-sheet requirement itself is backend-enforced (CONDITIONAL_REQUIRED_STORAGE) — its Connect-side face is not yet built, so this frame documents only the withheld Storage row (cond-block above); the future pick contract is drawn in F4.ROLE_IDS, required-roles.ts): “AI Model” → MystFlo AI · Connected (AiModelPicker.tsx shape) and “Storage” → MystFlo Storage · Connected — one Storage role, drawn ONLY in the default in-chat mode; under own_form it disappears entirely (shipped internalStorageWithheldForOwnForm, #3178 CTO/Design review — “Connected” would be a false claim when the roster lives in her own Sheet). MystFlo Storage is a real, shipped provider value (#3178, closed — STORAGE_PROVIDER_KEYS includes mystflo_storage, status pill via the shared ConnectionStatusButton primitive, never hand-rolled text); what remains not built is the Attendees destination it points at (sec-13, M1).design-decisions.md 2026-08-05, caret scope amended 2026-08-07, Peyton-locked; code #3198/PR #3209): label = the role (“Storage”), badge = the connected app carrying the ▾/▴, and tapping it expands the picker group in place as a hairline list. The DestinationPicker expansion (Create a new one — default, Template-first 2026-05-22 / Link a sheet I already have, with the how-this-works tip living under the Link option only) belongs to the FUTURE own_form roster-pick row — no longer drawn in this Active frame (the shipped code withholds the Storage role entirely under own_form; see the cond-block note above); its contract row lives in F4 only, per canon U9. Deviation from the master's generic three-group default, flagged not silently resolved: the master's 2026-08-07 ruling defines the Storage picker as three groups (MystFlo Storage / Google Sheet / Your Supabase) everywhere, but that future row's real gate is provider-specific (CONDITIONAL_REQUIRED_STORAGE names google_sheets only for own_form) — drawing a Supabase group there would promise a destination the backend validator would reject. This mockup keeps the two-group contract (Google Sheet only) rather than fabricate a third group the code doesn't support; reconciling this file's provider-specific gate with the master's generic default is an open follow-up, not decided in this pass. Caret rule, corrected: the default-mode MystFlo Storage row now carries the caret too (fixed in this pass — it was previously missing even the badge/chip wrapper). AI Model remains the one exception — no caret, no coming-soon row, deliberate product gate (BYOK_AI_HIDDEN). Master contract: assistant-lifecycle.html B9/frame 5b.+1 555-0142 ▾ control — a real, already-functioning picker for a different sub-choice (which number) than the label-badge caret's job (which channel/provider). It now lives on the WhatsApp pick-row rather than the outer conn-row, since the outer badge's caret has a real second group (Email) to open.confirmed, fires the $1.75 charge), so the read can never be optional. When the own_form Connect-side pick ships (separately tracked), it adopts the shipped DestinationPicker (T472, “Template-first, never blank” 2026-05-22): Create a new one (default — the MystFlo template roster cloned into her Drive at activation, zero structure concerns) vs Link a sheet I already have — the ONLY path where column mapping applies; the how-this-works tip (no data until read · header-name mapping per _shared/cohort-roster.ts · missing column pauses honestly) lives under that option, never as a blanket. Until then the requirement is backend-only — an own_form founder meets it at the activation gate, not on this tab.Runs on your WhatsApp from the moment you activate. Pause anytime.
assistant-lifecycle.html frame 6) covers this once for every assistant type instead of restating it here. The read-back's "Messages" row already flags the pending Meta approval, so activating isn't a surprise when the success screen repeats it (sec-6).Works the same whether they tapped your wa.me link or typed this directly — the AI reads intent, not just the word.
Retreat-style deposits live on Retreat Booking, not here.
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.
The AI reads intent — “any spots left tuesday?” books a seat just like “book” does. Nothing to configure.
Only if more than one assistant shares this number — these tell your assistants apart. Nothing to do with reading intent; most founders never see this field.
However they arrive — link tap or a direct message — it's one path in. What they see next is your call:
Shows the form once WhatsApp approves it; guests text back and forth until then — nothing to set up on your end.
One preview, not two — the 1-day and 1-hour sends use the identical body.
Shown when “QR code” is picked. No QR yet → a dashed drop box (upload once, it stays); Remove reverts to it.
Same words in both modes — the phone preview shows the link message and the QR variant in sequence. Only the delivery vehicle changes.
Only shown when you have On-Brand Support running on this number.
start_date gates Try and activation.free_rsvp skips the payment-ask leg entirely, straight to confirmed.payment_mode and booking_surface render as real radio controls the founder can select.wa.me link drops straight into the booking flow.cond-blocks; 05’s holding line + ping rows always render. Dead controls omitted outright: five no-runtime-reader toggles, deposit_balance (a Retreat Booking concept).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).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.About 20 seconds. Stay on this screen.
assistant-lifecycle.html / the activation-wizard PRD), not restated here — this card contributes no new criterion for the generic loader.assistant-lifecycle.html frame 7a) — no assistant-specific rewording, same 5 grouped steps.It's capturing seats on your WhatsApp now. Watch bookings fill in Pulse.
assistant-lifecycle.html) — same shell as every Pulse card, only the metrics line and feed content are Workshop-Booking-specific.Two things sit behind “route to the founder”: how the handover physically works (settled — it happens in the same WhatsApp thread, no second app needed) and what the founder can decide about it (nothing today — and one of those decisions costs her money, so it can't stay silent).
ONE number — the founder's own. Two things talk on it at once.
┌───────────────────────────────┐ ┌──────────────────────────────┐
│ WhatsApp Business APP │◄──────►│ Cloud API (MystFlo) │
│ on Priya's phone │ mirror │ the assistants │
│ she types here, by hand │ in │ they answer here │
└───────────────┬───────────────┘ real └───────────────┬──────────────┘
│ time │
└────────────────────┬────────────────────┘
▼
┌──────────────────┐
│ the guest's │ one thread.
│ WhatsApp chat │ one number.
└──────────────────┘ no handoff seam.
escalation fires ──► assistant goes SILENT (human mode)
├──► guest told "I'm connecting you with Priya"
├──► Priya opens WhatsApp and replies herself
└──► Slack (optional): a copy lands in her channel
── one-way. She CANNOT reply from Slack. ──
Every booking lands in MystFlo's own ledger. Nothing flows outward — not to Sheets, not to Airtable, not to Notion, not to a file. And the founder's only view of that ledger shows bookings that are still unpaid — the moment one is paid, it leaves her screen; the Attendees 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)
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.
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.
Your booking link is live. Share it and the first name lands here — usually within a day of posting.
Your booking link is live. Share it and the first name lands here — usually within a day of posting.
aria-busy="true" on the table; the shimmer is suppressed under prefers-reduced-motion.Nothing's lost — this is a connection problem on our end. Your bookings are safe.
Or ping us if it keeps happening.
We'll bring you straight back to this list.
<ProtectedRoute> redirect with return-to. Never a guest preview: a roster has no public shape.It may have been deleted, or the link belongs to a different account.
/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.email: null bug — discarding the one channel not rate-limited by WhatsApp's 24h window.Bookings and payment relay keep working. Only the reminder is blocked until the wording is fixed.
Resubmitting goes back into review — same one-to-two-day window.
assistant-lifecycle.html frame 7e), adapted to this assistant's two templates.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).
Same link as Personalize — share it anywhere without digging back into setup. Opens the in-chat booking form.
Workshop Booking captures the seat straight from your WhatsApp DMs — name, session, headcount — then relays your own payment link or bank details exactly as you wrote them. Once you mark it paid (or your roster shows it), the guest gets an instant confirmation and a reminder before class. You decide every payment; MystFlo never touches the money.
payment link, policy, voice — from your Brain
picks a session, gives headcount
one tap, or your sheet ticks it
Confirms + reminds
instant confirm, then 1 day + 1 hour before
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).
Details → on that card actually opens. This frame is that missing sheet.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.details.flow five-node HowItWorksFlow — Key benefits dropped (persuasion copy, not needed once already active; matches the retreat twin's 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.
pending_payment booking row this reply represents.shouldCaptureCohortBooking, shared by this booking engine) requires name AND email before a booking is persisted or the payment ask is shared; only the retreat-vs-class wording changes — the three-field capture contract itself is unchanged and correct.copy_payment_ask is bridged to cohorts.payment_instruction only at activation, and only the WA-Flows path reads it at runtime — the default in-chat path never does, so the AI falls back to a generic hand-off line instead of relaying the real link. The proposed fix makes the in-chat runtime read the same field, closing the gap.The two locked, Meta-approved template bodies (Booking confirmation · Class reminder) live at full size in Part II · Components — seated in the flow at Personalize 04a (sec-3/3b) and in every sec-9 thread.
booking_surface='own_form', in-chat capture never fires; the assistant relays the booking-form link and sets intent FORM_RELAY instead.booking_form_url is required the moment own_form is picked, enforced on all three save paths (activation, update-config, reactivation).Section 7 shows individual messages being fixed. This section shows the whole thread, unbroken, for every mode a founder can pick — and, beside each thread, exactly where every message's words come from and where the founder meets them in Personalize and the live preview. One card per mode; nothing here is invented — every bubble traces to a named template, config field, or the real runtime string, and anything not built lives in Part III · Future, never here.
Payment structure and where guests book are two independent settings, not one menu. A founder picks one of each — so the five things that feel like five options are really four shipped combinations plus one unbuilt entry path. Drawing them as one list would imply choices that don't exist (and hide ones that do).
where guests book ──► in_chat (default) own_form
payment structure ┌──────────────────┐ ┌──────────────────┐
│ │ MODE A │ │ │
├── full_prepay (default) ────────│ shipped │ │ MODE C │
│ └──────────────────┘ │ shipped │
│ ┌──────────────────┐ │ (payment shape │
├── free_rsvp ─────────────────────│ MODE B │ │ changes only │
│ │ shipped │ │ one word) │
│ └──────────────────┘ └──────────────────┘
│
└── deposit_balance ── hidden on this card — a retreat concept, lives on Retreat Booking
┌ MODE 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. │
└─────────────────────────────────────────────────────────────────────────────────┘
The default, and the one most founders will run. The guest never leaves WhatsApp; the money does — payment happens on the founder's own link, because WhatsApp's in-form payment is India/Singapore only.
pending_payment row and the assistant relays the founder's payment ask.wa.me only pre-fills "book") · 2 invite = copy_flow_intro founder field 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.shouldCaptureCohortBooking) — no row without both; seats re-checked at submit, not send. Mark paid (or a roster tick) is the ONLY door to confirmed — the assistant never takes the guest's word.Same thread as Mode A with the money taken out — and taking the money out removes two steps, not one. There is no payment ask, and no Mark paid: the booking confirms itself the moment the form comes back.
free_rsvp workshop skips the payment-ask leg entirely and goes straight to confirmed.captureCohortBooking calls learnPaid on submit, same meter, same exactly-once; no "awaiting payment" state ever appears in Pulse.cohorts.start_date, not payment. Free RSVP is the one mode needing no nudge machinery (no unpaid state to chase).The assistant hands over the founder's own form link and steps aside — no capture, no payment ask, no in-chat form. Everything that happens next happens somewhere MystFlo cannot see, and comes back only through the roster sheet.
FORM_RELAY. AC-WSBK-15 ↗ — booking_form_url required on all three save paths.booking_form_url, its sentence = copy_booking_form_link (or AI-written around the real link when blank); confirmation + reminders = the same Meta templates, with the confirmation landing hours later because it waits on the founder.booking-roster-sync) is the ONLY door a booking enters through, manual by design — and it is the billable event (confirms the booking, fires the $1.75 charge). Reminders are the part that stays automatic.full_prepay vs free_rsvp only shapes wording here ("pick your spot and pay there" → "pick your spot there") — MystFlo never asks for or learns payment independently in this mode.This is already built, and the answer to "route to the owner or swap in the on-brand assistant?" is both, in that order — the handover is automatic and the founder never configures it.
guest asks something off the booking script
│
▼
┌ can Workshop Booking answer it? ┐
│ from the workshop details or │──── yes ──► answers, thread continues
│ the Brain │
└────────────┬────────────────────┘
│ no (intent = UNSURE)
▼
┌ is On-Brand Support live on ────┐
│ this same WhatsApp number? │──── no ───┐
└────────────┬────────────────────┘ │
│ yes │
▼ │
┌ Support takes one more shot with ┐ │
│ the full Brain + FAQ, and the │── fails ─┤
│ answer is fact-checked before │ │
│ it is allowed out │ │
└────────────┬─────────────────────┘ │
│ passes ▼
▼ ┌ founder gets the thread ─────┐
guest gets a real │ holding line to the guest · │
answer — no handoff │ human mode on · host pinged │
└──────────────────────────────┘
"get me a human" / refund / date move ─────────────────────────► straight to founder,
no Support attempt
assistant-flows/assistant-lifecycle.html is the primary structural contract; assistant-flows/on-brand-support.html is the sibling exemplar this file borrows its token block and component classes from verbatim.prd-as-workshop-booking.md §Test Plan (AC-WSBK-* / AC-BILL-*) · DB migration 20260710080000_2012_b4_wsbk01_card.sql · ConfigureStep.tsx isBookingFamily gate · tests/e2e/assistants/tmpl-wsbk01.spec.ts.Business name ("Ember Clay Studio"), guest names, and phone numbers are illustrative. This file draws the proposed target.
workshop-templates.ts copy with example vars resolved; nothing invented.Only shown when you have On-Brand Support running on this number.
The guest hasn't tapped a link and hasn't said "book" — they just message the business, and want to choose between what's on offer. 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.
Today, none of this exists. Every mode above ends its automatic behaviour the moment the assistant sends the form or the payment link. If the guest reads it and does nothing, nothing happens — no nudge to them, no signal to the founder, ever. The three toggles that sound like they'd cover this ("notify me on unpaid timeout" and friends) are dead controls with no runtime reader, which is why this rescope removed them from Personalize rather than leave them lying.
Whether the guest should see a WhatsApp Flow waitlist form depends on which of three sold-out paths they hit — they behave nothing alike, and only one of them is missing anything a form could fix. Paths 1 and 3 below are covered in sec-13 and are fixed in code, not redesigned here — this section is scoped to path 2 only.
email: null, discarding data validated two
lines earlier (sec-13). No second form
belongs here. Asking someone to fill another form immediately after “sorry, sold out”
is friction with nothing to show for it — the fix is the one-line email bug, not new UI.email: null, because nothing was ever collected. This is the
only path with no name and no email to lose — they were never asked for. That's what a form
would add here, and why it belongs on this path and no other. Proposed form below.Same shape as the booking Flow (sec-9, Mode A) — a native in-chat form instead of typed capture — fired the moment the sold-out reply goes out, so joining the waitlist costs the guest one tap, not a typed back-and-forth.
provisionWorkshopFlow, sec-9) — not a reuse of the booking Flow, since it asks
for less and fires on a different trigger. Two fields only: name + email.email: null bug (sec-13).The Connect tab (sec-4) duplicated with ONE tweak: a deferred Stripe row. CPO-ruled post-beta (#3152, ADR-019's phase-2 door) — per canon U9 it must NOT appear in the Active Connect frame, so it lives only here.
FUTURE own_form roster-pick row (separately tracked — the shipped Connect tab withholds the Storage row entirely under own_form and has no roster-pick UI yet; see sec-4's cond-block note). Per canon U9 it is drawn only here, never in the Active frame — collapsed; this frame otherwise exists for the Stripe row.
Wallet · Brain Check · template notice unchanged from sec-4 — omitted here, this frame exists for the one new row.
learnPaid settle path (host Mark-paid · roster tick · Stripe webhook), all metering identically — never a second billing path. Deferred post-beta by CPO ruling; tracked as #3152.The recurring-schedule question (sec-3) is resolved, not deferred — single-session ships now; the lightweight repeating-session increment lives in #2798, tracked separately.