-
Notifications
You must be signed in to change notification settings - Fork 1
FlexForms JSON Template Designer Manual
A practical guide for designing application forms as JSON. Written for product/content designers and developers who are new to the FlexForms engine. Examples follow the Transfer Applications template patterns.
If you have never seen JSON before, start with §1.1 JSON in plain English.
Related guide: Tenant Admin User Manual — live forms, access, and Tenant Settings (including autocomplete / FormEngine).
A FlexForms template is a JSON document that describes:
- The structure of an application (sections → tasks → pages → questions)
- The controls users fill in (text, radios, dates, uploads, search, etc.)
- Rules that show, hide, or skip questions based on answers
- Optional repeatable lists (add many trusts, academies, members…)
- Optional derived lists (e.g. one declaration per trust already entered)
Users see a GOV.UK-style task list. Completing tasks moves status from Not started → In progress → Completed. Answers are stored by fieldId.
Think of the JSON as a blueprint. The engine renders it; you do not write HTML for each question.
You do not need to be a programmer. JSON is just structured text — a way of writing labelled facts so a computer can read them reliably.
Think of a paper form. Each box has a name (Date of birth, School name) and a value (the answer). JSON is the same idea, typed out:
{
"schoolName": "Oakwood Academy",
"numberOfPupils": 420,
"isLive": true
}Read that as: the school name is Oakwood Academy, there are 420 pupils, and the form is live. Curly braces { } mean “this is one record”. Each line is a key (the label, always in double quotes) then a colon, then the value.
| What it looks like | What it is | Everyday example |
|---|---|---|
"Oakwood Academy" |
Text (a string). Always inside double quotes. | A name, a question label, an id |
420 |
Number. No quotes. | A count, an order (1, 2, 3) |
true or false
|
Yes / no. No quotes, lowercase. | Is this field required? |
{ ... } |
Object. A group of labelled facts. | One field, one page, one task |
[ ... ] |
List (an array). Several things of the same kind, in order. | Several tasks, several questions |
There is also null, which means “nothing here”. FlexForms templates almost never need it.
A template is one big object. Inside it you put lists of task groups; inside those, lists of tasks; inside those, pages and fields. Same punctuation at every level:
{
"templateName": "School transfer",
"taskGroups": [
{
"title": "About the school",
"tasks": [
{
"title": "School name",
"pages": [
{
"title": "What is the school called?",
"fields": [
{
"fieldId": "schoolName",
"type": "text",
"label": { "value": "School name" }
}
]
}
]
}
]
}
]
}That is the same hierarchy as §2, just written as JSON. You do not invent HTML; you fill in these labelled boxes.
JSON is fussy. The computer cannot guess what you meant.
-
Keys use double quotes —
"title"nottitleand not'title'. -
A colon sits between the key and the value —
"type": "text". -
Commas go between items, not after the last one.
- Right:
{ "a": 1, "b": 2 } - Wrong:
{ "a": 1, "b": 2, }← extra comma
- Right:
-
Brackets must pair. Every
{needs a}, every[needs a]. Use an editor that highlights matching pairs (VS Code, or paste into jsonlint.com to check). -
Do not put a comma between
}and]when the}already closed the last list item — close the object, then close the list:} ].
A common slip when copying an example is leaving a trailing comma after the last field, or using a curly brace { where a list [ was needed (taskGroups is always a list).
In Template Manager you paste or edit this text in the JSON Schema box and save a version. If a quote or comma is wrong, save is rejected and you see There is a problem with the parse error — usually “missing comma” or “unexpected character”. Fix the punctuation and save again. The rest of this manual is the meaning of each key once the text is valid JSON.
FormTemplate
└── taskGroups[] ← major sections on the task list
└── tasks[] ← rows under each section
├── pages[] ← normal question pages (linear task)
└── summary ← OR collection / declaration flows
├── flows[] (multiCollectionFlow)
└── derivedFlows[] (derivedCollectionFlow)
| Layer | User sees | You define |
|---|---|---|
| Template | Whole service form | Name, description, global rules |
| Task group | Heading on task list | e.g. “About transferring academies” |
| Task | Clickable task row | e.g. “Risks”, “Members” |
| Page | One screen / question | Title, fields, save behaviour |
| Field | One input | Type, label, validation |
Rule of thumb
- Use
pagesfor a fixed sequence of questions. - Use
summary.mode = multiCollectionFlowwhen users must add 0..N items, each with its own mini-wizard. - Use
summary.mode = derivedCollectionFlowwhen items come from another collection (e.g. declarations for each outgoing trust).
In Transfer: “Trust details” uses a collection; “Reason and benefits” uses plain pages; “Declaration” uses derived flows.
{
"templateId": "form-001",
"templateName": "Transfer Applications",
"description": "A dynamic form for Transfer Applications",
"taskGroups": [ ],
"conditionalLogic": [ ],
"defaultFieldRequirementPolicy": "required",
"hideFieldLabelWhenOnlyOneField": true,
"contributorPattern": true,
"dashboard": {
"columns": [ ]
}
}| Property | Purpose |
|---|---|
templateId |
Logical id in the JSON (not the DB GUID). Keep stable across versions when possible. |
templateName |
Human name. |
description |
Short summary. |
taskGroups |
Required. At least one group with one task. |
conditionalLogic |
Optional. Show/hide/skip rules. |
defaultFieldRequirementPolicy |
"required" or "optional". If a field has no required validation and no required flag, this policy applies. Transfer uses "required". |
hideFieldLabelWhenOnlyOneField |
When true (default), a page with exactly one normal field hides that field’s label so the page title is the question. Complex fields are excluded from this behaviour. |
contributorPattern |
When true (default), invite-contributor features are available. Set false to hide them. |
dashboard |
Optional. Configures applications-dashboard columns (see §3.1). |
The applications dashboard always uses column headings from the latest published template. Cell values come from each application’s own answers, looked up by stable fieldId. Older applications on earlier versions show blank cells when a field is missing.
"dashboard": {
"columns": [
{ "type": "system", "id": "reference", "order": 10 },
{ "type": "field", "fieldId": "incomingTrustName", "header": "Trust name", "order": 20 },
{ "type": "system", "id": "dateStarted", "order": 30 },
{ "type": "system", "id": "dateSubmitted", "order": 40 },
{ "type": "field", "fieldId": "proposedTransferDate", "header": "Proposed date", "order": 45 },
{ "type": "system", "id": "status", "order": 50 },
{ "type": "system", "id": "action", "order": 60 }
]
}| Property | Purpose |
|---|---|
columns |
Ordered list of columns. When omitted, the default system columns are used. |
type |
"system" or "field". If omitted and fieldId is set, treated as "field". |
id |
System column id: reference, dateStarted, dateSubmitted, status, action. |
fieldId |
Stable answer key for a field column. Do not rename casually — it is the contract across versions. |
header |
Column heading. Required for field columns (falls back to fieldId if blank). Optional override for system columns. |
order |
Lower first. If omitted, array order is used (10, 20, …). |
Rules
- Maximum 3
fieldcolumns (extras are ignored). - If you only list field columns, default system columns are merged in automatically.
- An
actioncolumn is always kept so users can open an application. - Prefer scalar fields (text, date, radios, select). Uploads are a poor fit for a table cell.
Collection-flow fields
Answers inside a multiCollectionFlow are stored under the flow fieldId as an array of items (e.g. detailsOfIncomingTrust), not as top-level keys. Dashboard lookup still accepts the inner field id:
fieldId |
Result |
|---|---|
incomingTrustsSearch-field-flow |
Finds that field in each collection item; complex search JSON uses .name (or title/label) for display. Multiple items are joined with , . |
incomingTrustsSearch-field-flow.name |
Same, but forces a nested property (same pattern as itemTitleBinding). Use this for any child of a complex/autocomplete object (ukprn, companiesHouseNumber, etc.). |
detailsOfIncomingTrust.incomingTrustsSearch-field-flow |
Explicit path: collection → nested field. |
Example (trust search inside “Trust details” flow):
{ "type": "field", "fieldId": "incomingTrustsSearch-field-flow", "header": "Trust name", "order": 20 }Minimal custom-columns-only example (inserted among defaults by order):
"dashboard": {
"columns": [
{ "fieldId": "incomingTrustName", "header": "Trust name", "order": 15 },
{ "fieldId": "proposedTransferDate", "header": "Proposed date", "order": 25 }
]
}{
"groupId": "transferring-academies-group",
"groupName": "About transferring academies",
"groupOrder": 2,
"groupStatus": "Incomplete",
"tasks": [ ]
}| Property | Notes |
|---|---|
groupId |
Unique id. Use kebab-case. |
groupName |
Shown as a section heading. |
groupOrder |
Sort order (1, 2, 3…). |
groupStatus |
Initial status text; engine updates as users progress. |
tasks |
Tasks in this section. |
Transfer groups: joining trust → transferring academies → leaving trusts → declaration.
Two shapes:
{
"taskId": "reason-and-benefits-trust",
"taskName": "Reason and benefits",
"caption": "{detailsOfIncomingTrust.incomingTrustsSearch-field-flow.name ?? Reason and benefits}",
"taskOrder": 3,
"taskStatus": "Incomplete",
"pages": [ ],
"summary": null,
"startAtFirstPageWhenNotStarted": null,
"visibleInTaskList": null
}{
"taskId": "incoming-trust-details",
"taskName": "Trust details",
"caption": null,
"taskOrder": 1,
"taskStatus": "Incomplete",
"pages": null,
"summary": {
"mode": "multiCollectionFlow",
"flows": [ ]
},
"visibleInTaskList": true
}| Property | Notes |
|---|---|
taskId |
Unique. |
taskName |
Task list label. |
caption |
Optional text above page titles. Supports bindings (see §12). |
taskOrder |
Order within the group. |
taskStatus |
"NotStarted", "InProgress", "Completed", "CannotStartYet", or legacy "Incomplete". |
pages |
Linear pages, or null if the task is collection-driven. |
summary |
Collection / derived configuration, or null. |
startAtFirstPageWhenNotStarted |
If true, opens first page instead of summary when not started. |
visibleInTaskList |
Set true for collection tasks that should appear on the task list. |
{
"pageId": "proposedTransferDate-page",
"slug": "proposed-transfer-date",
"title": "What is the proposed transfer date?",
"description": "",
"pageOrder": 2,
"fields": [ ],
"returnToSummaryPage": false,
"saveButtonLabel": null
}| Property | Notes |
|---|---|
pageId |
Unique id. Conditional logic often targets this. |
slug |
URL-friendly segment. |
title |
Main question / heading. |
description |
Extra body text (Markdown-friendly). Can include links and {displayName} in derived flows. |
pageOrder |
Order in the task or collection wizard. |
fields |
One or more fields. |
returnToSummaryPage |
After save: return to task/collection summary (true) or continue the wizard (false). Transfer often uses false mid-wizard and true on the last page of a sub-flow. |
saveButtonLabel |
Override button text, e.g. "Sign the declaration". |
Design tip: Prefer one question per page (GOV.UK pattern). Put related short fields (name / phone / email) on one page when they form one “contact details” block.
Every field shares this shape:
{
"fieldId": "incomingTrustAccountingOfficerFullName",
"type": "text",
"label": {
"value": "Full name",
"isVisible": true,
"validationLabelValue": null
},
"placeholder": "Full name",
"tooltip": "",
"required": null,
"order": 1,
"visibility": { "default": true },
"validations": [ ],
"options": null,
"complexField": null,
"Value": null
}| Property | Notes |
|---|---|
fieldId |
Answer key. Must be unique in the template. Never reuse casually; changing it breaks existing answers. |
type |
Control type (next section). |
label.value |
Label text. |
label.isVisible |
Show label (true) or hide it (false) when the page title is enough. |
label.validationLabelValue |
Name used in errors (useful for dates: “Proposed transfer date”). |
placeholder |
Grey hint inside empty inputs. |
tooltip |
Hint under the label. Supports Markdown and links, e.g. [text](https://...). |
required |
Optional bool; usually prefer a required validation instead. |
order |
Display order on the page. |
visibility.default |
Starting visibility before conditional logic runs. |
validations |
Rules (required, regex, maxLength…). |
options |
For radios / checkboxes / select. |
complexField |
Only for type: "complexField". Must include id. Optional dropdownDisplay and confirmationDisplay — see 8.10. |
Value |
Leave null in the template (runtime answers live elsewhere). |
{
"fieldId": "memberName",
"type": "text",
"label": { "value": "Name", "isVisible": true },
"validations": [
{ "type": "required", "rule": "", "message": "Enter the full name of the member" },
{ "type": "maxLength", "rule": 100, "message": "Full name must be 100 characters or less" }
]
}Use for names, roles, short free text. For phone numbers Transfer still uses text + a regex validation (UK numbers).
{
"fieldId": "incomingTrustMainContactEmailAddress",
"type": "email",
"label": { "value": "Email address", "isVisible": true },
"tooltip": "We'll only use it to contact them about this application",
"validations": [
{ "type": "required", "rule": true, "message": "Enter an email address" }
]
}Engine also checks email format even without an extra regex.
{
"fieldId": "reasonAndBenefitsTrustStrategicNeeds",
"type": "character-count",
"label": {
"value": "What are the strategic needs of the trust?",
"isVisible": false
},
"validations": [
{ "type": "maxLength", "rule": 2000, "message": "You must enter 2000 characters or less" },
{ "type": "required", "rule": true, "message": "Enter the strategic needs of the trust." }
]
}- Pair with
maxLength(characters) ormaxWords. - Transfer often hides the field label (
isVisible: false) because the page title is the question.
Multi-line without the character-count widget. Use character-count when you need a visible limit (GOV.UK pattern).
{
"fieldId": "incomingTrustTypeOfTrust",
"type": "radios",
"label": { "value": "What is the type of trust?", "isVisible": true },
"options": [
{ "value": "Single academy trust", "label": "Single academy trust" },
{ "value": "Multi-academy trust", "label": "Multi-academy trust" }
],
"validations": [
{
"type": "required",
"rule": "",
"message": "Select if it is a single academy trust or a multi academy trust"
}
]
}Critical: Conditional logic compares against options[].value, not label.
Yes/No in Transfer is usually "yes" / "no" (lowercase). Elsewhere you may see "Yes" / "No" — be consistent and match rules exactly.
Same options shape as radios. Values are multi-select. Useful for “select all that apply”.
Same options shape. Prefer radios for short lists (GOV.UK); use select for long lists.
{
"fieldId": "proposedTransferDate",
"type": "date",
"label": {
"value": "What is the proposed transfer date?",
"isVisible": false,
"validationLabelValue": "Proposed transfer date"
},
"tooltip": "For example, 27 3 2026",
"validations": [
{ "type": "required", "rule": true, "message": "Enter the proposed transfer date" }
]
}Renders day / month / year. Engine checks completeness and that the date is real. Use validationLabelValue so errors read well when the visible label is hidden.
A built-in autocomplete control. For Trusts/Academies search, Transfer uses complexField instead (API-backed). Prefer complex fields when search hits an external API.
A search (autocomplete) or file upload question backed by tenant configuration.
You do not put the API URL, API key, or OAuth client secret in the template. Those live in Tenant Settings category FormEngine (Target Web), as a ComplexFields entry whose Id matches complexField.id.
Admin setup (auth, endpoint, default labels): Tenant Admin User Manual — 14.7 Autocomplete search.
You cannot invent a new search id in JSON alone. Ask a tenant Admin to add the Id under FormEngine first (API endpoint plus either an API key or OAuth2 client credentials). Then use that same id here.
JSON on complexField
|
Required | What it does |
|---|---|---|
id |
Yes | Must match a ComplexFields Id in FormEngine (e.g. TrustComplexField, EstablishmentComplexField, MemberComplexField). PascalCase. |
dropdownDisplay |
No | How each search result is labelled in the dropdown (and in the box after the user chooses). Overrides the same setting in FormEngine when both are set. |
confirmationDisplay |
No | How the chosen answer is shown on check your answers / preview, and which properties appear on “Is this the right …?”. Overrides FormEngine when set. |
If both display properties are omitted, FlexForms keeps the built-in Trusts/Academies layout:
| Surface | Default (no expressions) |
|---|---|
| Dropdown | Result name, plus UKPRN, code, and/or Companies House number when those properties exist on the API object |
| Check your answers | Bold name, then postcode, UKPRN, Companies House number when present |
| “Is this the right …?” | Trusts/academies-style fields depending on the config id |
{
"fieldId": "incomingTrustsSearch-field-flow",
"type": "complexField",
"label": { "value": "Trusts", "isVisible": false },
"placeholder": "Start typing to search for Trusts...",
"tooltip": "Enter at least 3 characters to search by name, UKPRN, or Companies House number",
"complexField": { "id": "TrustComplexField" },
"validations": [
{ "type": "regex", "rule": ".{3,}", "message": "You must enter 3 characters or more to search for a trust" },
{ "type": "required", "rule": true, "message": "Enter 3 characters or more to search for a trust" }
]
}Stored value is a rich JSON object. Bindings like
{detailsOfIncomingTrust.incomingTrustsSearch-field-flow.name} read nested properties.
"complexField": { "id": "EstablishmentComplexField" }Use this when the API returns different property names (not name / UKPRN). The id must already exist in FormEngine. Auth (API key vs client credentials) is not configured in the template.
Example API object:
{
"id": "123",
"displayName": "Jane Smith MP",
"firstName": "Jane",
"lastName": "Smith",
"constituencyName": "Example West",
"email": "jane.smith@example.gov.uk"
}Template field:
{
"fieldId": "memberSearch",
"type": "complexField",
"label": { "value": "Member of Parliament", "isVisible": true },
"placeholder": "Start typing a name...",
"tooltip": "Enter at least 3 characters to search",
"complexField": {
"id": "MemberComplexField",
"dropdownDisplay": "displayName + \" - \" + constituencyName",
"confirmationDisplay": "firstName + \" \" + lastName"
},
"validations": [
{ "type": "required", "rule": true, "message": "Search for and select a member" }
]
}What the applicant sees:
| Surface | With the expressions above |
|---|---|
| Dropdown row | Jane Smith MP - Example West |
| After they choose | Same text in the search box |
| Check your answers | Jane Smith |
| “Is this the right …?” | Lists the properties used in confirmationDisplay (firstName, lastName) |
The saved answer is still the full object (email, constituencyName, and so on). Use those names in collection captions, event mappings, and email ComplexFieldProperty paths.
If you omit dropdownDisplay / confirmationDisplay here, FormEngine’s DropdownDisplay / ConfirmationDisplay apply. If those are also empty, the built-in name/UKPRN layout applies (often a poor fit for a members API).
Two equivalent styles. Property names must match the JSON from the search API (case-insensitive).
Concatenation (quoted literals joined with +):
displayName + " - " + constituencyName
firstName + " " + lastName
Placeholders (only when the expression has no +):
{firstName} {lastName}
{displayName} — {constituencyName}
Rules:
- Empty properties are skipped. You do not get a trailing
-ifconstituencyNameis missing. - Separator-only literals (
-,|,/,,,:) are also dropped when there is nothing after them. - Nested objects and arrays (for example
roles) cannot be used in the expression; only strings, numbers, and booleans copied from the result. - FlexForms still sets a fallback
namefrom the first of:name,title,label,value,displayName,groupName,text. UsedropdownDisplaywhen you need more than that one field.
{
"fieldId": "incomingTrustUploadBoardResolution",
"type": "complexField",
"label": { "value": "Board resolution", "isVisible": false },
"tooltip": "This is the minutes from the meeting where...",
"complexField": { "id": "UploadDocumentsComplexField" },
"validations": [
{ "type": "required", "rule": true, "message": "Select a file" }
]
}dropdownDisplay / confirmationDisplay do not apply to uploads.
- Confirm the FormEngine
Idexists (Admin → Tenant Settings →FormEngine, Target Web). - Confirm auth is already configured there (API key or client credentials) — not in this JSON.
- Set
"type": "complexField"and"complexField": { "id": "…" }. - If the API is not trusts/academies-shaped, add
dropdownDisplayandconfirmationDisplay(or ask Admin to set them on the FormEngine entry). - Add
required(and usually a min-length regex matching the configMinLength). - Save a new template version, preview, type past the minimum length, pick a result, and check check your answers.
{
"type": "required",
"rule": true,
"message": "Enter the full name",
"condition": null
}type |
rule |
Meaning |
|---|---|---|
required |
often true or ""
|
Must answer |
regex |
pattern string | Must match |
maxLength |
number | Max characters |
maxWords |
number | Max words |
condition can attach a rule only when another condition is true (advanced; Transfer mostly uses top-level conditionalLogic instead).
UK phone pattern (from Transfer):
^(?:0|\+?44)\s?(\d\s?){9,10}$
(In JSON this is escaped as needed.)
Use when users add many similar items.
"summary": {
"mode": "multiCollectionFlow",
"title": null,
"description": null,
"flows": [ ],
"derivedFlows": null
}One task can have several flows (Transfer “Members”: members after transfer + members leaving).
{
"flowId": "detailsOfAcademies",
"title": "",
"description": "",
"fieldId": "detailsOfAcademies",
"addButtonLabel": "Add an academy",
"minItems": 1,
"maxItems": 50,
"itemKind": "Academy",
"itemKindPlural": "Academies",
"itemTitleBinding": "academiesSearch.name",
"summaryColumns": [
{ "label": "Academy name", "field": "academiesSearch" },
{ "label": "Proposed transfer date", "field": "proposedTransferDate" }
],
"addItemMessage": "{academiesSearch.name} has been added",
"updateItemMessage": "{academiesSearch.name} has been updated",
"deleteItemMessage": "{academiesSearch.name} has been removed",
"tableType": "card",
"pages": [ ]
}| Property | Meaning |
|---|---|
flowId |
Unique flow id. |
fieldId |
Where the array of items is stored in answers. |
addButtonLabel |
Button on the summary. |
minItems / maxItems
|
Limits (e.g. trusts joining: max 1; members: min 3). |
itemKind / itemKindPlural
|
Wording (“Trust”, “Academies”). |
itemTitleBinding |
Path for card title (name, academiesSearch.name, …). |
summaryColumns |
Rows shown on each card / list. field = fieldId inside the item. |
tableType |
"card" (Transfer) or "list". |
pages |
Pages users walk when adding/editing one item. |
| Messages | Support {fieldId}, {flowTitle}, nested paths. |
Incoming trust with max 1 still uses a collection so the UX is “add trust” + check-answers card, not a plain linear task.
Creates one item per entry already captured in another collection.
Transfer declarations:
- Source:
detailsOfIncomingTrust→ one declaration for the joining trust - Source:
detailsOfOutgoingTrusts→ one declaration per leaving trust
"summary": {
"mode": "derivedCollectionFlow",
"title": "Declaration from all chairs of trustees",
"description": "The chair of trustees from each trust involved...",
"flows": null,
"derivedFlows": [
{
"flowId": "trust-declarations-joining",
"title": "Declaration for the trust that academies are joining",
"sourceFieldId": "detailsOfIncomingTrust",
"sourceType": "collection",
"fieldId": "trustDeclarations",
"itemTitleBinding": "name",
"sectionOrder": 1,
"signedMessage": "Declaration for {displayName} has been signed",
"statusField": "status",
"pages": [
{
"pageId": "declaration-form-joining",
"slug": "declaration-form",
"title": "Declaration form",
"description": "I hereby certify...\n\n## Name of trust\n\n{displayName}",
"saveButtonLabel": "Sign the declaration",
"fields": [ ]
}
]
}
]
}| Property | Meaning |
|---|---|
sourceFieldId |
Collection fieldId to expand. |
sourceType |
How to read source: "collection", "autocomplete", "checkboxes", "select". |
fieldId |
Storage for derived answers / status. |
itemTitleBinding |
Display name property. |
signedMessage |
Banner after signing; {displayName} / {name}. |
pages |
Form each derived item must complete. |
Page description can include Markdown and {displayName} for the current trust/academy name.
| Pattern | Meaning |
|---|---|
{fieldId} |
Insert answer |
{fieldId.property} |
Nested property (e.g. trust .name) |
{path ?? Fallback text} |
Use fallback if empty |
Task caption showing trust name:
"caption": "{detailsOfIncomingTrust.incomingTrustsSearch-field-flow.name ?? Reason and benefits}"Collection success message:
"addItemMessage": "{memberName} has been added to {flowTitle}"Item title on cards:
"itemTitleBinding": "incomingTrustsSearch-field-flow.name"Use captions sparingly — they personalise later tasks once the joining trust exists.
Rules live at the template root, not on each field.
{
"id": "hide-pupil-forecast-for-no-risk",
"name": "Hide pupil forecast page when no pupil number risk",
"priority": 60,
"enabled": true,
"conditionGroup": {
"logicalOperator": "AND",
"conditions": [
{
"triggerField": "risksPupilNumbers",
"operator": "equals",
"value": "no",
"dataType": "string"
}
]
},
"affectedElements": [
{
"elementId": "risks-upload-pupil-numbers",
"elementType": "page",
"action": "skip"
},
{
"elementId": "risksUploadPupilNumbers",
"elementType": "field",
"action": "hide"
}
],
"executeOn": ["change", "load"],
"debounce": 300
}-
Trigger — which field’s answer changes the form (
triggerField). -
Condition — e.g. equals
"no". -
Effects — usually both:
-
page+skip/show(navigation) -
field+hide/show(answers / validation)
-
Transfer always pairs page skip with field hide (and show with show).
equals, notEquals, in, notIn, contains, isEmpty, isNotEmpty, greaterThan, lessThan, …
| Action | On | Effect |
|---|---|---|
show / hide
|
field, page | Visibility |
skip |
page | Skip in navigation when condition true |
require / makeOptional
|
field | Requirement |
enable / disable
|
field | Interaction |
Lower priority runs first. Transfer often uses 60 = hide/skip and 61 = show for the opposite rule.
Example: hide a page if either “no SAP academies” or “no LA agreements”:
"conditionGroup": {
"logicalOperator": "OR",
"conditions": [
{
"triggerField": "financeAndOperationsLocalAuthorityAgreements",
"operator": "equals",
"value": "no",
"dataType": "string"
},
{
"triggerField": "financeAndOperationsHaveSAPAcademies",
"operator": "equals",
"value": "no",
"dataType": "string"
}
]
}- Radios: “Does X apply?” →
yes/no - Next page: details / upload
- Rules: if
no→ skip + hide follow-up; ifyes→ show both
Used throughout Transfer (growth plan, diocesan consent, pupil forecast, etc.).
Gotcha: Option value strings must match conditional logic value strings exactly, or rules will not fire. Prefer aligning option values with rule values (e.g. always "yes" / "no").
{
"templateId": "my-first-form",
"templateName": "My first form",
"description": "A simple starter application",
"defaultFieldRequirementPolicy": "optional",
"hideFieldLabelWhenOnlyOneField": true,
"contributorPattern": false,
"taskGroups": [
{
"groupId": "about-you-group",
"groupName": "About you",
"groupOrder": 1,
"groupStatus": "NotStarted",
"tasks": [
{
"taskId": "your-details",
"taskName": "Your details",
"taskOrder": 1,
"taskStatus": "NotStarted",
"pages": [
{
"pageId": "full-name-page",
"slug": "full-name",
"title": "What is your full name?",
"description": "",
"pageOrder": 1,
"fields": [
{
"fieldId": "fullName",
"type": "text",
"label": { "value": "Full name", "isVisible": false },
"order": 1,
"validations": [
{ "type": "required", "rule": true, "message": "Enter your full name" }
]
}
],
"returnToSummaryPage": true
}
]
}
]
}
],
"conditionalLogic": []
}Grow by adding pages, then radios + follow-up logic, then a small collection flow.
| Thing | Convention | Example |
|---|---|---|
| Ids | kebab-case or camelCase, unique |
risks-pupil-numbers, risksPupilNumbers
|
pageId |
often ends with -page
|
proposedTransferDate-page |
slug |
kebab-case URL | proposed-transfer-date |
fieldId |
camelCase, descriptive | proposedTransferDate |
Collection fieldId
|
plural / flow name | detailsOfAcademies |
| Complex field config ids | PascalCase, shared | TrustComplexField |
Keep pageId and fieldId distinct: conditional logic targets both.
- Sketch the task list (groups → tasks) before JSON.
- Decide per task: linear pages vs collection vs derived.
- One clear question per page where possible.
- Give every field a stable
fieldId. - Add required messages that sound like GOV.UK (“Enter…”, “Select…”).
- For Yes/No follow-ups, add paired show/hide rules.
- For collections, set min/max, columns, and itemTitleBinding.
- Validate JSON (commas, quotes).
- Upload via Template Manager as a new version; test Live vs Not live.
- Remember: complexField ids must exist in tenant FormEngine settings. For a new API, confirm Admin has registered the id (endpoint + API key or client credentials). Add
dropdownDisplay/confirmationDisplayif the results are not trust/academy-shaped.
| Feature | Where in Transfer |
|---|---|
| Linear pages | Reason and benefits, Risks, Finance… |
| Radios + character-count | Almost every section |
| Dates | Proposed transfer date, declaration signed date |
| Complex search | Trust / academy finders |
| Uploads | Board resolutions, growth plans, forecasts |
| Collection max 1 | Incoming trust details |
| Collection many | Academies, outgoing trusts, members, trustees |
| Multiple flows in one task | Members after / leaving; Trustees after / leaving |
| Derived declarations | Chairs of trustees |
| Captions with bindings | Tasks under joining trust |
| Conditional skip/show | Growth plan, diocesan consent, deficits, “worked together”, etc. |
| Custom save label | “Sign the declaration” |
| Markdown tooltips / descriptions | Guidance links and multi-line hints |
- Database template GUID / tenant ownership
- FormEngine search API URLs, API keys, and OAuth client credentials (tenant settings — see Admin 14.7). The template may only name the id and optional display expressions.
- Email templates, auth, hostnames
- HTML/CSS — the engine renders GOV.UK components