Optionalopts: SayStepOptionsAsk a question and store the answer in c.state[stateKey]. With { schema } the parsed
value is stored and typed: .ask('¿Edad?', 'edad', { schema: z.coerce.number() }) →
c.state.edad: number.
Optionalopts: AskStepOptions<T>Send media. kind is one of image | video | audio | voice | document | sticker | location:
.media('image', 'https://…/menu.png', 'Nuestro menú')
.media('document', './factura.pdf', 'Tu factura', { filename: 'factura.pdf' })
.media('location', { lat: 4.71, lng: -74.07 }, 'Bogotá')
The legacy two-argument form .media(source, caption?) sends an image.
Optionalcaption: stringOptionalopts: FlowMediaOptionsSend media. kind is one of image | video | audio | voice | document | sticker | location:
.media('image', 'https://…/menu.png', 'Nuestro menú')
.media('document', './factura.pdf', 'Tu factura', { filename: 'factura.pdf' })
.media('location', { lat: 4.71, lng: -74.07 }, 'Bogotá')
The legacy two-argument form .media(source, caption?) sends an image.
Optionalcaption: stringOptionalopts: FlowMediaOptionsSend media. kind is one of image | video | audio | voice | document | sticker | location:
.media('image', 'https://…/menu.png', 'Nuestro menú')
.media('document', './factura.pdf', 'Tu factura', { filename: 'factura.pdf' })
.media('location', { lat: 4.71, lng: -74.07 }, 'Bogotá')
The legacy two-argument form .media(source, caption?) sends an image.
Optionalcaption: stringPresent a numbered menu and capture the selection. The picked option KEY is stored in
c.state[stateKey] (defaults to c.state.choice), so a later .say/.do can read it.
An option's then sub-flow runs inline and returns here (sharing c.state); its goto
leaves the flow for another conversation.
OptionalstateKey: KOptionalopts: ChooseStepOptionsRun another flow inline, then come back and continue with the next step. The sub-flow
shares c.state, so whatever it captures is visible here (and typed: the builder's
state merges the sub-flow's). The sub-flow is snapshotted at this call.
Run another flow inline, then come back and continue with the next step. The sub-flow
shares c.state, so whatever it captures is visible here (and typed: the builder's
state merges the sub-flow's). The sub-flow is snapshotted at this call.
Stop the flow unless check passes. On failure it sends onFail and ends the conversation,
or — with { redirect } — jumps to another conversation:
.guard(() => esHorarioLaboral(), 'Fuera de horario, vuelve mañana')
.guard(async (ctx) => isAdmin(ctx.sender), { redirect: 'no-permiso' })
OptionalonFail: GuardFailure<S>Collect a typed form. On providers with capabilities.forms === 'native'
(WhatsApp Cloud API → WhatsApp Flows) it sends one native form and waits for
its submission; elsewhere it asks field by field. Either way the validated
answers are stored in c.state[opts.stateKey ?? definition.id].
The definition is checked here: a malformed one throws a TypeError.
Optionalopts: FormStepOptionsflow('registro')
.form({ id: 'signup', title: 'Registro', fields: [
{ type: 'text', name: 'name', label: 'Nombre' },
{ type: 'email', name: 'email', label: 'Correo' },
{ type: 'select', name: 'plan', label: 'Plan', options: { basic: 'Básico', pro: 'Pro' } },
] })
.say((_ctx, c) => `Gracias ${(c.state.signup as { name: string }).name}`)
Send a message.
{ delay }pauses before sending.