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({ tool: "show_pet_summary", ... }) and showWidget({ tool: "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({ tool: "show_pricing", data })and pauses. - The flow engine returns an interrupt to the LLM with a standardized instruction to call the named display tool.
- The LLM 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.
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. Any per-widget AI guidance (conversational framing, compliance notes, etc.) belongs in the display tool’s own response, not in showWidget.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.