Flow Reference
A flow.json is an ordered list of steps. Each step has an id, a type, and (for some types) a source. The same id MUST appear as a key in schema.json — the runtime validates this on every save.
{
"steps": [
{ "type": "select", "id": "service" },
{ "type": "calendar", "id": "slot" },
{ "type": "form", "id": "contact" },
{ "type": "confirm", "id": "summary" }
]
}
There are exactly 4 step types: select, calendar, form, confirm. Together they cover every booking vertical we ship templates for.
Common keys (all step types)
| Key | Type | Required | Description |
|---|---|---|---|
id | string | yes | Lowercase snake_case, ^[a-z][a-z0-9_]*$, 1–64 chars. Must match a key in schema. |
type | enum | yes | One of select, calendar, form, confirm. |
label | string | yes (in schema) | Customer-facing label. Lives on the schema entry, not the step. |
Step IDs must be unique inside flow.steps[]. The runtime forbids duplicates.
select step
A choice-picker. Renders cards, avatars, a dropdown, or radio buttons. Fed from an IDAP source or an inline options list (use form + a select field for the latter — see below).
Step keys
| Key | Type | Required | Default | Notes |
|---|---|---|---|---|
type | "select" | yes | — | Discriminator. |
id | string | yes | — | See common keys. |
Schema keys (under schema[step.id])
| Key | Type | Required | Default | Notes |
|---|---|---|---|---|
id | string | yes | — | Mirror of step id. |
label | string | yes | — | Question shown above the choices. |
source | string | yes | — | idap://services, idap://staff, or any other IDAP collection. |
display | enum | no | card_grid | One of card_grid, avatar_list, dropdown, radio. |
depends_on | string | string[] | no | [] | Step ids this step depends on. The runtime filters the source by the answer to those steps. |
filter | object | no | — | Static filter passed to the source query. Example: {"active": true}. |
Example — service picker
{
"id": "service",
"label": "What are you coming in for?",
"source": "idap://services",
"display": "card_grid",
"filter": { "active": true }
}
Example — staff picker that depends on the chosen service
{
"id": "staff",
"label": "Choose your nail tech",
"source": "idap://staff",
"display": "avatar_list",
"depends_on": "service"
}
The runtime issues GET /api/v1/idap/staff?service_id={service.answer} and renders only the staff who can perform the chosen service.
calendar step
A date/time-slot picker. Sources slots either from Cal.com or from an IDAP availability table (e.g. restaurant tables).
Step keys
| Key | Type | Required | Default | Notes |
|---|---|---|---|---|
type | "calendar" | yes | — | Discriminator. |
id | string | yes | — | See common keys. |
Schema keys
| Key | Type | Required | Default | Notes |
|---|---|---|---|---|
id | string | yes | — | Mirror of step id. |
label | string | yes | — | Question shown above the picker. |
source | string | yes | — | cal.com or idap://availability. |
cal_event_type_id | int | "{{ flow.cal_event_type_id }}" | when source=cal.com | — | Cal.com event-type id, or a Liquid reference resolved server-side. |
depends_on | string | string[] | no | [] | Step ids that influence available slots. |
slot_duration_from | string | no | — | Path to a previous answer that supplies the slot length, e.g. service.duration_minutes. |
filter_by | string | no | — | Path to a previous answer used as a filter, e.g. party.size. Used by IDAP availability. |
Example — Cal.com slot picker
{
"id": "slot",
"label": "Pick a time",
"source": "cal.com",
"cal_event_type_id": "{{ flow.cal_event_type_id }}",
"depends_on": "staff",
"slot_duration_from": "service.duration_minutes"
}
Example — restaurant table availability
{
"id": "slot",
"label": "Pick a date and time",
"source": "idap://availability",
"filter_by": "party.size"
}
The runtime calls GET /api/v1/idap/availability?business_id={...}&party_size={party.size} and renders only slots where a table for that party size is free.
form step
A typeform-style sequential field set. Used for contact info, qualifiers, party size, anything that's a list of inputs.
Step keys
| Key | Type | Required | Default | Notes |
|---|---|---|---|---|
type | "form" | yes | — | Discriminator. |
id | string | yes | — | See common keys. |
Schema keys
| Key | Type | Required | Default | Notes |
|---|---|---|---|---|
id | string | yes | — | Mirror of step id. |
label | string | yes | — | Step heading. |
fields | FieldSchema[] | yes | — | 1–40 entries. Each is a field. |
Example — GDPR-compliant contact step
{
"id": "contact",
"label": "Your details",
"fields": [
{ "id": "name", "type": "text", "label": "Full name", "required": true },
{ "id": "phone", "type": "phone", "label": "Phone number", "required": true },
{ "id": "email", "type": "email", "label": "Email address", "required": false },
{ "id": "notes", "type": "textarea", "label": "Anything to add?", "required": false },
{ "id": "consent", "type": "checkbox", "label": "I agree to receive booking-related messages from {{ business.name }}.", "required": true },
{ "id": "marketing_opt_in", "type": "checkbox", "label": "Send me occasional offers and news from {{ business.name }}.", "required": false }
]
}
Example — sales qualifier
{
"id": "qualify",
"label": "Tell us about your business",
"fields": [
{ "id": "company", "type": "text", "label": "Company name", "required": true },
{ "id": "team_size", "type": "select", "label": "Team size",
"options": [
{ "label": "1–10", "value": "1-10" },
{ "label": "11–50", "value": "11-50" },
{ "label": "51–200", "value": "51-200" },
{ "label": "200+", "value": "200+" }
],
"required": true
},
{ "id": "budget", "type": "select", "label": "Monthly budget",
"options": [
{ "label": "< $1k", "value": "lt-1k" },
{ "label": "$1k–$5k", "value": "1k-5k" },
{ "label": "$5k–$20k", "value": "5k-20k" },
{ "label": "$20k+", "value": "gt-20k" }
],
"required": false
}
]
}
The full field-type list lives in Schema reference.
confirm step
The final step. Renders a summary of all collected answers and a submit button. Always last; the runtime rejects flows where confirm isn't the final step.
Step keys
| Key | Type | Required | Default | Notes |
|---|---|---|---|---|
type | "confirm" | yes | — | Discriminator. |
id | string | yes | — | See common keys. |
Schema keys
| Key | Type | Required | Default | Notes |
|---|---|---|---|---|
id | string | yes | — | Mirror of step id. |
label | string | yes | — | Heading shown above the summary. |
show | string[] | no | all step ids | Dotted answer paths to display. Allows hiding fields like consent. |
Example — show only the relevant answers
{
"id": "summary",
"label": "Confirm your appointment",
"show": ["service", "staff", "slot", "contact.name", "contact.phone"]
}
consent and marketing_opt_in are deliberately omitted — those are stored in IDAP but not re-shown to the customer.
Validation Rules (server-side)
The BookingFlow Pydantic model enforces these on every create/update — see app/schemas/booking.py:
| Rule | Why |
|---|---|
flow.steps[] length 1–20 | Sanity bound. |
Step id matches ^[a-z][a-z0-9_]*$ | Used as a JSON key and a JS identifier. |
Step id is unique inside flow.steps[] | No duplicate keys. |
Every id in flow.steps[] MUST have a key in schema | Otherwise the renderer has nothing to render. |
Every key in schema MUST appear as a step id | No orphan schemas. |
confirm step is always last | Submit logic depends on it. |
extra="forbid" on every model | Unknown keys are rejected, not silently dropped. |
A failing flow returns 400 Bad Request from POST /booking/flows — see Agent guide § dry_run.
End-to-end flow shape
{
"name": "Marina's Nail Studio — Booking",
"business_id": "8e7e4f8a-...-...",
"cal_event_type_id": 42,
"flow": {
"steps": [
{ "type": "select", "id": "service" },
{ "type": "select", "id": "staff" },
{ "type": "calendar", "id": "slot" },
{ "type": "form", "id": "contact" },
{ "type": "confirm", "id": "summary" }
]
},
"schema": {
"service": { /* … select schema … */ },
"staff": { /* … select schema … */ },
"slot": { /* … calendar schema … */ },
"contact": { /* … form schema … */ },
"summary": { /* … confirm schema … */ }
}
}
Where to go next:
- Schema reference — every field type and the seed templates that ship by default.
- Agent guide — call the MCP tools end-to-end.