# Build a form, step by step

> The create, edit, save and publish workflow an assistant follows in trulyform, including revisions, retries, conflicts and form design advice.

## What you need to know

1. Find out what the form is for, who will fill it in and what happens with the answers.
2. `create_form` (or `get_form` for an existing form) returns the whole definition and its `draft_revision`.
3. Edit the complete definition. Save it with `save_form_draft`, passing the current revision as `base_revision` and a new UUIDv7 as `mutation_id`.
4. Read the `issues` in the reply. Fix them and save again until the list is empty.
5. Show the person what you built. Call `publish_form` only when they ask, with the revision they reviewed.

## Before you build

Ask, or work out from the conversation:

- **The goal.** What decision or task do the answers feed? Leave out every question that does not serve it.
- **The respondent.** A customer on a phone, a colleague, a guest? It sets the tone and the length.
- **What is required.** Require only the answers the person cannot do without.
- **What happens next.** The ending screen should tell respondents what to expect.

For a long or sensitive form, list the questions in the chat and let the person confirm them before you save.

## Create the form

Call `create_form` with a name. The reply's `form` holds the `id`, `draft_revision` (1 for a new form), the `definition` and its current `issues`.

A new form has a welcome screen, one untitled short text question, a thank-you ending and a white theme. Its `issues` ask for the missing question title. Replace that starter question with your own, or reuse its ID for your first question; nothing depends on it yet.

`create_form` is not idempotent. If the reply is lost, call `list_forms` and look for the form before you create another one.

To change an existing form instead, find it with `list_forms` and read it with `get_form`.

## Edit the definition

- **Start from the definition you just read.** Keep every top-level key, including `schemaVersion`, `mediaDefaults`, `theme` and `settings`, and keep every existing ID. See [The form definition](https://trulyform.com/docs/form-definition.md).
- **Write the questions in `fields`,** in the order respondents will see them. Start each one from its template in [Question types](https://trulyform.com/docs/question-types.md) or from `list_field_types`, then give it a title.
- **Give every new question, choice, row, rule and calculation a fresh ID:** a lowercase UUIDv7, unique within the form. If you cannot generate one, call `list_field_types` with `id_count` and use the IDs it returns. Details are in [Identifiers](https://trulyform.com/docs/form-definition.md#identifiers).
- **Titles and descriptions are rich text,** not plain strings. See [Rich text](https://trulyform.com/docs/form-definition.md#rich-text).
- **To change a question's type,** replace the question with a new one that has a new ID, then update any rules, calculations or piped answers that pointed at the old ID.
- **Label the last question's button** "Submit" or similar with its `nextButton` setting. There is no separate submit button.

A complete, publishable definition:

**Contact request.** The smallest useful form: three required questions, and an ending that greets the person by the name they typed.

It shows:

- required questions
- placeholder and maxLength settings
- an answer piped into the ending

```json title="Contact request definition"
{
    "schemaVersion": 2,
    "mediaDefaults": {
        "assetId": null,
        "layout": "above",
        "altText": ""
    },
    "welcome": {
        "enabled": true,
        "title": {"type":"doc","content":[{"type":"paragraph","content":[{"type":"text","text":"Get in touch"}]}]},
        "description": {"type":"doc","content":[{"type":"paragraph","content":[{"type":"text","text":"Three quick questions. We reply within one working day."}]}]},
        "button": "Start"
    },
    "ending": {
        "title": {"type":"doc","content":[{"type":"paragraph","content":[{"type":"text","text":"Thanks, "},{"type":"answerToken","attrs":{"source":"answer","ref":"01990000-0000-7000-8000-000000000001","fallback":"there"}},{"type":"text","text":"!"}]}]},
        "description": {"type":"doc","content":[{"type":"paragraph","content":[{"type":"text","text":"We will reply to your email within one working day."}]}]}
    },
    "fields": [
        {
            "id": "01990000-0000-7000-8000-000000000001",
            "kind": "short_text",
            "title": {"type":"doc","content":[{"type":"paragraph","content":[{"type":"text","text":"What is your name?"}]}]},
            "description": {"type":"doc","content":[{"type":"paragraph","content":[]}]},
            "required": true,
            "options": [],
            "rows": [],
            "settings": {"nextButton":"Next","placeholder":"Jane Smith"},
            "media": {"inherit":true,"assetId":null,"layout":"above"}
        },
        {
            "id": "01990000-0000-7000-8000-000000000002",
            "kind": "email",
            "title": {"type":"doc","content":[{"type":"paragraph","content":[{"type":"text","text":"What is your email address?"}]}]},
            "description": {"type":"doc","content":[{"type":"paragraph","content":[{"type":"text","text":"We only use it to reply to this message."}]}]},
            "required": true,
            "options": [],
            "rows": [],
            "settings": {"nextButton":"Next"},
            "media": {"inherit":true,"assetId":null,"layout":"above"}
        },
        {
            "id": "01990000-0000-7000-8000-000000000003",
            "kind": "long_text",
            "title": {"type":"doc","content":[{"type":"paragraph","content":[{"type":"text","text":"How can we help?"}]}]},
            "description": {"type":"doc","content":[{"type":"paragraph","content":[]}]},
            "required": true,
            "options": [],
            "rows": [],
            "settings": {"nextButton":"Send message","maxLength":2000},
            "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": []
}
```

## Save the draft

Call `save_form_draft` with:

| Parameter | Value |
| --- | --- |
| `form_id` | The form's ID. |
| `base_revision` | The `draft_revision` you last read, or the `revision` your last save returned. |
| `mutation_id` | A new UUIDv7 for every save. |
| `name` | The full form name. An empty string saves it as "Untitled form". |
| `definition` | The complete definition, including the parts you did not change. A partial object is rejected. |

The reply holds the new `revision`, `issues`, `saved_at` and the `published_version_id`. Use `revision` as the `base_revision` of your next save.

If the network fails and you do not know whether a save went through, send the identical request again with the same `mutation_id`. trulyform returns the original result instead of saving twice. Reusing a `mutation_id` with different content is rejected.

### When a save is rejected

- **`validation_failed` with `errors`:** the definition has the wrong shape, such as an unknown key, an invalid ID or a plain-string title. Nothing was saved. Fix the paths named in `errors` and save again with the same `base_revision` and a new `mutation_id`. [Limits and errors](https://trulyform.com/docs/validation.md#save-errors) explains the messages.
- **`draft_conflict`:** a newer draft exists, perhaps because the person is editing the form in trulyform. Call `get_form`, apply your change to the definition it returns, and save with its `draft_revision`. Never overwrite the newer draft with an old copy.

## Fix the issues

A save succeeds even when the form is not ready to publish. The reply's `issues` list what still blocks publication:

```json title="One issue"
{
    "nodeId": "01990000-0000-7000-8000-000000000001",
    "path": "title",
    "message": "Write your question.",
    "severity": "error"
}
```

`nodeId` is the ID of the question, rule or calculation with the problem, or `form`, `theme`, `welcome` or `ending`. Each issue also has a `docs` field naming the section that explains it; pass it to `read_docs`. Fix every issue and save again. [Publish issues](https://trulyform.com/docs/validation.md#publish-issues) lists the common ones.

## Review with the person

Before publishing, summarize the form in plain words: the questions in order, which ones are required, and what the logic does ("If they choose In person, we ask about dietary needs"). Give the person the form's `editor_url` from `create_form`, `get_form` or `list_forms`: it opens the form in the trulyform editor, where they can preview it exactly as respondents will see it and make changes themselves.

## Publish

Call `publish_form` with the `form_id` and the `revision` the person reviewed, and only after they ask you to publish. The reply contains the `public_url` to share.

- If the draft changed since that revision, publishing fails with `draft_conflict`. Show the person the newer draft first.
- If the draft still has issues, publishing fails with `validation_failed` and the list of `issues`.
- Publishing identical content again does not create a new version.
- Responses already collected keep the version they were answered on.
- A closed form stays closed. The person reopens it in trulyform.

## Design a good form

- **Keep it short.** Most forms need fewer than ten questions. Each extra question costs answers.
- **One idea per question,** in plain words. Put help text in the description, not the title.
- **Match the type to the answer.** Use `email` for email addresses, `dropdown` for long lists such as countries, `single_choice` for a handful of options, `opinion_scale` or `nps` for ratings, and `consent` for agreements. A `number` answer may have decimals, so ask for whole counts, such as guests, with a choice question.
- **Use logic instead of "If yes…" questions,** so people only see what applies to them.
- **Use the welcome screen** to say what the form is for and how long it takes. Turn it off for very short forms.
- **Use the ending** to thank people and say what happens next. Piping in their name makes it personal.


---

Previous: [Tools](https://trulyform.com/docs/tools.md) · Next: [Form definition](https://trulyform.com/docs/form-definition.md)

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