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 completed line, 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’s lapsed line.

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}
KindWhat the user givesWhat the model reads
email, textTyped textOnly whether it was given
choiceOne of options, or of a collection’s rows with options_fromThe chosen option’s id, unless the input says sealed: true
choicesAny of options, between min and maxThe chosen ids, unless sealed
booleanOne checkboxTrue 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 bind for 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

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.