Skip to content
AI Assistant

Guides

Forms and sign-in inside the chat

Ask for missing details as a card with real fields instead of a list to type, and let visitors sign in to your site without leaving the conversation.

On this page

AI Assistant can ask for what it is missing as a form card — labelled fields and one button, inside the conversation — instead of writing "I'll need: 1. your name 2. your phone number". On a phone that difference is the whole experience: the right keyboard per field, the visitor's own autofill, and nothing to remember. The same card lets a visitor authenticate on your site while the conversation keeps going.

Your tool answers with fields The chat draws native controls The visitor fills it in, submits the values go back to your tool

1. Decide which action needs details

Any action the visitor asks for that you cannot complete from what they said: a booking that needs a name and a phone number, an order lookup that needs the order number, a return that needs a reason. Write down the fields, their types, and which are required.

2. Answer with a card instead of a result

A tool asks for a form by returning one, in place of its answer:

javascript
BusymateAI.registerPageTools([
  {
    name: "book_table",
    description: "Book a table. Call with no arguments to show the booking form.",
    inputSchema: { type: "object", properties: {} },
    annotations: { readOnlyHint: false },
    async execute(input) {
      // No details yet? Ask for them as a CARD, not as a sentence.
      if (!input?.name) {
        return {
          $bmForm: 1,
          title: "Book a table",
          description: "Two minutes and you're done.",
          fields: [
            { name: "name",  label: "Your name", type: "text", required: true, autocomplete: "name" },
            { name: "phone", label: "Phone",     type: "tel",  required: true, autocomplete: "tel" },
            { name: "party", label: "People",    type: "number", min: 1, max: 12 },
            { name: "time",  label: "Time",      type: "time" },
          ],
          submit: { label: "Book it", tool: "book_table" },
          cancel: { label: "Not now" },
        };
      }
      // Submitted: the same tool, now with the visitor's values.
      const response = await fetch("/api/bookings", {
        method: "POST",
        headers: { "content-type": "application/json" },
        body: JSON.stringify(input),
      });
      return response.ok ? await response.json() : { error: "could_not_book" };
    },
  },
]);

Answer with $bmForm: 1 and a fields array. submit.tool names where the values go — usually the same tool, now with arguments. A field type is text, textarea, tel, email, number, date, time, select or password, with optional required, placeholder, help, autocomplete, min/max, and options for a select. Twelve fields at most.

3. Or declare it from your MCP server

A connected MCP server needs no page: attach the identical object at _meta.ui.form on the tool result. That is the same declaration channel as _meta.ui.resourceUri — one says "mount my document", the other "draw these fields" — and both render on the visitor's surface, the embedded widget and your hosted chat, not only in the Console.

If you write neither, the assistant still shows a card: it asks for the details it is missing with the platform's own form rather than a list. Your own tool is better, because it knows its fields and completes the action as well as collecting it.

That same card appears when an argument was never the visitor's to give. Before any tool runs, the platform checks each string argument against the visitor's own words: a value they typed goes through, a value that looks generated (customer@example.com, John Doe, 123-456-7890, 12345) or a required value that appears nowhere in what they said is not sent — the field is asked for instead. So your tool is never handed an invented email to write into a real record, and you do not need a guard of your own for it. Optional arguments the assistant is meant to choose, values from an enum, and dates it worked out from "tomorrow" are untouched.

4. Sign a visitor in without leaving the chat

If what they asked for needs their account, the honest answer is not "go and find the Sign in button". Sign-in is the same contract under a standard name: register sign_in — a page tool or a tool on your connected server — and return a form card from it. sign_up and sign_out are its optional companions.

javascript
BusymateAI.registerPageTools([
  {
    name: "sign_in",
    description:
      "Show the sign-in form in the chat when something needs the visitor's account.",
    inputSchema: { type: "object", properties: {} },
    // Showing a form changes nothing, so no confirmation stands in front of it.
    annotations: { readOnlyHint: true },
    execute: () => ({
      $bmForm: 1,
      title: "Sign in",
      description: "You'll stay right here in this conversation.",
      fields: [
        { name: "email",    label: "Email",    type: "email",    required: true, autocomplete: "username" },
        { name: "password", label: "Password", type: "password", required: true, autocomplete: "current-password" },
      ],
      submit: { label: "Sign in", tool: "sign_in_submit" },
      cancel: { label: "Not now" },
    }),
  },
  {
    name: "sign_in_submit",
    description: "Complete the sign-in the card collected.",
    inputSchema: { type: "object", properties: {} },
    annotations: { readOnlyHint: false },
    async execute({ email, password }) {
      const response = await fetch("/api/login", {
        method: "POST",
        credentials: "same-origin",
        headers: { "content-type": "application/json", "x-csrf-token": window.CSRF_TOKEN },
        body: JSON.stringify({ email, password }),
      });
      // `signedIn: true` is the ONE thing the chat reads. On it, the widget
      // re-asks your page for an identity token and the SAME conversation
      // continues signed in — no reload, nothing retyped.
      if (!response.ok) return { signedIn: false, error: "invalid_credentials" };
      return { signedIn: true };
    },
  },
]);

Your card is drawn as you wrote it, and it may have as many steps as your login does: a submit that answers with another form replaces the card with the next step. Let your users sign in from the chat walks through the passwordless, two-step version.

A password field is accepted only on a card that submits back into your own page, so its value goes from the input straight to your execute — never to the assistant, the transcript or a log, and the settled card shows ••••• rather than the value or its length. Send your CSRF token and rate-limit the endpoint as you already do: this is a form on your page.

Register none of it and there is no sign-in card: the assistant falls back to your real sign-in link rather than standing something of ours in for your login.

5. Let the conversation carry on

When your sign-in tool answers { signedIn: true }, the widget asks your page for a fresh identity token — the getIdentity handoff in Recognize signed-in customers — and re-mints the session in place. Nothing reloads, the visitor retypes nothing, and what they asked for before signing in is answered straight after. The token is verified against your registered JWKS exactly as at launch, so signing in here grants nothing a normal sign-in would not.

Verify

  1. Ask for something your tool needs details for. A card appears with your fields — not a numbered list in a message.
  2. On a phone, tap the phone field: the dial pad opens. Tap the email field: the @ row does.
  3. Leave a required field empty and submit: it says so and nothing is sent.
  4. Submit the form. Your tool runs with the values and the assistant answers from what it returned.
  5. Signed out, ask for something needing an account: the sign-in card appears in the chat, not a link away.
  6. Sign in from the card. The same conversation continues, now identified, and your original request is answered without repeating it.
  7. Open the tool call's details: the password is •••••, never the value.

Next

Questions

Does the assistant see what the visitor types?

Only for an ordinary form, where the values become a visible message — which is what a booking or an order number should be. A card that submits into your own page never shows its values to the assistant, and a password field is only accepted on that kind of card.

What if my tool returns something that is not a form?

Nothing changes. The card only renders for a result that positively declares $bmForm: 1 with at least one usable field; everything else shows as it always has.

Can I use it without WebMCP page tools?

Yes. A connected MCP server declares the same object at _meta.ui.form, and the platform's own card needs nothing from you at all.

A link ends the conversation. The visitor signs in somewhere else, comes back to a chat that may have moved on, and retypes their question. The card keeps the thread.