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. A loop that says ends: {finish: false} never offers finish: it
keeps answering follow-ups, and the conversation ends when the person goes quiet. A finish in
the same reply as a proposal the platform refused for its contents (a loop, page, tool, agent or claim) does not end the run: the model is told the proposal
was refused, and it fixes the proposal before it finishes.
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. A question that ends the model’s last reply before finish, such as “Is there anything
else I can help with?”, is not shown either, because nobody can answer it after the run ends. When
that question is all the reply says, as in “Hello there, how can I help?”, the run does not end:
the user reads the question, the run waits for their answer, and the run’s journal records that its
finish was overruled.
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. An option that is itself a question (“What’s your role?”) is
refused before the user sees a card, and the model is told to ask one question with no options;
options that are all questions, each ending in ”?”, still get no card if they arrive: the user
answers them in the box. Otherwise 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
type an answer of their
own below the card. Skip sends “I’d rather not answer.” as the reply, so the loop goes on
instead of waiting: write the loop’s instructions so that it carries on without that answer. The
model never asks the user to type an exact phrase.
How long a conversation waits
A conversation waits for the person, and it ends when they stay quiet:
- After a reply that is only text, it waits 10 minutes, then ends as completed: the reply was
the answer, and the loop’s outputs run. The person then reads the loop’s
completedline, and the console’s feed says Ended after 10 minutes with no message from the person. - With a question open from
ask_user, it waits the platform’s limit for a question, then ends as lapsed, and the person reads the loop’slapsedline.
A loop sets both waits with ends: {idle_minutes: N}, from 1 to 60 minutes. Where the platform’s
own limit for a question is shorter, the question waits that limit instead.
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 form that asks for part of a record also shows every required input the record is filled
from that the user has not given on a form yet, even one they already said in the chat: only the
form fills a record. The platform adds those inputs to the form itself, and tells the model which
ones it added.
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.
When the model leaves a choice or choices input empty, pio fills it itself if the user’s own
messages in this conversation name exactly one of its option ids as a whole word, such as 3307
in “I never got order 3307”. If they name two or more, the input stays empty for the user to pick.
A value the model sent always wins, a boolean or sealed input is never filled this way, and the
model is told which inputs pio filled.
After a save that a form filled, the user reads your loop’s completed line. The model never
sees a typed value, and what the user said in the chat is not in the record unless a field holds
it: a phone number mentioned in a message, with the form’s phone field left empty, is not on file.
So before the form, the model never promises to note something the user said: it asks them to
add it on the form, under the field’s label. Under its buttons, a form with typed fields says
“Send passes on what you entered. Don’t send passes on nothing, and the chat goes on.”, and it
says who reads what: “What you enter goes to this site; the assistant never reads it.” And the first reply after the save shows end_messages.completed in place of the model’s words
(see The last line of every run).
Write it as what the user should read once their details are saved, such as “Thanks, your
booking request is saved.”
A refusal is an answer. The user can press Don’t send, 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 ignored, the user’s value is written,
and the model is told which fields it should leave out. 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.
- Nobody else reads the value. The console and My conversations show which value the
user’s chat filled, such as “[the person’s name]”. The run’s record, a tool argument (use
bindfor that) and an output keep the template as written. - 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.