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_formreturns, and usenullfor new questions. See Design and theme. - 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:
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": []
}
Select the text to copy it.
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 with8,9,aorb. For example01990000-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_typeswithid_countto 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_idofsave_form_draftis 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.
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.
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?" }
]
}
]
}
Select the text to copy it.
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 explains where tokens may appear.
Welcome and ending
welcome:{enabled, title, description, button}. Setenabledto false to start with the first question. When it is enabled,buttonmust 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. Over MCP, keep asset IDs exactly as get_form returns them and give new questions {"inherit": true, "assetId": null, "layout": "above"}.