One Workflow. Three Channels. Real Booking Infrastructure. β
Experimental source tour
The Boulevard package passes its current TypeScript and test suites. It remains experimental because CI uses mock Boulevard/provider boundaries rather than live production services.
An experimental Boulevard booking architecture where Web UI, AI Chat, and Gemini Live Voice target the same workflow.
The channels interpret interaction. The runtime owns execution.
text
Source code: /invariant/examples/boulevard-bookingπ‘ The Core Idea β
A customer can book the same salon appointment through a web UI, chat conversationally with an agent, or speak naturally via browser-direct Gemini Live audio.
The channel changes. The booking workflow does not.
text
Web UI (Calendar / Dropdowns) ββββββ
β
AI Chat Agent (Text Intent) ββββββββΌβββΊ Runtime Boundary βββΊ Booking Workflow βββΊ Boulevard API
β
Gemini Live (Direct Speech) ββββββββEach channel only translates user interaction into proposed input for the current workflow boundary.
No channel owns booking state, decides which step comes next, or bypasses workflow validation.
Different channels. Same execution truth.
π Follow One Booking: "Selecting an Appointment Date" β
To understand why this architecture is so powerful, let's trace a single step through all three channels.
The master workflow is currently suspended at a .wait() boundary:
ts
.capability('get-bookable-dates', getBookableDatesCapability)
.wait<{ selectedDate: string }>('select-date-ui', {
schema: SelectDateSchema,
presentation: {
type: 'date',
label: 'Appointment Date',
prompt: 'Which date works best for your appointment?',
},
})Here is how each channel interacts with that exact same boundary:
1. Web UI (Deterministic Component) β
The widget receives the SessionSnapshot via SSE and renders a native calendar picker showing bookable dates:
text
Customer clicks "August 20, 2026"
β
βΌ
POST /api/sessions/:id/input { "selectedDate": "2026-08-20" }2. Direct Voice (Gemini Live Audio) β
The Voice Channel Projector transforms the boundary into natural spoken context, while the Agent (boulevardBookingAgent) defines the exact dynamic tool:
spokenPrompt: "Which date works best for your appointment?"tools:submit_input({ selectedDate: string })
text
Customer speaks: "I'd like to come in this Thursday, August 20th"
β
βΌ
Gemini Live calls: submit_input({ "selectedDate": "2026-08-20" })
β
βΌ
POST /api/live/session/:id/execute βββΊ agent.handleAction()3. Conversational AI Chat (app.agent()) β
The multi-turn chat agent receives the user message:
text
Customer types: "Thursday works great for me"
β
βΌ
Agent resolves intent β calls submit_input({ "selectedDate": "2026-08-20" })The Convergence: Schema vs. Fresh-State Validation β
All three channels submit their proposed input to the Runtime Boundary. Invariant differentiates between structural validity and execution validity:
text
Gemini proposes: { selectedDate: "2026-09-99" }
β
βΌ
Schema Validation
β
Malformed date
β
βΌ
REJECT
(Workflow unchanged)
Gemini proposes: { selectedDate: "2026-08-23" }
β
βΌ
Schema Validation
β
β
βΌ
Fresh-State / Domain Check
β
Date no longer bookable
β
βΌ
REJECT
(Workflow unchanged)A proposal can be structurally valid and still be invalid for the current execution state. In both cases, rejection means no workflow transition is committed.
Model mistakes do not become execution history. The workflow remains safely waiting at select-date-ui until valid input is submitted.
Reasoning may be probabilistic. Execution progress is not.
ποΈ Who Owns What? β
Invariant establishes a strict separation of concerns across 5 distinct architectural layers:
Workflow = Business logic.
Capabilities = External I/O.
Projectors = Channel representation.
Routes = Transport & delivery.
Runtime = Execution guarantees.
This separation makes channels replaceable. Gemini Live can be replaced, the web widget can be redesigned, or a new MCP interface can be added without redefining the booking workflow. Channels change how users interact with the processβnot what the process means.
π Complete System Architecture β
Notice how Projection ("What should this channel see?") is strictly separated from Validation ("May this proposed action become execution?"):
πΊοΈ The Map & The Zoom: 8-Phase Master Workflow β
The master workflow (examples/boulevard-booking/src/workflows/booking.ts in this repository) serves as The Map of the entire business. Complex sub-domains live in isolated fragments (The Zoom):
ts
export const boulevardBookingWorkflow = app.workflow<BookingWorkflowInput>('boulevard-booking', {
description: 'Durable Boulevard salon appointment booking workflow',
version: '3.0.0',
})
// ---------------------------------------------------------------------------
// PHASE 1 β LOCATION & CART
// Resolve booking location, then initialize Boulevard cart.
// ---------------------------------------------------------------------------
.branch('check-location-provided', ({ input }) => (input.locationId ? 'PROVIDED' : 'PROMPT'), {
PROVIDED: app.fragment('use-loc').step('set-loc', ({ input }) => ({ locationId: input.locationId ?? 'loc_soho' })),
PROMPT: app.fragment('ask-loc').capability('list-locs', listLocationsCapability).wait('select-loc-ui', {
schema: SelectLocationSchema,
presentation: { type: 'selection', label: 'Salon Location', prompt: 'Which studio would you like to visit?' },
}),
})
.capability('init-cart', createCartCapability)
// ---------------------------------------------------------------------------
// PHASE 2 β SERVICE SELECTION (Guided Haircut Tree vs Catalog)
// ---------------------------------------------------------------------------
.step('filter-service-categories', ({ state }) => ({
availableCategories: state.cart.availableCategories.filter((c: ServiceCategory) => c.categoryType === 'SERVICE'),
}))
.wait('select-category-ui', {
schema: SelectCategorySchema,
presentation: { type: 'selection', label: 'Service Category', prompt: 'Which category of service are you looking for?' },
})
.branch<{ serviceSelection: ServiceSelection }>(
'service-selection-path',
({ state }) => (state.categoryName === 'Grooming' ? 'GUIDED_GROOMING' : 'STANDARD_SERVICE'),
{
GUIDED_GROOMING: haircutGuidedFragment, // The Zoom: length -> shampoo -> custom recommendation
STANDARD_SERVICE: standardServiceFragment, // The Zoom: direct category picker
}
)
// ---------------------------------------------------------------------------
// PHASE 3 β STAFF SPECIALIST
// ---------------------------------------------------------------------------
.wait('select-staff-ui', {
schema: SelectStaffSchema,
presentation: { type: 'selection', label: 'Select Stylist', prompt: 'Do you have a preferred stylist?' },
})
.capability('add-service-to-cart', addServiceToCartCapability)
// ---------------------------------------------------------------------------
// PHASE 4 β ADD-ONS & UPSELL
// ---------------------------------------------------------------------------
.capability('refresh-cart-for-addons', getCartCapability)
.wait('select-addons-ui', {
schema: SelectAddonsSchema,
presentation: { type: 'selection', label: 'Enhance Your Visit', prompt: 'Would you like to add an Olaplex mask?' },
})
.capability('add-addons-to-cart', addAddonsToCartCapability)
// ---------------------------------------------------------------------------
// PHASE 5 β CLIENT CONTACT & IDENTITY
// ---------------------------------------------------------------------------
.wait('client-contact-ui', {
schema: ClientContactSchema,
presentation: { type: 'form', label: 'Contact Information', prompt: 'Please enter your contact details.' },
})
.branch('client-identity-path', ({ input }) => (input.userUid ? 'AUTHENTICATED' : 'GUEST'), {
AUTHENTICATED: authenticatedClientFragment,
GUEST: guestClientFragment,
})
// ---------------------------------------------------------------------------
// PHASE 6 β SCHEDULING & TIME HOLD
// ---------------------------------------------------------------------------
.capability('get-bookable-dates', getBookableDatesCapability)
.wait('select-date-ui', {
schema: SelectDateSchema,
presentation: { type: 'date', label: 'Appointment Date', prompt: 'Which day works best for you?' },
})
.capability('get-available-times', getAvailableTimesCapability)
.wait('select-time-ui', {
schema: SelectTimeSlotSchema,
presentation: { type: 'time', label: 'Appointment Time', prompt: 'What time would you prefer?' },
})
.capability('reserve-time-slot', reserveTimeSlotCapability)
// ---------------------------------------------------------------------------
// PHASE 7 β SUMMARY REVIEW & PAYMENT AUTHORIZATION
// ---------------------------------------------------------------------------
.capability('refresh-cart-summary', getCartCapability)
.wait('review-booking-summary-ui', {
schema: SummaryConfirmSchema,
presentation: { type: 'confirmation', label: 'Review Booking Summary', prompt: 'Please confirm your appointment details.' },
})
.branch('payment-requirement-path', ({ state }) => (state.cart.summary.paymentMethodRequired ? 'REQUIRED' : 'NOT_REQUIRED'), {
REQUIRED: paymentFlowFragment,
NOT_REQUIRED: app.fragment('skip-payment').step('acknowledge-no-payment', () => ({ paymentApplied: false })),
})
// ---------------------------------------------------------------------------
// PHASE 8 β TRANSACTIONAL OUTBOX CHECKOUT
// Stable operation identity allows checkout retries to reuse
// the same idempotency key across recovery attempts.
// ---------------------------------------------------------------------------
.capability('checkout-appointment', {
idempotencyKey: "blvd-checkout:{{runId}}",
handler: checkoutAppointmentCapability,
});π‘ Connecting External Clients β
1. Interactive UI (ai-agents/chat-widget) β
bash
# Streams live state_change SSE snapshots
curl -N http://localhost:3000/api/sessions/sess_100/stream2. Direct Gemini Live Audio β
bash
# 1. Fetch ephemeral Gemini Live session token
curl -X POST http://localhost:3000/api/live/session/sess_100/token
# 2. Fetch active voice projection & dynamic tool definition
curl http://localhost:3000/api/live/session/sess_100/context3. Conversational AI Chat Agent β
bash
curl -X POST http://localhost:3000/conversations/session/sess_100/messages \
-H "Content-Type: application/json" \
-d '{"message": "I want to book a signature haircut in Soho this Thursday with Elena"}'π§ͺ Comprehensive Conformance Tests β
Run the full end-to-end test suite:
bash
pnpm --filter example-boulevard-booking testtext
Test Files 7 passed (7)
Tests 28 passed (28)
β test/voice-projector.test.ts # Bounded action space & natural speech
β test/appointment-management.test.ts # Appointment cancel & reschedule flows
β test/workflow-conformance.test.ts # 8-phase booking execution & outbox
β test/projection-stream-sse.test.ts # SSE streamCursor & Last-Event-ID replay
β test/compatibility-routes.test.ts # ai-agents/chat-widget 1-to-1 delegation
β test/app.test.ts # Fastify integration & session snapshots
β test/live-voice.test.ts # Gemini Live token & fresh-state execution