Asking the User
A loop can ask the person it serves for four things: an answer to a question, details on a form, proof of an email address, and consent before it acts on their page. Each one pauses the run until the user answers. A run with no user cannot ask anything; see Triggers and Schedules.
A question
The model asks with ask_user, and the user’s next message is the answer. A reply that is only
text also waits for the user. When the job is done, the model calls finish, and the user reads
the loop’s closing line.
The user answers one question at a time. A second ask_user in the same reply is refused, and the
model asks it after the user answers. A question the model’s text asks just before its ask_user is
not shown, because the user reads the ask_user question; the run’s journal counts each one it
held back.
When the answer is one of a small fixed set, the model passes them as options (two to six
answers of a few words, each one a real answer). A question whose answer is free text, such as a
list or an address, gets no options. The user sees them on a card under the question, picks one and presses Send, and the
model reads the picked answer’s words as the reply. The user can also press Skip and type an
answer of their own. The model never asks the user to type an exact phrase.
A form
A form collects details that no model reads, such as an email address. Declare each input in the loop’s record:
inputs:
- id: email
label: Work email
kind: email
required: true
- id: company_size
label: Company size
kind: choice
options:
- {id: small, label: Under 50 people}
- {id: large, label: 50 people or more}
| Kind | What the user gives | What the model reads |
|---|---|---|
email, text | Typed text | Only whether it was given |
choice | One of options, or of a collection’s rows with options_from | The chosen option’s id, unless the input says sealed: true |
choices | Any of options, between min and max | The chosen ids, unless sealed |
boolean | One checkbox | True or false, unless sealed |
The model asks for inputs by id with request_inputs, and the chat shows the form. required
inputs must be filled before the form sends, but the user can always decline the whole form.
A value from a fixed list is decided first, and asked on a card only when it cannot be. When
what the user already gave settles it — a restaurant receipt is a meal — the model uses it without
asking. When it does not, the model asks with request_inputs if the loop declares a choice
input for it, so the user picks from the options; with no such input, it asks with ask_user
and offers the options on a card. Declare the choice input for any fixed list your loop needs.
What the user already said is filled in. When the model asks with a form and the conversation
already answers part of it (“my order 1042 arrived broken”), it passes those answers in
prefill, and the form opens with them chosen. The user changes any of them or sends the form as
it is, so they never give the same answer twice. Only an input the model can see is filled in: a
choice, choices or boolean input that is not sealed: true, with one of its own options
(for options_from, one of the collection’s rows). An email or text input, or any sealed
input, always opens empty, because the user types it themselves. A value that cannot be used is
left out, and the model is told which one and why.
A refusal is an answer. The user can press Don’t share, or type in the chat while the
form is open; typing closes the form, and the loop reads their words next. Either way
request_inputs returns declined, and the loop replies to it: it carries on without a detail it
does not need, and for one it does, it says in one sentence why and what the user can do instead,
and ends kindly without inviting a reply, because the conversation ends there. It shows the form
again only when the user’s own message asks for it. A user’s typed words count as
their answer where they settle the question (“bath and brush, please”). Write in your loop’s
entry what it does without each required detail.
Send a value to a tool without the model
A tool receives a value through bind, which maps a tool argument to an input id:
tools:
- use: create_lead
bind:
email: email
The bound argument leaves what the model sees, and the platform fills it in when the tool runs.
Publish refuses a bind that names an input the loop does not declare, or an argument the tool
does not have.
Keep a value in a collection without the model
A collection can take a value through bind too, which
maps a field to an input id:
collections:
- name: leads
key: email
fields:
email: string
company: string
bind:
email: email
When a run writes leads, the platform fills email from what the user typed, and the model
leaves it out. A value the model sends for a bound field is refused. If the user leaves an input
that is not required empty, the record is written without that field. The write waits, and
tells the model to ask with request_inputs, only while a required input is empty, while the
input the collection’s key comes from is empty, or before the user has given any of them.
A bound field is sealed: no model reads it back. records_query leaves it out of every record
and says so, for every loop that reads the collection. A record keyed on a bound field shows the
model its id as [sealed:email]. You still see the value on Collections, through an API key,
and on a page. A search index cannot cover a sealed field.
A collection keyed on a sealed input must bind it. With no bind, every record would carry the
same placeholder key and replace the one before it, so publish refuses it and names both fixes:
bind the input, or key the collection on a field the model writes.
Each input must be declared in inputs, and each bound field in fields with the type of the
input’s value: string for email, text and choice, array for choices, and boolean for
boolean.
A seal is never lifted. A later version without the bind keeps the field sealed, because the
records already hold real values.
Record who started the run
A collection can also bind run_subject, which holds who started the run. The user does not type
it, and you do not declare it in inputs:
collections:
- name: bookings
key: slot
fields:
slot: string
visitor: string
bind:
visitor: run_subject
The platform fills visitor from the run itself. For a visitor who is not signed in, the value is
anonymous: and their visitor id. A page with rows: owner_field:visitor
then shows each reader only the bookings their own runs wrote. A run that an API key or a schedule
starts has no subject, so its record is written without visitor. An input cannot use the id
run_subject: publish refuses it.
Keep one entry per order
A collection’s key is the thing there is exactly one entry of. Key orders on the customer’s
email and their second order replaces the first. Bind run_id instead, which holds which
conversation wrote the entry, and key on that field:
collections:
- name: cake_orders
key: order_ref
fields:
order_ref: string
email: string
size: string
bind:
order_ref: run_id
email: email
The platform fills order_ref from the conversation itself, so each conversation keeps one order,
and a change later in the same conversation updates it. Key on email only when you mean one
entry per person, such as a waitlist. An input cannot use the id run_id: publish refuses it.
When one conversation files several entries, such as a meeting’s action items or an order’s lines,
bind run_item instead and declare a number field n. The model numbers the entries 1, 2, 3 in
the order it meets them, and the platform fills the bound field with the conversation and that
number, so each entry has its own key, and a corrected entry written again with the same n
updates it:
collections:
- name: action_items
key: item_ref
fields:
item_ref: string
n: number
task: string
owner: string
bind:
item_ref: run_item
Publish refuses run_item on a collection with no n: number, and in a tool’s bind.
When each person keeps their own entries across conversations, and the same thing filed twice is
one entry, such as an employee’s expense receipts, bind subject_item instead and declare a text
field item. The model writes in item what identifies the thing itself, such as a receipt’s
date and total, never wording it chooses. The platform fills the bound field with who started the
run and that item, so two people filing the same receipt keep one entry each, and one person
filing it twice keeps one:
collections:
- name: expense_claims
key: claim_ref
writes: create
fields:
claim_ref: string
item: string
merchant: string
amount: number
bind:
claim_ref: subject_item
With writes: create, the second filing of the same receipt is refused rather than replacing the
first, and the loop tells the person it looks like one they already filed. Publish refuses
subject_item on a collection with no item: string, and in a tool’s bind. A run nobody
started, by a key or a schedule, cannot write such a collection.
Offer only what is open
A day and a time typed as text can be anything, and a real scheduler offers only the slots it has.
When what the user may pick changes while the loop runs, give the choice input options_from
instead of options: each row of one of the loop’s collections is an option.
collections:
- name: slots
key: at
writes: none
fields: {at: string, slot: string, label: string}
- name: appointments
key: slot
writes: create
fields: {slot: string, email: string}
bind: {slot: slot, email: email}
inputs:
- id: slot
label: Which slot
kind: choice
required: true
options_from:
collection: slots
value: slot
label: label
exclude: {collection: appointments, field: slot}
The form reads the rows when the model asks for the input, in the order of the collection’s
key. value is the field that is the option’s id, and label the field the user reads; both
are string fields, and neither may be one a bind fills. exclude leaves out a row whose
value the other collection’s field already holds, so a booked slot is not offered to the next
user. Key the bookings on the slot with writes: create, as above, and each slot is booked
once: when two users were shown the same open slot, the second booking is refused, the model is
told the slot was just taken, and it asks for another on a new form. writes: none marks a
collection no run writes, such as the slots themselves; the default, upsert, replaces a record
of the same key. When no row is open,
the form is not shown: request_inputs tells the model so, and the model tells the user. The model
reads the picked row’s id in chosen and the label the form showed in chosen_labels, so a label
such as an order’s items and total is how the model learns them.
Your loop writes the rows it offers, or you add them yourself. A test_case answers with an id
that a row holds when the case runs, because publish cannot know which rows will be open.
Greet the user by name
A reply can use a value that the model never sees. The model writes {{user.<name>}}, and the
user’s own chat fills it in:
entry: >
Greet the visitor as "Hello {{user.name}}!" and ask what they need.
The names a reply may use are the loop’s input ids and the claims that identity.claims copies
from the user’s token, such as name. The model’s prompt lists them.
- Only the embedded chat fills a template, from what the user typed into the form in that window and from their own token. No server reads the value to fill it.
- Everyone else reads the template as written: the run’s record, the console and the Chat
bench, a tool argument (use
bindfor that), and an output. - A template fills in plain text only. Inside a link, an image or code it stays as written, so no reply can carry the value to another site.
- A value the chat does not hold reads as “there”, such as an input the user has not filled yet, or one they filled in another tab: “Hello there!”
A reply that names something the run cannot fill, or writes {{name}} without user., is sent
back to the model once, with the names it may use, so that it writes the reply again.
Every sealed input must reach something
Publish refuses a sealed input that no tool’s or collection’s bind names and no
{{user.<name>}} in entry uses. A value the user typed into it would reach nothing. The refusal
names the three fixes: bind it, greet with it, or remove it.
If the user does not answer
If the user declines the form, or nobody answers it in time, the loop reads that the user gave nothing, and the conversation goes on.
An email check
A loop can ask the user to prove an email address before it acts for them:
checks:
- kind: user_verification
method: email
The run waits before the first step that writes or sends anything, and before its outputs leave. The user receives a code by email, types it into the chat, and the run goes on. If the user’s identity already gives an address, the code goes there. A code that is not redeemed in time ends the run with the loop’s own words.
Neither the address nor the code reaches a model. A tool can receive the checked address through
bind, as verified_email:
tools:
- use: lookup_account
bind:
email: verified_email
Consent before an action
A loop can ask your web page to act for the user, through a client tool. Before an action, the chat shows the user a card that describes it, and the user approves or declines. See Embedding the Chat.