Mind the Front Docs Examples For AI agents llms.txt

Writing a config

One JSON document: submission declares what a request is, flow declares how it is collected.

PartDeclares
versionalways 2
meta.businessNamethe name the widget shows
meta.localeBCP 47 tag such as en or pt-PT: language of formatted dates and times; write config texts in it, one language per config
theme.primaryColoraccent colour: #rgb, #rgba, #rrggbb or #rrggbbaa
theme.avatarimage next to bot lines
submissionfields, types, allowed values, required
flowsteps, transitions, screens

Every text in the config is shown as plain text; HTML is not interpreted. The widget's own labels — loading, errors, Try again — are English.

A complete config is at the end of this page.

The canonical source is the schema at /api/public/config-schema; this page is the guide to it.

The submission schema

A subset of JSON Schema: every document here must be valid JSON Schema and renderable by the widget.

AllowedNot allowed
type: "object" at the rootnested objects, arrays of objects
additionalProperties: falseadditionalProperties: true or absent
oneOf of { const, title }enum with enumNames
string fields with the annotations below$ref, allOf, anyOf, not
required as a list of namespatterned property names
{
  "type": "object",
  "additionalProperties": false,
  "required": [
    "service",
    "name",
    "phone"
  ],
  "properties": {
    "service": {
      "title": "Service",
      "oneOf": [
        {
          "const": "consultation",
          "title": "Consultation"
        },
        {
          "const": "hygiene",
          "title": "Hygienist clean"
        },
        {
          "const": "filling",
          "title": "Filling"
        }
      ]
    },
    "dentist": {
      "title": "Dentist",
      "oneOf": [
        {
          "const": "hall",
          "title": "Dr Hall"
        },
        {
          "const": "price",
          "title": "Dr Price"
        },
        {
          "const": "any",
          "title": "Whoever is free"
        }
      ]
    },
    "preferred_date": {
      "title": "Preferred date",
      "type": "string",
      "format": "date"
    },
    "name": {
      "title": "Your name",
      "type": "string",
      "minLength": 1
    },
    "phone": {
      "title": "Phone",
      "type": "string",
      "format": "phone",
      "placeholder": "+44 7700 900000"
    },
    "email": {
      "title": "Email",
      "type": "string",
      "format": "email"
    },
    "note": {
      "title": "Anything we should know",
      "type": "string",
      "maxLength": 200,
      "placeholder": "Optional"
    }
  }
}

Field annotations

AnnotationApplies toRequiredMeaning
titleevery fieldyeslabel in the dialogue and row label in summary
oneOfchoice fields—options, each { const, title }; presence makes the field a choice
type: "string"everything elseyesthe only scalar type the widget renders
formatstring fieldsnoemail, phone, date, date-time — same four the schema accepts
placeholderstring fieldsnohint inside the empty input
minLength, maxLengthstring fieldsnolength checked before the step advances
durationMinutesdate-time fieldsyesslot length in minutes, positive integer
scheduledate-time fieldsyesworking hours as rules; see appointment
timeZonedate-time fieldsyesIANA zone the schedule hours are read in

Which annotation produces which input — widget catalogue.

The flow

flow.start names the first step; flow.steps maps ids to steps.

StepRequired keysOptional keysPurpose
fieldfields, nextprompt, submitLabelfills one or more declared fields; prompt is the bot line above them, submitLabel the form button
messagetext, next—a bot line
summaryfields, actionstitlereview and send
status_trackerentitytitleshows the request status; entity has no effect, write "request"

next is either a step id or a rule list:

"next": {
  "rules": [{ "if": { "field": "service", "eq": "filling" }, "then": "urgency" }],
  "else": "contacts"
}

Rules are evaluated in order; the first match wins; else is required.

Rules a stored config obeys

Saving a draft and POST /api/public/configs/validate run the same checks. path is a JSON Pointer to the node that breaks the rule. A schema violation comes back as several errors, one per shape the validator tried; the path in the table names it.

RuleRejected byWhat validate returns
step types are the four aboveschemapath /flow/steps/<id>/type
a conditional transition has elseschemapath /flow/steps/<id>/next
a choice field lists optionsschemapath /submission/properties/<name>/oneOf
a field has only the annotations aboveschemapath /submission/properties/<name>
theme.primaryColor has 3, 4, 6 or 8 hex digitsschemapath /theme/primaryColor
durationMinutes is a positive integerschemapath /submission/properties/<name>/durationMinutes
references resolve to existing stepsflow-lintStep "<id>" references a step that does not exist: "<target>"
every step is reachable from startflow-lintStep "<id>" is unreachable from the start step
steps name declared fieldsflow-lintStep "<id>" references field "<name>", which is not declared in submission.properties
every declared field is filled somewhereflow-lintField "<name>" is declared in submission but no step fills it
required holds on every path to confirm, not only on oneflow-lintField "<name>" is required but is not filled on every path to the submit step "<id>"
an appointment field declares durationMinutesflow-lintField "<name>" has format "date-time" but no durationMinutes
an appointment field declares scheduleflow-lintField "<name>" has format "date-time" but no schedule
an appointment field declares timeZoneflow-lintField "<name>" has format "date-time" but no timeZone
schedule hours run forwardflow-lintField "<name>": schedule rule <n> ends at or before it starts (<from>–<to>)
rules for one day do not overlapflow-lintField "<name>": schedule intervals overlap on <day>
some interval fits a visitflow-lintField "<name>": no schedule interval is long enough for a <n>-minute appointment

Assembling a config

  1. Declare the request. List what the business needs to know as fields, with title and, where it matters, format. Mark as required only what a person on the phone would insist on.
  2. Lay out the dialogue. One field step per question, or one step per group of questions that belong together. Screens between them.
  3. Branch where the process branches, and give every branch an else.
  4. End with summary and status_tracker. The confirm action is what sends the request.
  5. Validate until valid — request and response.

A complete config

{
  "version": 2,
  "meta": {
    "businessName": "Northside Barbers",
    "locale": "en"
  },
  "theme": {
    "primaryColor": "#1f6f5c"
  },
  "submission": {
    "type": "object",
    "additionalProperties": false,
    "required": [
      "service",
      "appointment",
      "name",
      "phone"
    ],
    "properties": {
      "service": {
        "title": "Service",
        "oneOf": [
          {
            "const": "haircut",
            "title": "Haircut"
          },
          {
            "const": "beard",
            "title": "Beard trim"
          },
          {
            "const": "both",
            "title": "Haircut and beard"
          }
        ]
      },
      "appointment": {
        "title": "Appointment time",
        "type": "string",
        "format": "date-time",
        "durationMinutes": 45,
        "timeZone": "Europe/London",
        "schedule": [
          {
            "days": [
              "mon",
              "wed",
              "thu"
            ],
            "from": "10:00",
            "to": "19:00"
          },
          {
            "days": [
              "fri"
            ],
            "from": "10:00",
            "to": "21:00"
          },
          {
            "days": [
              "tue"
            ],
            "from": "15:00",
            "to": "19:00"
          },
          {
            "days": [
              "sat"
            ],
            "from": "11:00",
            "to": "14:00"
          },
          {
            "days": [
              "sat"
            ],
            "from": "15:00",
            "to": "18:00"
          }
        ]
      },
      "name": {
        "title": "Your name",
        "type": "string",
        "minLength": 1
      },
      "phone": {
        "title": "Phone",
        "type": "string",
        "format": "phone",
        "placeholder": "+44 7700 900000"
      }
    }
  },
  "flow": {
    "start": "greeting",
    "steps": {
      "greeting": {
        "type": "message",
        "text": "Hi! Let's book you a chair. Takes about a minute.",
        "next": "service"
      },
      "service": {
        "type": "field",
        "prompt": "What are you coming in for?",
        "fields": [
          "service"
        ],
        "next": "appointment"
      },
      "appointment": {
        "type": "field",
        "prompt": "Pick a time that works for you:",
        "fields": [
          "appointment"
        ],
        "next": "contacts"
      },
      "contacts": {
        "type": "field",
        "prompt": "Last thing — how do we reach you?",
        "fields": [
          "name",
          "phone"
        ],
        "submitLabel": "Continue",
        "next": "confirm"
      },
      "confirm": {
        "type": "summary",
        "title": "Check your booking:",
        "fields": [
          "service",
          "appointment",
          "name",
          "phone"
        ],
        "actions": [
          {
            "type": "confirm",
            "label": "Book it",
            "next": "status"
          },
          {
            "type": "edit",
            "label": "Change time",
            "target": "appointment"
          },
          {
            "type": "restart",
            "label": "Start over"
          }
        ]
      },
      "status": {
        "type": "status_tracker",
        "entity": "booking",
        "title": "Your booking"
      }
    }
  }
}

Reserved

KeyStatus
integrationsaccepted, no effect: notifications are set up in the console