# Forms for agents

> How an agent fills a Cube form: read it as Markdown or JSON, submit over HTTP or MCP, use it in a browser with WebMCP, or hand a person a prefilled link.

A Cube form is one link. Anyone signed in can make one at `/forms/new` with a drag-and-drop builder (21 field types), or have their agent make it: [Make a form](https://ui.usecube.io/new.md) tells an agent how. A person fills the page. An agent fills the same link without opening a browser. What an agent sends waits for the form's owner to approve it; what a person sends counts at once.

The examples use the early-access form, [https://ui.usecube.io/f/early-access](https://ui.usecube.io/f/early-access).

## Read the form

Ask for the form's address without asking for HTML and you get Markdown: what the form is for, each field (name, type, required, description) and exactly how to submit it.

```sh
curl https://ui.usecube.io/f/early-access
```

| Address | What you get |
| --- | --- |
| `/f/early-access` | The page for a browser (an `Accept` that names `text/html`); Markdown for anything else. `Vary: Accept`. |
| `/f/early-access.md` | Always the Markdown. |
| `/f/early-access.json` | The JSON Schema of the answers. |

## Submit over HTTP

POST the answers as JSON to the same address. `agent` and `on_behalf_of` are optional; the owner approves faster when they can see who sent it.

```sh
curl -X POST https://ui.usecube.io/f/early-access \
  -H 'Content-Type: application/json' \
  -d '{"answers": {"name": "Jane Doe", "email": "jane@example.com", "use": "Vendor intake forms."}, "agent": {"name": "Claude", "model": "claude-sonnet-4-5"}, "on_behalf_of": "Jane Doe"}'
```

A success is `201 Created`:

```json
{ "id": "s_mgk2x1a4f3c9", "status": "working", "metadata": { "review": "pending", "message": "Received and waiting for the form owner's approval. It counts once approved." } }
```

`status` uses [A2A](https://a2a-protocol.org)'s task states: `working` while it waits for the owner's Do or Skip, then `completed` (Do) or `rejected` (Skip). The reply also has a `receipt_url`, your person's own view of what was sent.

A mistake is `400 Bad Request` with an `errors` list, each `{ "field", "message" }` written so a model can fix the answer and send again. `429 Too Many Requests` means wait the `Retry-After` seconds (ten submissions a minute from one address).

## Files

A form can ask for a file (PDF, Word `.doc` or `.docx`, PNG, JPEG, WebP or GIF). The bytes are checked, not the name or the type you claim. At most 10 MB a file and 50 MB a submission. If the file is on the person's computer: an agent that can run commands POSTs it as multipart, so the file never passes through the agent. An agent that cannot calls `prefill_link` and gives the person the link; the person attaches the file on the page and presses Submit. A file can go in three ways; `resume` stands for the file question's name.

1. **Multipart.** Send `multipart/form-data`: the other answers as JSON in the part `answers` (`agent` and `on_behalf_of` are parts too), each file as a part named after its question.

```sh
curl -X POST https://ui.usecube.io/f/<id>   -F 'answers={"name": "Jane Doe"}'   -F 'agent={"name": "Claude"}'   -F 'resume=@resume.pdf'
```

2. **Base64 in JSON.** The answer is an object. `name` and `base64` are required; `type` is optional. A `data:` prefix is accepted. Only for a small file the agent already holds (about 1 MB at most); do not read a file into the agent just to send it.

```json
{ "answers": { "resume": { "name": "resume.pdf", "type": "application/pdf", "base64": "JVBERi0xLjQK..." } } }
```

3. **A link the server fetches.** The answer is `{"url": "https://..."}` (`name` optional). Only `https`, no user or password in it, and the address must be public: private, loopback, link-local and other special addresses are refused. Redirects are followed only after the same check, at most 3. 10 seconds in all, at most 10 MB read.

```json
{ "answers": { "resume": { "url": "https://example.com/resume.pdf" } } }
```

A file that fails comes back as a `400` error on that field (too big, not a type the form takes, not base64, the link could not be fetched). Over 50 MB in all is `413`. Files are kept for the owner only; they are never served to the public.

## MCP

`https://ui.usecube.io/mcp` is a Streamable HTTP MCP server with five tools:

| Tool | What it does |
| --- | --- |
| `list_forms` | The site's own forms you can fill: id, title, what each is for. A form someone made is reached by its link, never listed. |
| `read_form(id)` | One form's fields, the JSON Schema of its answers, `link_for_a_person`, and, for a form with a file, `files` with the exact curl command. The id is the last part of its `/f/<id>` link. |
| `submit_form(id, answers, on_behalf_of?, agent?)` | Submits the answers; held for the owner like any agent's. A file answer is `{"name", "type", "base64"}` or `{"url"}`. |
| `prefill_link(id, answers)` | Sends nothing. Returns `url` (the form with the answers in the link), `filled` and `tell_your_person`; also `left_out`, `needs_fixing` and `still_required` when they apply. A file and a consent box are never in the link: the person attaches and ticks them on the page. |
| `draft_form(title, description?, fields)` | Makes a new form for the person, and stores nothing. Returns `url` (the builder with the form in the link, where the person signs in and presses Publish), `questions` and `tell_your_person`; also `fixed` and `defaults` when they apply. The question types and their settings are in [Make a form](https://ui.usecube.io/new.md). |

Add it to Claude Code:

```sh
claude mcp add --transport http cube https://ui.usecube.io/mcp
```

Or to any client that takes a URL:

```json
{ "mcpServers": { "cube": { "url": "https://ui.usecube.io/mcp" } } }
```

## WebMCP

In a browser with [WebMCP](https://webmachinelearning.github.io/webmcp/), the page's form is a tool. It carries the declarative attributes (`toolname`, `tooldescription`, and `toolparamdescription` on each field), so an in-browser agent can fill and submit it, and when the imperative API is there (`document.modelContext`) the page also registers a `submit_form` tool. Where neither exists, nothing changes.

## A prefilled link, for a chatbot

A chat assistant that cannot send requests or use MCP can still help: it gives its person a link with the answers in the fragment, each URL-encoded.

```text
https://ui.usecube.io/f/early-access#name=...&email=...&use=...&website=...
```

The fragment never reaches the server. The page fills the fields from it, removes it from the address bar so it is not shared by accident, and says "Filled in by your assistant. Check and submit." The person checks the answers and presses Submit. It counts at once, and the owner sees that an assistant drafted it.

## Who sent it

Every submission records how it came in (`page`, `prefill`, `http`, `mcp` or `webmcp`), the agent's name and model if it gave them, `on_behalf_of`, the user agent, a salted hash of the address, and any Web Bot Auth headers (`Signature-Agent`, `Signature-Input`) exactly as they arrived. The owner sees the agents' submissions as suggestions on the form's Answers sheet and presses Do or Skip on each. The owner can send answers on by email, webhook or Slack; see [Receiving answers](/docs/forms/receiving-answers).

## The early-access form

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `name` | text | yes | The person's full name. |
| `email` | email address | yes | The person's email address, where Cube will write to them. |
| `use` | long text | yes | What the person would use agent-native forms for, in their own words. |
| `website` | web address | no | The person's or their company's website, if they have one. |
