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}
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 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 bind for 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

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.