Skip to content

07 · Interactive forms ​

The agent renders a form inside its chat bubble — radio groups, checkboxes, selects, text inputs — and gets the answers back as a normal user turn. Wire protocol: a new ui part agent→browser and ui_submit browser→agent. Full reference: Protocol → Interactive UI.

Bridle-only feature. Telegram, email, and pre-v0.12 SDK clients don't render it — see capability discovery below.

Drop-in helper for the showcase ​

If you just want the /form demo working end-to-end (the same flow the embed example page and its trigger button use), bridle/runtime/ ships a one-line opt-in:

ts
import { BridleRepository } from './channels/bridle.repository'
import { attachFormDemo }   from './channels/bridle.demo'

const bridle = new BridleRepository(process.env.BRIDLE_URL)
attachFormDemo(bridle)          // wires `/form` + ui_submit ack
await bridle.start()

That's enough to make any Bridle-connected embed page render the plan-picker form when the visitor sends /form. Below is the same code expanded inline if you want to write your own variants.

Agent code ​

Build the form on the runtime side and emit it as one of the parts[] of an assistant message:

ts
import { buildUiForm, BridlePartTypes } from '@cleanslice/bridle/runtime'

bridle.onMessage(async (msg) => {
  if (!msg.capabilities?.includes('ui')) {
    // Fallback for Telegram, older SDKs, etc.
    await bridle.send(msg.from, 'Reply with: basic, pro, or team')
    return
  }

  const form = buildUiForm(
    [
      { type: 'heading', text: 'Pick a plan' },
      { type: 'text',    text: 'You can change this later in account settings.' },
      {
        type: 'radio',
        name: 'plan',
        label: 'Plan',
        required: true,
        default: 'basic',
        options: [
          { value: 'basic', label: 'Basic — $0 / mo' },
          { value: 'pro',   label: 'Pro — $10 / mo' },
          { value: 'team',  label: 'Team — $30 / mo' },
        ],
      },
      {
        type: 'checkbox',
        name: 'newsletter',
        label: 'Send me weekly product updates',
      },
    ],
    { uiId: 'plan-2026-05', submitLabel: 'Continue' },
  )

  await bridle.send(msg.from, "Quick one before we get you set up:", [
    { type: BridlePartTypes.Text, text: "Quick one before we get you set up:" },
    form,
  ])
})

The next bridle.onMessage for the same client will carry the answers:

ts
bridle.onMessage(async (msg) => {
  for (const part of msg.parts) {
    if (part.type !== 'ui_submit') continue
    if (part.uiId === 'plan-2026-05') {
      const { plan, newsletter } = part.values
      await provisionAccount({ plan: String(plan), newsletter: !!newsletter })
      await bridle.send(msg.from, `Done — your ${plan} plan is set up.`)
    }
  }
})

What the visitor sees ​

A bordered card slides into the assistant bubble with your fields and a Submit button. Click Submit:

  1. Inline validation runs — required fields raise an inline error.
  2. The form disables itself (one-shot — no double submit).
  3. A user-side summary bubble appears in the transcript: Plan: pro · Send me weekly product updates: yes.
  4. The agent gets a ui_submit part on its next onMessage.

Capability discovery ​

Bridle SDK sends auth.capabilities: ['streaming', 'images', 'files', 'ui'] on the WebSocket handshake. The hub forwards it on every incoming message:

ts
bridle.onMessage(async (msg) => {
  msg.capabilities  // string[] | undefined
})

Always guard with msg.capabilities?.includes('ui') before emitting ui parts. Older SDKs (≤ 0.11.0) and non-Bridle channels won't set it — assume text only when missing.

Component cheat sheet ​

NeedComponent
Section titleheading
Help text under the titletext
Short free-form answerinput
Long free-form answertextarea
Pick exactly oneradio
Yes / nocheckbox
Pick multiplecheckbox-group
Long pick-one listselect

Full field reference: Protocol → Component reference.

Tips ​

  • Always set uiId. Use a stable string per flow step (plan-pick, email-collect, onboarding-step3). The agent uses it on ui_submit to know which form was answered.
  • Keep forms short. 3–6 fields fit cleanly in a bubble; more starts to look like a settings page. For long flows, split into multi-step (one form per agent message).
  • No conditional fields client-side. "Show B when A = pro" — emit B as a follow-up ui after the first submit.
  • Theming. Style via customCss targeting .bridle__ui, .bridle__ui-submit, etc. Full class list in Protocol → Themable classes.

Released under the MIT License.