- On providers with
capabilities.forms === 'native'(today the WhatsApp Cloud API,@botmesh/meta) the form goes out as one native form, a WhatsApp Flow, and the submission comes back as a single message. - Everywhere else (Baileys, Telegram, Discord, Messenger, Instagram, the simulator) the bot asks field by field with plain text questions.
c.state.
Fields
Every field also takes
name, label, required (default true, false for
optin), helper, prompt (field-by-field question text) and error (custom
error message). The definition is checked when the flow is built: a bad id, a
duplicate field name or empty options throw a TypeError. Besides id,
title, intro and fields, a definition takes cta (the button that opens a
native form) and submitLabel. When omitted, they come from the core.form.cta
/ core.form.submit catalog entries in the subject’s ctx.lang ('Abrir formulario' / 'Enviar' in Spanish, 'Open form' / 'Submit' in English);
a definition with neither intro nor title gets core.form.body as the
message body. The submit label is part of the published WhatsApp Flow, so it
is fixed when you publish it: toFlowJson(def, { lang: 'es' }), or for
publishForm() the provider’s lang option (new CloudAPIProvider({ lang: 'es' }),
set it to your bot’s BotConfig.lang) or publishForm(def, { lang: 'es' })
(default 'en', the core default locale), or set submitLabel.
In field-by-field mode, select/multiselect accept the menu number, the key or
the label (several picks separated by commas, ;, and/y or spaces between
numbers); dates accept YYYY-MM-DD, DD/MM/YYYY, DD-MM-YYYY and DD.MM.YYYY; numbers accept a decimal
point or comma (1.5, 1,5). Grouped numbers such as 1.500 or 1,500 are
rejected (the separator could mean thousands or decimals), and the user is
asked to type them without a thousands separator.
Native submissions are validated again on the server: a client-side form is never
trusted.
Options
skipWord, pendingMessage, the field-by-field hints (1) Yes / 2) No, the
date format, “type … to skip”) and every validation error default to the
built-in core.form.* messages in the subject’s language (ctx.lang, es and
en included). Override them per locale with bot.locale() (see
i18n); a field’s own error still wins.
In 'auto' mode the step renders natively only when the provider declares
forms: 'native' and can render this particular form. Providers say so with
the optional IProvider.canRenderForm(form, { nativeId }) (available as
ctx.canRenderForm()). When it returns false, the step asks field by field
straight away, without sending a form message that would fail. If a native send
is still rejected with UnsupportedContentError, auto mode falls back to field
by field too. 'native' never falls back.
WhatsApp Cloud API
@botmesh/meta compiles a form into a static, single-screen WhatsApp Flow
(Flow JSON 6.2, navigate action, no endpoint and no encryption) and sends it
as an interactive flow message. The nfm_reply webhook is turned into a
'form' message whose flow_token matches the pending step.
A Flow has to exist on Meta before it can be sent. Map your form ids to Flows:
config.flows entry, no publishForm() and no
nativeId) is asked field by field, as on any other channel.
flowJsonWarnings(def) lists labels WhatsApp will truncate (FLOW_LIMITS).
toFlowJson(def, { lang }) picks the language of the default submit label.
Endpoint Flows (data_exchange, dynamic encrypted screens) are not supported yet.
Testing and introspection
MockProvider declares no native forms, so createTestBot runs forms field by
field. To test the native path, set provider.capabilities.forms = 'native' and
answer with chat.send(formReplyBody({ token, values }), { type: 'form' })
(formReplyBody comes from @botmesh/core/spi; token is the one on the form
message the bot sent, in provider.outbox).
bot.describe() draws form nodes with their fields, and bot.lint() reports
form-state-collision when a form writes a c.state key that another
ask/choose/form of the same flow also writes. The timeout rules apply to
forms too. See Inspect & lint flows.