Skip to main content
A Reply.blocks array is what the widget renders. Each entry has a type that decides the rendering, an id, a payload with the type-specific content, and an optional meta object (debug / source info — usually safe to ignore).
{ "id": "b_xxx", "type": "<type>", "payload": { ... }, "meta": { ... } }
Render rule: iterate blocks in order. For unknown type, render nothing (or a tiny debug stub in dev). Never block the conversation because of an unfamiliar block — the engine may add types over time and old widgets must keep working.

message

A line of agent text. Most common block.
{
  "id": "b_msg_1",
  "type": "message",
  "payload": {
    "role": "agent",
    "text": "What's your order number?",
    "format": "plain"
  }
}
FieldNotes
roleAlways agent for outgoing blocks. (user/system appear in history reads, not in widget replies.)
textThe body.
formatplain (escape everything) or markdown (render with a safe markdown renderer — no raw HTML).
Render as a chat bubble. For markdown, use a sandboxed renderer (e.g. marked with HTML disabled, or markdown-it with html: false).

form

A structured input. Triggers status: waiting_input on the reply.
{
  "id": "b_form_lookup",
  "type": "form",
  "payload": {
    "title": "Order lookup",
    "fields": [
      { "name": "order_number", "type": "text",   "label": "Order #", "required": true },
      { "name": "email",        "type": "email",  "label": "Email",   "required": false },
      { "name": "reason",       "type": "select", "label": "Reason",  "required": true,
        "options": [
          { "value": "late",    "label": "Arrived late" },
          { "value": "damaged", "label": "Damaged on arrival" }
        ] }
    ],
    "submit_label": "Check"
  }
}
Input field types:
CategoryTypesSubmitted value shape
Texttext, textarea, email, tel, url, number, datestring (or number for number)
Booleancheckboxboolean
Choiceselect, radiostring — one of options[].value
Choice (multi)multi_selectarray of strings
Ratingratinginteger 1..maxStars
Uploadfile_upload, image_upload, signatureFileRef object (or FileRef[] if multiple: true) — see below
For the choice types payload.fields[].options is a list of {value, label}multi_select accepts more than one selection. Display-only field types: heading, paragraph and divider carry no name, collect no value, and never appear in expectedInput.schema. Render them as a section title, helper text, and a horizontal rule respectively — not as inputs. Validation: the reply’s top-level expectedInput.schema is a JSON Schema covering every input field name. Run that schema client-side before sending the resume — saves a 422 round-trip. Upload-type properties carry an x-upload discriminator (one of file_upload / image_upload / signature) plus optional accept and maxSizeMb constraints; rating properties carry minimum: 1 and maximum: <maxStars>. Conditional logic. Any field may carry an optional visibleIf predicate describing when it should be shown. Evaluate it client-side as the user fills the form — when a visibleIf rule evaluates to false against the values the user has supplied so far, hide the field and do not submit its value.
{
  "name": "company_name", "type": "text", "label": "Company",
  "visibleIf": {
    "all_of": [
      { "field": "user_type", "op": "equals", "value": "business" }
    ]
  }
}
op is one of equals, not_equals, in, not_in, empty, not_empty. in/not_in take an array value; empty/not_empty omit value. Every rule in all_of must hold for the field to be visible. The server re-checks visibility against the submitted values on resume, so a required field that ended up hidden does not fail validation. On submit, send a resume body:
{
  "waitToken": "<from reply.waitToken>",
  "executionId": "<from reply.executionId>",
  "values": { "order_number": "12345", "email": "[email protected]" }
}

Rating

{
  "name": "satisfaction", "type": "rating", "label": "How was support?",
  "required": true, "maxStars": 5, "ratingIcon": "star"
}
FieldNotes
maxStarsNumber of slots (1–10). The validator enforces 1 ≤ value ≤ maxStars.
ratingIconstar (default), heart, or thumb. Purely visual.
Submitted value is the integer the user picked. Render as maxStars slots, update on hover/click.

File / image upload

{
  "name": "receipt", "type": "file_upload", "label": "Upload your receipt",
  "required": true,
  "accept": "application/pdf, image/*",
  "maxSizeMb": 10,
  "multiple": false,
  "retention": "persistent"
}
FieldNotes
acceptBrowser accept-style allow-list. MIME types (image/*, application/pdf) and extensions (.csv) both accepted. Empty/omitted = any type.
maxSizeMbPer-file cap, 1–100.
multipleWhen true, the submitted value is FileRef[]. Default false.
retentionpersistent (default — file kept until manually deleted) or transient (file deleted when the flow execution that submitted it terminates). Affects server-side retention only; the widget treats both modes identically.
Image-upload fields work the same way; accept defaults to image/*.

Signature

{
  "name": "sign_here", "type": "signature", "label": "Sign to confirm",
  "required": true,
  "canvasWidth": 400,
  "canvasHeight": 160,
  "retention": "persistent"
}
canvasWidth / canvasHeight are the HTML canvas dimensions in CSS pixels. On submit, encode the drawn signature as a PNG, upload it the same way as a file_upload (described below) and submit the resulting FileRef as the field value. The validator treats signature exactly like a single-file upload.

FileRef — the upload descriptor

Submit this object as the field value (never the raw bytes):
{
  "file_id": "0193f8a2-7c3e-7c11-bf60-9c1f3f8a2e10",
  "url":     "/api/v1/forms/uploads/0193f8a2-7c3e-7c11-bf60-9c1f3f8a2e10",
  "name":    "receipt.pdf",
  "mime":    "application/pdf",
  "size":    124032
}
The widget never uploads to the public chat endpoint directly. The two-phase flow — POST bytes to an integrator-owned upload endpoint, then submit the descriptor here — is documented in Lifecycle → Step 3 with uploads.

choice

One-of-many selection. Also triggers status: waiting_input.
{
  "id": "b_choice_topic",
  "type": "choice",
  "payload": {
    "prompt": "What can I help you with?",
    "options": [
      { "value": "order_status", "label": "Order status" },
      { "value": "returns",      "label": "Start a return" },
      { "value": "human",        "label": "Talk to a human" }
    ],
    "multiple": false
  }
}
Render as buttons (single-select) or checkboxes + submit (multi-select). On submit, resume with values: { <field>: <value> } — the field name is typically the block’s id or payload.name (check expectedInput.schema to be sure). A clickable call-to-action.
{
  "id": "b_link_help",
  "type": "link",
  "payload": {
    "label": "Open help center",
    "url": "https://acme.com/help",
    "target": "_blank"
  }
}
Render as a button/anchor. Sanitise url — always validate it parses as http/https before assigning to href. Fire a link_click telemetry event on click if you want analytics on CTA effectiveness.

image

{
  "id": "b_img_receipt",
  "type": "image",
  "payload": {
    "url": "https://cdn.acme.com/receipt.png",
    "alt": "Order receipt",
    "width": 400,
    "height": 240
  }
}
Set alt for accessibility. Constrain width with CSS — never trust the backend’s pixel hint to be smaller than your container.

card

A composite (image + headline + body + actions). Rich but optional — your widget can degrade to rendering the inner pieces as separate blocks.
{
  "id": "b_card_order",
  "type": "card",
  "payload": {
    "image": { "url": "...", "alt": "..." },
    "title": "Order #12345",
    "body": "Ships May 16. Tracking: 1Z…",
    "actions": [
      { "label": "Track", "url": "https://carrier.com/1Z…" },
      { "label": "Cancel", "value": "cancel_order" }
    ]
  }
}
Actions split into two flavours:
  • url — open externally (treat like a link).
  • value — feeds back into the conversation. Submit as a resume with values: { action: "<value>" } (check expectedInput.schema for the key name).

Defensive rendering checklist

  • Always render blocks in order. Don’t reorder by type.
  • Always escape user-supplied text. Never set innerHTML from a block payload unless it’s documented as markdown and you’re using a safe renderer.
  • Unknown type → skip silently.
  • Unknown fields on a known type → ignore.
  • Empty blocks array on completed is normal — the flow may end without a final message. Don’t show “no reply” — just close the input.