This is a real flow shipped in production by a WaniWani customer — a pet insurance distributor selling policies through their own MCP. It’s the kind of build we open-sourced WaniWani for: a multi-step quote conversation that has to feel natural, validate input strictly, branch on edge cases (mixed-breed dogs), and let the user correct themselves without restarting. We’re showing it here, simplified and anonymized, because every pattern in it is one we hit repeatedly across customer projects — and the shape of the answer is whatDocumentation Index
Fetch the complete documentation index at: https://docs.waniwani.ai/llms.txt
Use this file to discover all available pages before exploring further.
createFlow was designed around.
API calls use a fictional
petApi and the pricing logic is anonymized. The graph shape, validation patterns, widget hand-off, and correction loop are exactly as deployed.Flow graph
Patterns demonstrated
Open-ended questions
One natural question extracts multiple state fields at once.
Conversational follow-ups
Asks remaining questions 1-2 at a time with warm, contextual reactions.
Rich system prompt
Compliance rules and tone guidance embedded in the tool description.
Conditional branching
Different paths based on animal type and breed composition.
Async validation
Server-side breed and location validation between questions and confirmation.
Widget display
Confirmation card and pricing cards rendered via display tools.
Correction loop
User fixes details → re-validate → re-display until confirmed.
Pricing adjustment loop
User tweaks deductible/copay → recalculate → show updated prices.
Full flow
Register the widget display tools
showWidget("show_pet_summary", ...) and showWidget("show_pricing", ...) in the flow refer to widget names the model will call to render each card. Register them with Skybridge’s registerWidget — it binds the named widget to the matching React component under web/src/widgets/ and handles all of the OpenAI/Claude render plumbing for you:
server/src/app.ts
web/src/widgets/show_pet_summary.tsx (and show_pricing.tsx) and mounts via mountWidget from skybridge/web. The widget name passed to registerWidget matches the file name — that’s how Skybridge wires the binding, so you never deal with _meta.openai/outputTemplate URIs by hand.
Skybridge ships in the MCP distribution template as the default dev/build runtime. If you’re starting from that template (
bun dev runs skybridge dev), registerWidget is already available on the server you instantiate. See the Skybridge docs for the full widget mounting API.Key patterns explained
Why the system prompt lives in description
Why the system prompt lives in description
The
description field of a flow becomes the MCP tool’s description. It’s the first thing the model reads when deciding how to use the tool. By embedding compliance rules and tone guidance here, you ensure every single interaction follows your guidelines without relying on external system prompts you don’t control.This is especially powerful for regulated industries (insurance, finance, healthcare) where the AI must never give recommendations or advice.How open-ended extraction works
How open-ended extraction works
The
ask_about_pet node asks one broad question but declares petName as its interrupt field. The context instructs the model to extract all recognizable fields from the user’s free-form response into stateUpdates.This creates the feeling of a natural conversation: the user says “I have a 2-year-old Golden Retriever named Max, he’s neutered” and the flow captures 5+ fields in one turn. The next node then only asks what’s still missing.The correction loop pattern
The correction loop pattern
After
show_confirmation displays the summary widget:- If the user says “looks good” →
confirmed = true→ proceed to pricing - If the user says “actually his name is Max, not Mac” →
confirmed = false+ correction in stateUpdates →fix_detailsresets confirmed →validate_detailsre-runs →show_confirmationre-displays with corrected data
The pricing adjustment loop
The pricing adjustment loop
After showing pricing plans, the user might say “can I see prices with a lower deductible?” The model sets the new deductible value and
pricingAction: "adjust". The recalculate_pricing node resets pricingAction to undefined, which loops back to show_pricing, which calls the API with the new parameters and re-renders the widget.This pattern works for any “configure → preview → adjust” cycle.How flows call widgets (the showWidget handoff)
How flows call widgets (the showWidget handoff)
Flows don’t render widgets directly. Instead, This separation means the flow controls when and with what data a widget appears, while the LLM controls the conversational framing around it.
showWidget creates a special interrupt that instructs the LLM to call a separate display tool. Here’s the sequence:- The flow node calls
showWidget("show_pricing", { description, data })and pauses. - The flow engine returns an interrupt to the LLM with the
descriptionyou provided. - The LLM reads that description (which says “You MUST now call the
show_pricingtool”), writes a short intro message, then calls the display tool. - The display tool renders the widget in the chat UI.
- After the user interacts with the widget, the LLM continues the flow with updated state.
description is the key bridge. It’s your instruction to the LLM about what tool to call and how to frame it conversationally. This is why it says things like “Write a brief intro, then immediately call show_pet_summary” rather than just describing what the widget does.The field parameter tells the flow which state field the widget interaction resolves (e.g., confirmed). The data is passed directly to the widget component for rendering.Validate callbacks for real-time enrichment
Validate callbacks for real-time enrichment
The This runs server-side when the model provides a breed name. If the breed doesn’t exist in the database, the validation throws an error with suggestions and the flow automatically re-asks the user. If it succeeds, it can return state updates (like
breed interrupt field in ask_remaining_details has a validate callback:breedId) that enrich the state without an extra round-trip.