> ## Documentation Index
> Fetch the complete documentation index at: https://ago.mintlify.site/llms.txt
> Use this file to discover all available pages before exploring further.

# Conversational Forms

> Let an AGO agent fill a form through conversation, with the UI updating live

A form collector lets an AGO agent fill a form by talking to the user. You define the form's fields once. The SDK gives the agent a function to record values as the conversation goes, keeps a live store you can render, and tells the agent which required fields are still missing so it knows what to ask next.

You define the fields in one place. From that single schema the SDK derives:

* an observable **store** holding the collected `values` and whether the form was `submitted` — its completeness (`missing`, `complete`) is computed on read with `deriveFormStatus`, never stored
* an `update_<name>` function the agent calls to record values (it can call it many times, with any subset of fields)
* a context entry sent with every message so the agent always knows the current values and what's left
* an optional `submit_<name>` function that sends the completed form

## Create a Collector

```javascript theme={null}
import { AgoClient, createFormCollector, deriveFormStatus } from "@useago/sdk";

const client = new AgoClient({ baseUrl: "https://YOUR-DOMAIN.useago.com" });

// Keep the schema in one place so you can also use it to derive completeness.
const schema = {
  type: "object",
  properties: {
    product: { type: "string", description: "Product name" },
    quantity: { type: "number", description: "Number of units" },
    address: { type: "string", description: "Shipping address" },
  },
  required: ["product", "quantity", "address"],
};

const order = createFormCollector({
  name: "order",
  description: "The order the user wants to place.",
  schema,
  submit: "/api/orders", // POST the values to this URL
});

// Register the functions + context on the client.
order.install(client);

// Render whenever the form changes. The store holds { values, submitted };
// derive what's missing / whether it's complete on read.
order.store.subscribe((state) => {
  const { missing, complete } = deriveFormStatus(schema, state.values);
  console.log(state.values, missing, complete);
});
```

The `name` becomes the function names (`update_order`, `submit_order`), so it must contain only letters, numbers, underscores, or hyphens, and be at most 40 characters.

## Define the form in the backend

Instead of writing the schema in your page, you can store it once in AGO and fetch it by name. The form then has a single source of truth: change a field in the admin interface and every page picks it up on next load, with no redeploy.

Create the definition in the admin interface (its name, description, schema, and submit target), then reference it by name:

```javascript theme={null}
import { AgoClient, loadFormCollector } from "@useago/sdk";

const client = new AgoClient({ baseUrl: "https://YOUR-DOMAIN.useago.com" });

// Fetch the definition (description + schema + submit) stored under "order".
const order = await loadFormCollector(client, { name: "order" });
order.install(client);
```

Any option you pass overrides the stored one — useful for a browser-only submit `handler`, which can't be stored on the server:

```javascript theme={null}
const order = await loadFormCollector(client, {
  name: "order",
  submit: { via: "client", handler: async (values) => myApi.createOrder(values) },
});
```

A runnable widget version (the schema lives in the backend, the page only references it by name) is in the SDK repository under `examples/simple-html/credit-form-backend.html`.

## React

`useFormCollector` registers the collector on mount, removes it on unmount, and returns the live state.

```tsx theme={null}
import { useFormCollector } from "@useago/sdk/react";

function OrderForm() {
  const order = useFormCollector({
    name: "order",
    description: "The order the user wants to place.",
    schema: {
      type: "object",
      properties: {
        product: { type: "string", description: "Product name" },
        quantity: { type: "number", description: "Number of units" },
        address: { type: "string", description: "Shipping address" },
      },
      required: ["product", "quantity", "address"],
    },
    submit: "/api/orders",
  });

  return (
    <div>
      <p>Product: {order.state.values.product ?? "—"}</p>
      <p>Quantity: {order.state.values.quantity ?? "—"}</p>
      <p>Address: {order.state.values.address ?? "—"}</p>
      {order.state.complete ? (
        <button onClick={() => order.submit()}>Place order</button>
      ) : (
        <p>Still needed: {order.state.missing.join(", ")}</p>
      )}
    </div>
  );
}
```

Keep the `schema` and `submit` object stable across renders (declare them outside the component or memoize them) so an inline `handler` isn't captured stale.

To use a backend-defined form instead, pass only its `name` — the hook fetches the definition and exposes `loading` while it does:

```tsx theme={null}
function OrderForm() {
  const order = useFormCollector({ name: "order" });

  if (order.loading) return <p>Loading…</p>;
  return order.state.complete ? (
    <button onClick={() => order.submit()}>Place order</button>
  ) : (
    <p>Still needed: {order.state.missing.join(", ")}</p>
  );
}
```

## State

The store holds only the canonical state:

| Field       | Description                   |
| ----------- | ----------------------------- |
| `values`    | The fields collected so far   |
| `submitted` | `true` once a submit succeeds |

Completeness is **derived** from the values, not stored. Compute it with `deriveFormStatus(schema, values)` — or, in React, read it straight off `order.state`, which already includes it:

| Field      | Description                               |
| ---------- | ----------------------------------------- |
| `missing`  | Required field names that are still empty |
| `complete` | `true` when no required field is missing  |

## Restoring state after a reload

When a visitor reloads the page and you reopen their previous conversation, the collector rebuilds itself from the values the agent already recorded. Loading a conversation replays its `update_<name>` and `submit_<name>` calls onto the store, so the collected `values` and the `submitted` flag come back exactly as they were (and `missing`/`complete` derive from them), and the visitor doesn't have to start over.

This happens on its own once the collector is installed (with `install` or `useFormCollector`): loading a conversation restores any matching form. Loading a conversation that never used the form leaves it at its initial values.

If you build the conversation loading yourself, call `order.hydrate(toolCalls)` with the tool calls of the loaded messages to restore the state manually.

## Submitting

Choose how the completed form is sent with the `submit` option.

<Tabs>
  <Tab title="From the browser">
    The browser sends the values to a URL you control, using the page's existing session. Use this when the endpoint is your own API and needs no server-only secret.

    ```javascript theme={null}
    submit: "/api/orders"                          // shorthand: POST the values to this URL
    submit: { via: "client", url: "/api/orders" }  // the same, written out
    submit: { via: "client", handler: async (values) => myApi.createOrder(values) } // custom logic
    ```
  </Tab>

  <Tab title="From the server">
    The browser sends the values to AGO, which forwards them to the destination configured on the form's server-side definition (its name, URL, and any secret stay on the server). Use this when the destination needs a secret or sits behind authentication — the secret never reaches the browser. This requires a server-side form definition with a configured destination.

    ```javascript theme={null}
    submit: { via: "backend" }
    ```

    The destination is looked up by the form's **name** on the server — the browser never holds it. Configure it on the form definition in the admin interface, in one of two ways:

    * **Webhook URL** — the simplest setup. Enter the URL in the form builder and AGO POSTs the completed values to it as JSON. To add request headers, use the advanced submit config instead: `{ "via": "backend", "url": "…", "headers": { … } }`.
    * **Tool destination** — `{ "via": "backend", "destination": "<tool name>" }` reuses the URL, headers, and secret of an existing HTTP tool. Pick this when the endpoint needs a stored secret.
  </Tab>
</Tabs>

`submit_<name>` only submits once every required field is present. If fields are still missing it returns them instead, so the agent asks for the rest before trying again. You can also submit from your own UI by calling `order.submit()`.

## Submission history

Every form submitted through the server (`{ via: "backend" }`) is kept in a history you can review in the admin interface. Open **Form Collectors** and click **Form Submissions** to see, for each submission:

* the date and time it was sent
* the form it belongs to
* whether it completed or failed
* the user who submitted it
* a preview of the submitted values

Click a row to open its detail page, with the full submitted data and the failure reason if it failed. Filter the list by form, status, or date range. Submissions handled entirely in the browser (`{ via: "client" }`) never reach the server, so they don't appear here.

## Complete Example

A contact-intake form in plain JavaScript. The agent collects the prospect's details through the conversation, the summary updates live, and the form submits once it's complete.

```javascript theme={null}
import { AgoClient, createFormCollector, deriveFormStatus } from "@useago/sdk";

const client = new AgoClient({
  baseUrl: "https://YOUR-DOMAIN.useago.com",
  agent: "YOUR-AGENT-ID", // the agent that handles this conversation (id or slug)
});

const schema = {
  type: "object",
  properties: {
    full_name: { type: "string", description: "Contact's full name" },
    work_email: { type: "string", description: "Work email address" },
    company: { type: "string", description: "Company name" },
    reason: {
      type: "string",
      description: "Reason for getting in touch",
      enum: ["Demo", "Pricing", "Support", "Partnership", "Other"],
    },
    message: { type: "string", description: "Anything else (optional)" },
  },
  required: ["full_name", "work_email", "company", "reason"],
};

const contact = createFormCollector({
  name: "contact",
  description: "Contact details of a business prospect who wants to be called back.",
  schema,
  submit: "/api/leads", // POST the values to your API
});

contact.install(client);

// Render a live summary as the agent fills the form. The store holds the values;
// derive completeness on read.
contact.store.subscribe((state) => {
  const { complete, missing } = deriveFormStatus(schema, state.values);
  document.getElementById("summary").textContent = complete
    ? "Ready to send"
    : `Still needed: ${missing.join(", ")}`;
});

// The agent records values as the user chats, and calls submit_contact when complete.
const reply = await client.sendMessage(
  "Hi, I'd like a demo for my company Acme — I'm Dana, dana@acme.com"
);
console.log(reply.content);
```

A runnable version (a self-contained HTML page) lives in the SDK repository under `examples/simple-html/contact-form.html`.

## Related Documentation

* [Client Functions](/features/client-functions) — the lower-level browser functions this builds on
* [JavaScript SDK](/features/sdk-integration) — full SDK setup and usage
* [SDK API Reference](/api/sdk-api) — REST API endpoints
