Skip to main content

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)

KeyTypeRequiredDescription
idstringyesLowercase snake_case, ^[a-z][a-z0-9_]*$, 1–64 chars. Must match a key in schema.
typeenumyesOne of select, calendar, form, confirm.
labelstringyes (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

KeyTypeRequiredDefaultNotes
type"select"yesDiscriminator.
idstringyesSee common keys.

Schema keys (under schema[step.id])

KeyTypeRequiredDefaultNotes
idstringyesMirror of step id.
labelstringyesQuestion shown above the choices.
sourcestringyesidap://services, idap://staff, or any other IDAP collection.
displayenumnocard_gridOne of card_grid, avatar_list, dropdown, radio.
depends_onstring | string[]no[]Step ids this step depends on. The runtime filters the source by the answer to those steps.
filterobjectnoStatic 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

KeyTypeRequiredDefaultNotes
type"calendar"yesDiscriminator.
idstringyesSee common keys.

Schema keys

KeyTypeRequiredDefaultNotes
idstringyesMirror of step id.
labelstringyesQuestion shown above the picker.
sourcestringyescal.com or idap://availability.
cal_event_type_idint | "{{ flow.cal_event_type_id }}"when source=cal.comCal.com event-type id, or a Liquid reference resolved server-side.
depends_onstring | string[]no[]Step ids that influence available slots.
slot_duration_fromstringnoPath to a previous answer that supplies the slot length, e.g. service.duration_minutes.
filter_bystringnoPath 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

KeyTypeRequiredDefaultNotes
type"form"yesDiscriminator.
idstringyesSee common keys.

Schema keys

KeyTypeRequiredDefaultNotes
idstringyesMirror of step id.
labelstringyesStep heading.
fieldsFieldSchema[]yes1–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

KeyTypeRequiredDefaultNotes
type"confirm"yesDiscriminator.
idstringyesSee common keys.

Schema keys

KeyTypeRequiredDefaultNotes
idstringyesMirror of step id.
labelstringyesHeading shown above the summary.
showstring[]noall step idsDotted 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:

RuleWhy
flow.steps[] length 1–20Sanity 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 schemaOtherwise the renderer has nothing to render.
Every key in schema MUST appear as a step idNo orphan schemas.
confirm step is always lastSubmit logic depends on it.
extra="forbid" on every modelUnknown 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: