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).
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.
| Field | Notes |
|---|---|
role | Always agent for outgoing blocks. (user/system appear in history reads, not in widget replies.) |
text | The body. |
format | plain (escape everything) or markdown (render with a safe markdown renderer — no raw HTML). |
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.
| Category | Types | Submitted value shape |
|---|---|---|
| Text | text, textarea, email, tel, url, number, date | string (or number for number) |
| Boolean | checkbox | boolean |
| Choice | select, radio | string — one of options[].value |
| Choice (multi) | multi_select | array of strings |
| Rating | rating | integer 1..maxStars |
| Upload | file_upload, image_upload, signature | FileRef object (or FileRef[] if multiple: true) — see below |
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.
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:
Rating
| Field | Notes |
|---|---|
maxStars | Number of slots (1–10). The validator enforces 1 ≤ value ≤ maxStars. |
ratingIcon | star (default), heart, or thumb. Purely visual. |
maxStars slots,
update on hover/click.
File / image upload
| Field | Notes |
|---|---|
accept | Browser accept-style allow-list. MIME types (image/*, application/pdf) and extensions (.csv) both accepted. Empty/omitted = any type. |
maxSizeMb | Per-file cap, 1–100. |
multiple | When true, the submitted value is FileRef[]. Default false. |
retention | persistent (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. |
accept defaults to image/*.
Signature
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):
choice
One-of-many selection. Also triggers status: waiting_input.
values: { <field>: <value> } — the field name is
typically the block’s id or payload.name (check expectedInput.schema to
be sure).
link
A clickable call-to-action.
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
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.
url— open externally (treat like alink).value— feeds back into the conversation. Submit as a resume withvalues: { action: "<value>" }(checkexpectedInput.schemafor the key name).
Defensive rendering checklist
- Always render blocks in order. Don’t reorder by
type. - Always escape user-supplied text. Never set
innerHTMLfrom 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
blocksarray oncompletedis normal — the flow may end without a final message. Don’t show “no reply” — just close the input.