Appointment Fields
The ServiceAppointment model defines the essentials every booking needs — service, customer, time, duration, price, and status. Appointment fields capture everything beyond that: the business-specific detail that makes a booking useful but that no fixed model could anticipate. A salon needs a preferred stylist and allergy notes; a clinic needs a reason for visit and insurance details; a consultancy needs a project brief.
Rather than forcing every business into one rigid form, appointment fields let you declare your own data model for appointments — typed fields, validation, grouping, and conditional logic — defined once at the organization level and tailored per service. When a customer books through an AI voice or chat agent, the agent reads these definitions and guides the customer through providing exactly the right detail, validating answers as it goes.
Appointment fields are defined declaratively as configuration (the schemas below) and applied at two scopes: organization-wide and per-service. The structures on this page describe that configuration and the data captured against it.
The three-level hierarchy
Configuration flows downward, data flows back up. The organization defines a reusable library; each service selects, overrides, and extends it; each appointment captures the resulting values.
This architecture gives you reusable field definitions at the organization level, service-specific customization of which fields appear, and per-appointment data capture linked to the customer for reuse on future visits.
Organization field library
AppointmentFieldConfig is the organization-wide library of fields and groups that services draw from. Define a field once here and any service can inherit it.
| Field | Type | Required | Description |
|---|---|---|---|
id | string | Yes | Unique identifier |
fields | FieldDefinition[] | Yes | Field definitions (default: []) |
groups | FieldGroup[] | Yes | Field groupings for form sections (default: []) |
reuseDetails | boolean | Yes | Pre-fill from a returning customer's last answers (default: false) |
ensureEmail | boolean | Yes | Always include an email field (default: false) |
ensurePhone | boolean | Yes | Always include a phone field (default: false) |
supportService | enum | null | No | Which platform service this config applies to |
supportService
Scopes a field library to one platform service, so appointment booking and, say, order capture can each have their own fields.
| Value | Applies to |
|---|---|
appointment_management | Service appointments |
menu_order_management | Menu orders |
product_order_management | Product orders |
reservation_management | Reservations |
property_management | Property inquiries |
none / null | Not scoped to a specific service |
FieldDefinition
Each field declares its key, type, label, and optional validation, conditions, and UI hints.
| Property | Type | Required | Description |
|---|---|---|---|
fieldKey | string | Yes | Stable key; pattern ^[a-z][a-z0-9_]*$ (used as the data key) |
fieldType | enum | Yes | One of the field types below |
label | string | Yes | Human-readable label (min length 1) |
description | string | No | Helper description |
groupKey | string | No | Group this field belongs to |
options | object[] | No | Choices for select / multiselect (value, label, displayOrder) |
defaultValue | any | No | Default value |
isActive | boolean | No | Whether the field is shown |
condition | object | No | Show the field only when another field's value matches (see below) |
validation | object | No | Validation rules (see below) |
uiHints | object | No | Presentation hints (see below) |
Field types
text, textarea, number, boolean, date, time, datetime, email, phone, select, multiselect.
Validation
| Rule | Type | Description |
|---|---|---|
required | boolean | Field must be answered |
minLength / maxLength | integer (> 0) | Length bounds for text values |
min / max | number | Numeric bounds |
pattern | string | Regex the value must match |
patternMessage | string | Message shown when pattern fails |
Conditional display
A field can depend on another field's value, so the agent only asks when relevant.
| Property | Type | Description |
|---|---|---|
dependsOn | string | fieldKey of the controlling field |
operator | enum | equals, notEquals, contains, isEmpty, isNotEmpty, greaterThan, lessThan |
value | any | Value to compare against |
UI hints
| Property | Type | Description |
|---|---|---|
placeholder | string | Placeholder text |
helpText | string | Inline help |
displayOrder | integer | Order within the group |
width | enum | full, half, or third |
FieldGroup
Groups organize fields into labeled, optionally collapsible sections.
| Field | Required | Description |
|---|---|---|
groupKey | Yes | Stable group key referenced by fields |
label | Yes | Section label |
description | No | Section description |
displayOrder | No | Section order |
collapsible | No | Whether the section can collapse |
defaultCollapsed | No | Whether it starts collapsed |
Example
{
"id": "afc_salon_booking",
"fields": [
{
"fieldKey": "preferred_stylist",
"fieldType": "select",
"label": "Preferred Stylist",
"groupKey": "preferences",
"options": [
{ "value": "no_preference", "label": "No Preference", "displayOrder": 0 },
{ "value": "jane", "label": "Jane Smith", "displayOrder": 1 },
{ "value": "john", "label": "John Doe", "displayOrder": 2 }
],
"uiHints": { "displayOrder": 1 }
},
{
"fieldKey": "special_requests",
"fieldType": "textarea",
"label": "Special Requests",
"groupKey": "preferences",
"uiHints": {
"placeholder": "Any allergies, preferences, or special requests?",
"displayOrder": 2
}
},
{
"fieldKey": "referral_source",
"fieldType": "select",
"label": "How did you hear about us?",
"groupKey": "marketing",
"options": [
{ "value": "google", "label": "Google Search" },
{ "value": "social", "label": "Social Media" },
{ "value": "friend", "label": "Friend/Family" },
{ "value": "other", "label": "Other" }
],
"validation": { "required": true },
"uiHints": { "displayOrder": 1 }
}
],
"groups": [
{
"groupKey": "preferences",
"label": "Your Preferences",
"description": "Help us personalize your experience",
"displayOrder": 1,
"collapsible": false
},
{
"groupKey": "marketing",
"label": "About You",
"displayOrder": 2,
"collapsible": true,
"defaultCollapsed": false
}
],
"reuseDetails": true,
"ensureEmail": true,
"ensurePhone": true,
"supportService": "appointment_management"
}
Service-level overrides
Most services don't need every field in the library, and some need their own. A service tailors the library through its requiredDatafieldConfig — a ServiceAppointmentFieldConfig embedded on the BusinessServiceConfig. It inherits a subset of org fields, overrides individual properties, and adds service-only fields.
| Field | Type | Default | Description |
|---|---|---|---|
inheritedFieldKeys | string[] | [] | fieldKey values pulled in from the org library |
fieldOverrides | FieldDefinition[] | [] | Per-field overrides (e.g. relabel, make required) |
additionalFields | FieldDefinition[] | [] | Fields unique to this service |
isActive | boolean | true | Whether this service config is active |
reuseDetails | boolean | false | Reuse the customer's previous values for this service |
Example
A haircut service inherits two org fields, makes one of them required with a friendlier label, and adds a hair-length question only it needs:
{
"inheritedFieldKeys": ["special_requests", "referral_source"],
"fieldOverrides": [
{
"fieldKey": "special_requests",
"fieldType": "textarea",
"label": "Notes for your stylist",
"validation": { "required": true }
}
],
"additionalFields": [
{
"fieldKey": "hair_length",
"fieldType": "select",
"label": "Current Hair Length",
"options": [
{ "value": "short", "label": "Short" },
{ "value": "medium", "label": "Medium" },
{ "value": "long", "label": "Long" }
],
"validation": { "required": true }
}
],
"isActive": true,
"reuseDetails": true
}
requiredDatafieldConfig is a field on the service itself. See Services & Categories for where it sits in BusinessServiceConfig, alongside duration, bookingRules, and pricing.
Per-appointment capture
When a booking is made, the answered values are stored as an AppointmentAdditionalInfo record — one per appointment.
| Field | Type | Required | Description |
|---|---|---|---|
id | string | Yes | Unique identifier |
businessServiceId | string | Yes | Reference to the service |
appointmentId | string | Yes | Reference to the appointment (1:1) |
customerId | string | Yes | Reference to the customer |
data | object | Yes | Key-value store of captured field values |
How values are typed
In data, each key is a field's fieldKey and each value is typed by that field's fieldType:
| Field type | Value in data |
|---|---|
text, textarea, email, phone | string |
number | number |
boolean | boolean |
date | string — YYYY-MM-DD |
time | string — HH:MM (24-hour) |
datetime | string — YYYY-MM-DDTHH:MM:SS |
select | string (the selected value) |
multiselect | string[] (selected values) |
Example
{
"id": "aai_booking_12345",
"businessServiceId": "svc_haircut",
"appointmentId": "apt_12345",
"customerId": "cust_jane",
"data": {
"preferred_stylist": "jane",
"special_requests": "Please use hypoallergenic products",
"referral_source": "friend",
"hair_length": "medium"
}
}
How AI agents use these fields
Because the fields are declared with types, validation, and conditions, an AI voice or chat agent can run the whole capture conversationally — no static form required:
- Guides the customer — the agent asks for each active field in
displayOrder, using thelabel,description, anduiHints.helpTextto phrase natural questions. - Validates in the moment — answers are checked against
validation(required, length, numeric bounds,pattern), and the agent re-asks withpatternMessagewhen something doesn't fit. - Skips irrelevant questions —
conditionrules mean the agent only asks a field when itsdependsOnvalue matches, keeping the conversation short. - Reuses what it knows — when
reuseDetailsis true, the agent pre-fills from the customer's most recentAppointmentAdditionalInfofor that service and simply confirms rather than re-asking. - Captures structured data — every answer lands in
dataas a typed value, ready for your systems, staff, and reporting.
Query options
Both resources support filtering and sorting.
| Resource | Filters | Sort fields |
|---|---|---|
AppointmentFieldConfig | reuseDetails, ensureEmail, ensurePhone | createdAt, updatedAt (asc / desc) |
AppointmentAdditionalInfo | businessServiceId, appointmentId, customerId | createdAt, updatedAt (asc / desc) |
Usage patterns
- Build the form (or conversation) — fetch the org
AppointmentFieldConfig, apply the service'srequiredDatafieldConfigto resolve the final field set, then render byfieldTypeand group, or let the agent drive it. - Capture values — validate answers against the field definitions, then store an
AppointmentAdditionalInfolinked byappointmentIdandcustomerId. - Reuse data — when
reuseDetailsis true, look up the customer's most recent entry for the same service and pre-fill, so returning customers only confirm.