-
Notifications
You must be signed in to change notification settings - Fork 0
widget contract
The schema-driven contract between playbook outputs and SPA UI.
A widget is a JSON schema plus a React component. The SPA
dispatches incoming widget envelopes by type and renders the
matching component.
Source-of-truth files:
- Schemas:
playbooks/widget-contract/*.schema.json - Generated TS types:
src/contracts/widgets.ts - Components:
src/components/widgets/*.tsx - Smoke harness:
scripts/widget_dispatch_smoke.mjs
Every widget the SPA renders arrives wrapped in the envelope
defined by
_envelope.schema.json.
Conceptually:
{
"widget_id": "uuid",
"type": "hotel_card | flight_list | place_list | order_confirmation | ...",
"version": 1,
"data": { /* widget-specific shape, validated by <type>.schema.json */ },
"actions": [ { /* optional, user-actionable affordances */ } ],
"rendered_at": "ISO8601"
}The envelope is created inside a playbook step (typically
render_widget_chat in
itinerary-planner.yaml)
and then persisted to Firestore as an append_widget_event
record. The SPA receives it either:
- as part of an executePlaybook GraphQL response (immediate results), or
- through a
subscription/eventSSE frame (chat_threads/<thread_id>/events/*collection updates).
| Widget type | Component | Used for |
|---|---|---|
action_chooser |
ActionChooser.tsx |
Present a small set of next-actions to the user. |
bot_text / user_text
|
BotText.tsx / UserText.tsx
|
Plain conversational text bubbles. |
calendar_view |
CalendarView.tsx |
Live calendar of trip events, fed by subscribeToCalendarEvents. |
clarify_question |
ClarifyQuestion.tsx |
Ask the user a structured follow-up. |
date_range_picker |
DateRangePicker.tsx |
Date-range input for trips. |
error_card |
ErrorCard.tsx |
Human-readable failure summary. |
filter_panel |
FilterPanel.tsx |
Faceted filters over a result list. |
flight_card / flight_list
|
FlightCard.tsx / FlightList.tsx
|
Flight offer cards (Duffel/Amadeus). |
hotel_card / hotel_list
|
HotelCard.tsx / HotelList.tsx
|
Hotel cards (Amadeus). |
hotel_compare |
HotelCompare.tsx |
Side-by-side comparison. |
itinerary_summary |
ItinerarySummary.tsx |
Final trip-plan summary. |
loading_card |
LoadingCard.tsx |
"Working on it" state. |
map_view |
MapView.tsx |
Google Maps embed for places. |
notification |
Notification.tsx |
Inline alert (info/warn/error). |
order_confirmation |
OrderConfirmation.tsx |
Post-booking confirmation with PDF link. |
party_picker |
PartyPicker.tsx |
Number of travelers / ages. |
place_autocomplete_input |
PlaceAutocompleteInput.tsx |
Google Places autocomplete control. |
place_card / place_list
|
PlaceCard.tsx / PlaceList.tsx
|
Point-of-interest cards. |
property_block |
PropertyBlock.tsx |
Property detail block in chat. |
typing_indicator |
TypingIndicator.tsx |
Animated typing state. |
Each entry has a <type>.schema.json declaring its data
shape. The schema's additionalProperties is constrained so
adding a field to the component without updating the schema
fails npm run type-check.
- SPA receives the envelope (GraphQL response or SSE frame).
- The dispatcher reads
envelope.typeand looks up the matching component inwidgetUtils.tsx. - The dispatcher passes
envelope.data(andenvelope.actionswhere relevant) into the component. - The component renders Material-UI elements with the data.
Actions (buttons, CTAs) fire callbacks that re-invoke
playbooks via
executePlaybook(...).
A widget action is not a side effect the SPA performs. The action is "invoke playbook X with workload Y." The SPA's only local action is "ask the gateway to run the next playbook."
Five steps, in this order:
Add playbooks/widget-contract/<your_widget>.schema.json. Use
the existing schemas as templates. The schema must:
- declare a
typeconstant matching the filename stem, - describe
dataexhaustively (every field, every enum, every nested shape), - set
additionalProperties: falseat every object level.
npm run contractsThis regenerates src/contracts/widgets.ts. Commit both the
new schema and the regenerated types.
Add src/components/widgets/<YourWidget>.tsx. Import the
generated type from src/contracts/widgets. The component
must accept data (and actions if applicable) typed against
the generated shape. Existing widgets are the style template:
Material UI, Emotion for component-scoped styles.
Wire the new component into the dispatch table in
src/components/widgets/widgetUtils.tsx
keyed by the widget's type string.
Add a minimal example envelope to
playbooks/widget-contract/widget_envelope_examples.md
(near a similar widget) so the smoke harness exercises it.
Run:
npm run smoke:widgets # round-trips every example through the dispatcher
npm test # vitest suite
npm run type-check
npm run lintIf the schema and the component agree, all four pass.
The new widget is now renderable. To get it onto the page, a
playbook step must produce an envelope of the new type. The
typical pattern is a tool: python step that builds the
envelope dict, then arcs into append_widget_event which
persists it.
The
render_widget_chat
step in the orchestrator is the example to follow.
The widget contract is the boundary between "domain knowledge" and "rendering knowledge."
- The playbook author knows what to say to the user. They emit envelopes.
- The component author knows how to draw it. They render envelopes.
The two sides agree only on the schema. There is no shared domain code, no shared formatting helpers, no shared business logic. This is what lets the SPA shell stay domain-neutral and makes the fork-for-another-domain workflow tractable.
- Architecture — how widgets fit in the four layers.
- Playbook: itinerary-planner — the playbook that emits these widgets in this repo.
- Adapting for your domain — how to replace this widget set with your own.
- In-repo:
docs/architecture/widget-contract.md— the detailed in-repo design doc.
Travel SPA
Architecture
- Architecture
- Widget contract
- Business data via playbooks
- Playbook: itinerary-planner
- Playbook: calendar/list
Integration
Operations
See also
- noetl wiki (app)
- ops wiki (deploy)
- Ephemeral Blueprints