# The form definition

> The JSON an assistant edits to build a trulyform form: top-level keys, identifiers, rich text, welcome and ending screens, and form settings.

## What you need to know

- A form is one JSON object with the top-level keys below. Every save sends all of them.
- Every question, choice, row, rule and calculation has an `id`: a lowercase UUIDv7, unique within the form.
- Titles and descriptions are small rich-text documents, not strings. Choice, row and button labels are plain strings.
- Images and logos are uploaded in trulyform, not over MCP. Keep the asset IDs `get_form` returns, and use `null` for new questions. See [Design and theme](https://trulyform.com/docs/design.md).
- A key the format does not define makes the save fail, wherever it appears. A known setting that a question type does not use is ignored.

## Top-level keys

| Key | Type | What it holds |
| --- | --- | --- |
| `schemaVersion` | 2 | Always 2 for new and edited forms. Keep the value get_form returns. |
| `mediaDefaults` | object | The form-wide image: {assetId, layout, altText}. Keep it as get_form returns it. |
| `welcome` | object | The first screen: {enabled, title, description, button}. title and description are rich text; button is the start button label. |
| `ending` | object | The screen after the last question: {title, description}, both rich text. It can show piped answers. |
| `fields` | list | The questions, in the order respondents see them. |
| `theme` | object | Colors, font, corner radius and spacing. See the theme fields below. |
| `settings` | object | Form-wide behavior: {allowReanswer, hiddenFields, fillLimit}. |
| `rules` | list | Logic rules that show, hide, require or skip questions. |
| `calculations` | list | Named numeric results computed from answers. |

A new form, as `create_form` returns it:

```json title="A new form, as create_form returns it"
{
    "schemaVersion": 2,
    "mediaDefaults": {
        "assetId": null,
        "layout": "above",
        "altText": ""
    },
    "welcome": {
        "enabled": true,
        "title": {"type":"doc","content":[{"type":"paragraph","content":[{"type":"text","text":"Welcome!"}]}]},
        "description": {"type":"doc","content":[{"type":"paragraph","content":[{"type":"text","text":"We would love to hear from you."}]}]},
        "button": "Start"
    },
    "ending": {
        "title": {"type":"doc","content":[{"type":"paragraph","content":[{"type":"text","text":"Thank you!"}]}]},
        "description": {"type":"doc","content":[{"type":"paragraph","content":[{"type":"text","text":"Your response has been received."}]}]}
    },
    "fields": [
        {
            "id": "01990000-0000-7000-8000-000000000001",
            "kind": "short_text",
            "title": {"type":"doc","content":[{"type":"paragraph","content":[]}]},
            "description": {"type":"doc","content":[{"type":"paragraph","content":[]}]},
            "required": false,
            "options": [],
            "rows": [],
            "settings": {"nextButton":"Next"},
            "media": {"inherit":true,"assetId":null,"layout":"above"}
        }
    ],
    "theme": {
        "preset": "paper",
        "background": "#ffffff",
        "foreground": "#111827",
        "primary": "#2563eb",
        "buttonText": "#ffffff",
        "font": "Inter",
        "radius": 12,
        "density": "comfortable",
        "logoAssetId": null,
        "backgroundAssetId": null
    },
    "settings": {
        "allowReanswer": true,
        "hiddenFields": [],
        "fillLimit": null
    },
    "rules": [],
    "calculations": []
}
```

## Identifiers

- **Format:** 32 lowercase hexadecimal digits in groups of 8-4-4-4-12. The third group starts with `7` (version 7) and the fourth with `8`, `9`, `a` or `b`. For example `01990000-0000-7000-8000-000000000001`.
- **Unique within the form,** across all questions, choices, rows, rules and calculations together. Two choices in different questions cannot share an ID.
- **Keep existing IDs** when you edit. Responses, logic rules and piped answers refer to them; a question with a new ID is a new question.
- **New IDs:** generate a UUIDv7, or call `list_field_types` with `id_count` to receive fresh ones. The timestamp part of a UUIDv7 is not checked. The IDs in these docs are readable placeholders; make sure none of them collides with an ID already in the form.
- The `mutation_id` of `save_form_draft` is a UUIDv7 too, new for every save.

## Questions

Each entry in `fields` is one question:

| Key | Type | Notes |
| --- | --- | --- |
| `id` | UUIDv7 | Unique across every question, choice, row, rule and calculation in the form. |
| `kind` | string | One of the question types. |
| `title` | rich text | The question. Required before publishing. |
| `description` | rich text | Optional help text under the question. Use an empty paragraph when there is none. |
| `required` | boolean | Whether respondents must answer. Always false for statements. |
| `options` | list | Choices as {id, label}, with an optional decimal-string score. Empty for kinds without choices. |
| `rows` | list | Matrix rows as {id, label}. Empty for every other kind. |
| `settings` | object | Kind-specific settings. Include nextButton; leave out settings the kind does not use. |
| `media` | object | {inherit, assetId, layout}. Keep {"inherit": true, "assetId": null, "layout": "above"} unless get_form shows an image. |

What each kind accepts is in [Question types](https://trulyform.com/docs/question-types.md).

## Rich text

Question titles and descriptions, and the titles and descriptions of the welcome and ending screens, are rich-text documents: a `doc` holding paragraphs, which hold text.

```json title="A title with one bold word"
{
    "type": "doc",
    "content": [
        {
            "type": "paragraph",
            "content": [
                { "type": "text", "text": "What is your " },
                { "type": "text", "text": "work", "marks": [{ "type": "bold" }] },
                { "type": "text", "text": " email address?" }
            ]
        }
    ]
}
```

An empty description is `{"type": "doc", "content": [{"type": "paragraph", "content": []}]}`.

| Node | Use |
| --- | --- |
| `doc` | The root. Its content is a list of paragraphs. |
| `paragraph` | A paragraph. Its content holds text, hardBreak and answerToken nodes. An empty paragraph has "content": []. |
| `text` | A run of text: {"type": "text", "text": "…"}. Optional "marks" style it. |
| `hardBreak` | A line break inside a paragraph: {"type": "hardBreak"}. |
| `answerToken` | Pipes in an earlier answer or a calculation: {"type": "answerToken", "attrs": {"source": "answer", "ref": "<question id>", "fallback": "there"}}. |

Text marks: `bold`, `italic`, `underline`, `strike`, written as "marks": [{"type": "bold"}]. Up to 4 marks per text node and 10,000 characters per text node.

Use several paragraphs for several lines of text. There is no link mark: to point people to a page, write its address as text, for example in the description. Do not add a required marker such as an asterisk: trulyform shows one for required questions.

### Piped answers

An `answerToken` node shows an earlier answer or a calculation result inside text, with a `fallback` for when there is nothing to show. [Piped answers](https://trulyform.com/docs/calculations-and-piping.md#piped-answers) explains where tokens may appear.

## Welcome and ending

- `welcome`: `{enabled, title, description, button}`. Set `enabled` to false to start with the first question. When it is enabled, `button` must not be blank.
- `ending`: `{title, description}`, shown after the last question.

## Form settings

| Setting | Type | Notes |
| --- | --- | --- |
| `allowReanswer` | boolean | Lets respondents go back and change earlier answers. |
| `hiddenFields` | list of strings | URL parameter names to capture with each response, such as "source". Each starts with a letter and uses only letters, digits and underscores, up to 64 characters. They are stored with the response; they cannot be used in logic or piping, and get_response does not return them. |
| `fillLimit` | integer or null | Stop accepting responses after this many. null means no limit. |

## Theme and images

The `theme`, `mediaDefaults` and each question's `media` are described in [Design and theme](https://trulyform.com/docs/design.md). Over MCP, keep asset IDs exactly as `get_form` returns them and give new questions `{"inherit": true, "assetId": null, "layout": "above"}`.


---

Previous: [Building a form](https://trulyform.com/docs/building-a-form.md) · Next: [Question types](https://trulyform.com/docs/question-types.md)

All pages: https://trulyform.com/llms.txt. Connected over MCP? Call read_docs with a page name instead.
