The Form Revisions API gives a project admin a draft/publish workflow for form definitions. Once revisions are enabled on a form, every published change creates a new numbered revision; admins can work on a draft that does not yet affect submissions, publish that draft to create the next revision, and retrieve any historical revision by its version number or revision ID. This skill covers enabling revisions, drafting, publishing, listing revisions, and fetching a specific revision. For non-revisioned form CRUD, see project-forms.md.
All endpoints below are rooted at ${FORMIO_PROJECT_URL} — the project endpoint, equivalent to {{baseUrl}}/{{projectName}} in Postman.
Every request to these endpoints MUST include an x-jwt-token header holding the user JWT issued by the MCP server's browser-based portal-login flow. The MCP server attaches this header automatically via formioFetch; external clients must obtain the JWT through the same portal-login flow. Do not use any other authentication mechanism with these endpoints.
- Every form created via
form_createdefaults torevisions: 'original'on a licensed deployment. The caller may override by passingrevisions: 'current'orrevisions: ''on the form body, but the tool's default isoriginalso submission history is preserved out of the box. On unlicensed deployments,revisionsis stripped from the body entirely. - Every
form_updatecall writes a revision note (note). The caller passes one for standard updates, drafts, publishes, and explicit revert notes; forrevert: truethe tool defaultsnotetoReverted to version {version}when the caller omits it. The note is prefixed (@formio/mcp:) and persisted on the revision document — never skipped.
Prefer the MCP server's first-party tools for every operation on this page; fall back to the raw HTTP endpoints only if the tool cannot satisfy the request.
- List revisions (
GET /form/:id/v) — useform_revisions_list. Accepts the form by_idor path alias. - Get a single revision (
GET /form/:id/v/:version) — useform_revision_get.versionmay be the sequential_vidor the revision document's_id. - Inspect the current draft (
GET /form/:id/draft) — useform_getwithdraft: true. Accepts the form by_idor path alias. The underlying endpoint falls back to the live form when no draft exists; the tool distinguishes by checking_vid === 'draft'and throws a clear "no draft exists" error when the fallback fires. - Enable or change revisions setting (
PUT /form/:idwithrevisions) — useform_updateand passrevisions: "current" | "original" | ""on the form body. Omittingrevisionsonform_updateleaves the stored value unchanged; when the stored form has revisions disabled and the caller did NOT opt in viarevisions: 'original' | 'current', the tool prompts (elicitation, with a browser fallback) for the per-form mode before applying the update. - Save a draft (
PUT /form/:id/draft) — useform_updatewithdraft: true. Callerformfields merge on top of the existing draft (caller wins), preserving prior unpublished edits. - Publish the current draft (
PUT /form/:idfrom/draftbody) — useform_updatewithpublish: true. The tool fetches the staged draft and the live form, then PUTs the live form overlaid with a strict revision-field allowlist from the draft:components,settings,tags,properties,controller,esign,display. All other fields (title,name,path,type,access,submissionAccess,submissionRevisions,owner,project,revisions, identity/server-managed fields) keep their live values. The caller'sformargument is ignored in this mode;notemust still describe the actual diff between the live form and the draft (generic placeholders like "publishing changes" are forbidden). - Revert to a prior revision — use
form_updatewithrevert: trueandversion: "<vid>"(a sequential_vidlike"3") orversion: "<revisionDocId>"(the 24-char hex revision document_id). The tool fetches that revision and the live form, then PUTs live overlaid with a narrower revert allowlist:components,tags,properties,display. All other fields keep their live values; the caller'sformargument is ignored. Inspect the target revision viaform_revision_getfirst sonotecan describe what reverting restores (e.g.Revert to v3: rollback bad release); when omitted, the tool defaultsnotetoReverted to version {version}.draft,publish, andrevertare mutually exclusive — pass at most one (the tool throws when more than one is set).
note is required on every form_update call EXCEPT revert: true (which defaults the note to Reverted to version {version} when the caller omits it). The LLM SHALL generate it by diffing the prior state against the new body — no action preambles (Published draft:, Saved draft:, Reverted:).
draft, publish, and revert require the Security Module on the deployment's license. When the deployment is unlicensed:
draft/publish/revert— the tool throws immediately telling the caller to drop the flag and callform_updateas a standard update.- Standard updates — the tool prompts once per deployment for "continue without revision tracking" consent (cached across sessions in
~/.formio/revisions-license-consent.json). On consent, therevisionsfield is stripped from the body so the API doesn't silently write a value it can't honor.
Distinct from the deployment-level license gate above, this gate asks "for THIS specific form, how should revisions be tracked." It fires on a standard form_update ONLY when ALL of the following hold:
- The deployment IS licensed for revisions.
- The stored form has
revisionsdisabled (falsy). - The caller did NOT opt in by passing
revisions: 'original' | 'current'on the body. Passingrevisions: ''mirrors the disabled stored state and does NOT bypass the prompt — that loophole would let an LLM silently skip the audit-trail decision on every form by always echoing the disabled value. - The user has not already approved "proceed without history" for this form in the current process (session-scoped cache).
When all conditions hold, the tool prompts (elicitation, with a browser fallback) with three choices:
- Enable revisions (original) — submissions render against the form version active when they were submitted. Tool sets
revisions: 'original'on the PUT body. - Enable revisions (current) — submissions always render against the latest form version. Tool sets
revisions: 'current'on the PUT body. - Proceed without history (not tracked) — tool strips any caller-supplied
revisionsfrom the PUT body and remembers the approval for thisformIdfor the rest of the process so the user is asked only once per form.
On cancel, the tool throws and no update is performed.
Enable form revisions on an existing form by setting revisions on the form document. This endpoint is the standard form update (see project-forms.md), but when a form is saved with revisions turned on, subsequent draft/publish operations become available on that form. Once enabled, every PUT to this path creates a new published revision.
| Path parameter | Type | Description |
|---|---|---|
formId |
string | The MongoDB _id of the form to update. |
Request body (JSON): the full form definition, including _id, title, name, path, type, display, components, and (to enable the feature) a revisions flag per the form's settings.
{
"_id": "69d69ce1040fa2cea2572c71",
"title": "Example Form 8",
"name": "example",
"path": "example",
"type": "form",
"display": "form",
"tags": [],
"owner": "69d6813b040fa2cea257285a",
"components": [
{ "type": "textfield", "label": "First Name", "key": "firstName", "validate": { "required": true } },
{ "type": "textfield", "label": "Last Name", "key": "lastName", "validate": { "required": true } },
{ "type": "email", "label": "Email", "key": "email", "validate": { "required": true } }
]
}Response: the updated form document. The server bumps _vid (version ID) and records a revision entry accessible via GET .../v.
Errors: 400 for validation errors; 404 if the form does not exist; 409 if a concurrent edit has already advanced _vid; 401/403 as above.
Example:
curl -X PUT -H "x-jwt-token: $FORMIO_JWT" -H "Content-Type: application/json" \
-d @form.json \
"${FORMIO_PROJECT_URL}/form/69d69ce1040fa2cea2572c71"Save an in-progress draft of a revisioned form. Drafts do not affect live submissions; they are a working copy that the admin iterates on until ready to publish.
| Path parameter | Type | Description |
|---|---|---|
formId |
string | The MongoDB _id of the form whose draft is being updated. |
Request body (JSON): same shape as the published form body — _id, title, name, path, type, display, components, etc.
Response: the saved draft document (same shape as a form document). Subsequent calls to GET .../draft will return this state until publish or overwrite.
Errors: 400 for validation errors; 404 if the form does not exist or revisions are not enabled; 401/403 as above.
Example:
curl -X PUT -H "x-jwt-token: $FORMIO_JWT" -H "Content-Type: application/json" \
-d @draft.json \
"${FORMIO_PROJECT_URL}/form/69d69ce1040fa2cea2572c71/draft"Retrieve the current draft for a revisioned form.
| Path parameter | Type | Description |
|---|---|---|
formId |
string | The MongoDB _id of the form. |
Response: the draft form document, including _id, title, name, path, components, access, submissionAccess, and any in-progress edits.
Example:
curl -H "x-jwt-token: $FORMIO_JWT" \
"${FORMIO_PROJECT_URL}/form/69d69ce1040fa2cea2572c71/draft"Publish a draft. Publishing is done by issuing a standard PUT to the form endpoint with the desired definition — Form.io treats that save as the next published revision when revisions are enabled. After publishing, the draft is cleared and the new revision is appended to the revision list.
| Path parameter | Type | Description |
|---|---|---|
formId |
string | The MongoDB _id of the form being published. |
Request body (JSON): the full form definition to publish. Typically the admin fetches GET .../draft, makes any last edits, then PUTs that body here.
Response: the published form document with an incremented _vid and a new entry in GET .../v.
Errors: 400 for validation errors; 404 if the form does not exist; 409 on _vid conflicts; 401/403 as above.
Example:
curl -X PUT -H "x-jwt-token: $FORMIO_JWT" -H "Content-Type: application/json" \
-d @published.json \
"${FORMIO_PROJECT_URL}/form/69d69ce1040fa2cea2572c71"List every revision of a form, ordered oldest to newest. Each revision is a full snapshot of the form definition at publish time.
| Path parameter | Type | Description |
|---|---|---|
formId |
string | The MongoDB _id of the form. |
Response: JSON array of form-revision documents. Each entry includes _id (revision ID, distinct from the form's _id), title, name, path, type, display, tags, access, submissionAccess, owner, and components. The revision's sequential version number is implied by array order and by the URL used to retrieve it.
Errors: 404 if the form does not exist or has no revisions; 401/403 as above.
Example:
curl -H "x-jwt-token: $FORMIO_JWT" \
"${FORMIO_PROJECT_URL}/form/69d69ce1040fa2cea2572c71/v"Retrieve a specific form revision by sequential version number (1, 2, ...) or by revision _id.
| Path parameter | Type | Description |
|---|---|---|
formId |
string | The MongoDB _id of the form. |
version |
string | Either the sequential version number (e.g., 2) or the revision document's _id. Form.io accepts both in the same path segment. |
Response: the full revision document (same shape as a published form). Useful for diffing, rolling back, or rehydrating a past form definition when replaying submissions.
Errors: 404 if no matching revision exists; 401/403 as above.
Examples:
# by sequential version number
curl -H "x-jwt-token: $FORMIO_JWT" \
"${FORMIO_PROJECT_URL}/form/69d69ce1040fa2cea2572c71/v/2"
# by revision _id
curl -H "x-jwt-token: $FORMIO_JWT" \
"${FORMIO_PROJECT_URL}/form/69d69ce1040fa2cea2572c71/v/69d69df5040fa2cea2572ce4"- project-forms — base form CRUD; enabling revisions is done via the standard form update
- project-actions — actions attached to a form (actions themselves are not revisioned)
- project-roles — roles referenced by the
accessblocks captured in each revision