Template JSON
How to write a Chronicle form template as JSON, field by field, so it can be imported as a working form.
Overview
A Chronicle form template can be written as a single JSON file and imported through the Hub. This page is the complete specification of that file: every key, every accepted value, and the rules that decide whether the form works once it is in.
It is written to be handed to an AI assistant. Give it this page along with the object and field list you want the form built against, and it has everything it needs to produce an importable file. It works just as well read by hand.
The file is one JSON object with two required keys, config for the form's settings and layout for its structure, plus an optional name:
{ "name": "My Form", "config": { /* settings */ }, "layout": { /* structure */ } }
To import it, open the Hub landing, click Import, paste the JSON, and confirm. A brand-new template is created every time, so nothing is overwritten and the same file can be imported more than once. See Exporting and importing templates.
Nothing checks your form's internal logic on import. Chronicle confirms the object and fields exist in this org and warns you about the ones that don't, but a page reference pointing nowhere, an orphaned page, or an operator that isn't recognized is accepted and then breaks quietly when someone fills the form out. An unrecognized operator fails closed: it evaluates false, so a gated section stays hidden and a conditional "required" never fires. Safe, and silently wrong. Correctness is yours to get right, which is what the Validation checklist is for.
The file
Only four keys are read at the top level. Anything else is ignored.
| Key | Type | Required | Notes |
|---|---|---|---|
config | object | yes | The form's settings. See The config object. |
layout | object | yes | Pages, flow, and structure. See The layout object. |
name | string | no | The form's display name. Omitted, it imports as "Imported Form". |
nestedTemplates | array | no | Child form bundles, used only by template-mode Related Records. An export emits it so the file stays self-contained, and on import each bundled child is created as a new form with the parent's references rewritten to match. Omit it when generating a form from scratch and use an inline Related Records cell instead. |
Three shape rules decide whether the file imports at all:
configandlayoutare nested JSON objects, not stringified JSON. Emit"config": { "sobjectName": "Contact" }, never"config": "{\"sobjectName\":...}".- Types are strict. The import deserializes into fixed types, so a mismatched value is rejected with "Illegal value for primitive". Emit integers and booleans as literals:
"span": 6,"active": true. - Quote the string-typed numeric fields. Condition
value,minValue,maxValue, andnumberStepare strings, so"value": "100"and never"value": 100.
What is filled in for you
Knowing this tells you what you do not have to get perfect.
- Input types are derived automatically. You can omit
frontEndTypeandbackEndTypeon Question cells. They come from the field you bind to, or fromcustomDataTypeon a cache cell. Don't set them. dependencyMapis computed for you. Always emit{}.- Explicit
nullvalues are harmless. A key set tonullis treated as absent. - IDs must be unique and correctly prefixed, and every reference to an ID has to match one that exists.
- Per-cell metadata is stamped on import, including
maxLength,referencedSobjectName, and the two type keys, as long asconfig.sobjectNameand eachfieldNameare correct.
IDs
Prefixed tokens, unique within the file, used consistently everywhere they are referenced.
| Element | Prefix | Example |
|---|---|---|
| Page | pg_ | pg_applicant1 |
| Section | sec_ | sec_contact01 |
| Cell | cell_ | cell_fname001 |
| Decision (flow node) | dec_ | dec_route001 |
Only uniqueness and consistent references matter. A flow pageId or a goto target has to match a page's id. A decision's id is its own.
The layout object
layout is a single JSON object with three required keys, plus two optional localization keys.
| Key | Type | Required | Default | Notes |
|---|---|---|---|---|
pages | array of pages | yes | [] | All page content. Order here is not presentation order. |
flow | array of flow nodes | yes | [] | Navigation: which pages show, in what order, with branching. An empty flow shows nothing. |
dependencyMap | object | yes | {} | Always emit {}. |
languages | array of strings | no | - | Enabled locale codes, base first. Omit for a form in one language. See Localization. |
translations | object | no | - | Per-locale string overlay. See Localization. |
{ "pages": [ /* page */ ], "flow": [ /* flow node */ ], "dependencyMap": {} }
A page in pages shows only if a flow node references its id, and presentation order is the order of flow, not of pages.
The bound object
A form usually targets one Salesforce object, named in config.sobjectName. Every Question cell's fieldName has to be a real field on that object. Display Text, Image, Divider, Blank, Signature, Scratchpad, and File Upload cells never bind a field.
The one exception is a Related Records cell, which collects rows of a different, child object. Its inner fields resolve against its own childObject.
Page
| Field | Type | Required | Default | Notes |
|---|---|---|---|---|
id | string | yes | - | pg_ prefix. |
title | string | no | - | Page heading, shown as the form header while on this page. Give every page one. |
active | boolean | no | true | false hides the page. |
hideContinue | boolean | no | false | Hide this page's Continue button. |
saveOnContinue | boolean | no | false | Save early: leaving this page writes the record immediately, so formula and computed fields resolve in merge tags on later pages. Only meaningful when config.saveMode is "onSubmit". |
width | string | no | - | Optional CSS width override. |
weight | string | no | - | Optional layout weight. |
nestedTemplateId | string | no | - | Not implemented. Don't emit it. The schema accepts it and no runtime honors it. For child records use a Related Records cell. |
sections | array of sections | yes | [] | One or more sections. |
Section
| Field | Type | Required | Default | Notes |
|---|---|---|---|---|
id | string | yes | - | sec_ prefix. |
label | string | no | - | Section heading. |
sortOrder | integer | no | 0 | Lower shows first. |
active | boolean | no | true | - |
conditions | section conditions | no | { "visibility": null } | Sections support visibility only. |
cells | array of cells | yes | [] | - |
Cell: common fields
These apply to a cell of any type.
| Field | Type | Required | Default | Notes |
|---|---|---|---|---|
id | string | yes | - | cell_ prefix. |
type | string | yes | - | One of the cell types. |
span | integer | no | 12 | 1 to 12 grid columns. Use 6 for inputs and 12 for Divider and Display Text. Set it explicitly. |
sortOrder | integer | no | 0 | Lower shows first within the section. |
active | boolean | no | true | - |
fieldLabel | string | no | - | Display label, on Question, Image, Signature, Scratchpad, and File Upload. |
helpText | string | no | - | Helper text under the label. |
variant | string | no | - | Multi-select picklist only: "rows" renders a checklist, omitted renders toggle buttons. |
conditions | cell conditions | no | { "visibility": null, "required": null, "readOnly": null } | See Conditions. |
Cell: Question
Only on cells whose type is Question.
| Field | Type | Required | Default | Notes |
|---|---|---|---|---|
fieldName | string | yes | - | API name on the bound object. |
frontEndType | string | no | derived | Filled in automatically. Omit it. |
backEndType | string | no | derived | Filled in automatically. Omit it. |
referencedSobjectName | string | for lookups | - | Target object of a lookup field. |
lookupTitleTemplate | string | no | - | The label on each lookup result, built from merge tags and resolved against the result record, such as "{!LastName} Household" or "{!Account.Name}". Blank uses Name. |
lookupFilter | condition group | no | - | Limits which records the lookup can find. Its conditions may use valueSource: "field". |
maxLength | integer | no | - | Text length cap. |
minValue | string | no | - | Number minimum. Quote it. |
maxValue | string | no | - | Number maximum. Quote it. |
numberStep | string | no | - | Number step. Quote it. |
picklistOptions | array of picklist options | no | - | Manual options, which override the field's own picklist when present. See the rules below. |
isRequired | boolean | no | false | Deprecated. Do not emit. Mark a cell required with an always-true conditions.required group instead. It is still honored at runtime and is remapped to a condition the next time the template is opened in the builder, so existing templates keep working, but it will be removed. |
isReadOnly | boolean | no | false | Static read-only. |
saveDataCache | boolean | no | false | true stores the value in the form's data cache instead of writing it to a record field. Use it when there is no bound object. |
customDataType | string | no | - | Pins the cell's input type when there is no field to derive it from. See below. |
picklistOptions has three rules worth stating separately, because each of them produces a form that looks right and behaves wrong:
- It is required if the choices have to be translated. A field's own picklist values are read at runtime and can never be localized.
- It is required on a cache cell typed
PICKLISTorMULTIPICKLIST. There is no field to read options from, so without it the form shows an empty dropdown. - Never emit
[]. An empty array overrides the field with no choices at all. Omit the key instead.
customDataType is a type pin. It wins over the field describe wherever both exist, and it does not itself require saveDataCache, though a cache Question cell should still set both. Use one of these uppercase keys:
STRING, TEXTAREA, EMAIL, PHONE, URL, PICKLIST, MULTIPICKLIST, BOOLEAN, INTEGER, DECIMAL, CURRENCY, PERCENT, DATE, DATETIME, TIME
They map to the input types listed in Field types and input types. A value outside this list is ignored and the cell falls back to the field describe. REFERENCE is not usable on a cache cell, because a lookup needs an object to search.
Cell: Display Text
| Field | Type | Required | Notes |
|---|---|---|---|
richText | string | yes | HTML. Supports {!Field_API_Name} merge tags, including relationship paths. |
Formatting survives into a generated PDF. The HTML goes to the renderer as it is, so bold, italic, underline, bulleted and numbered lists, links, headings, and color all print. Use real list markup (<ul><li>) for a checklist rather than faking bullets with characters. A plain value with no tags is fine and prints literally. For images use an Image cell, not an <img> tag.
Cell: Image
| Field | Type | Required | Notes |
|---|---|---|---|
imageUrl | string | yes | Image source URL. |
imageAltText | string | no | Alt text. |
imageLink | string | no | Click-through URL, opened in a new tab. Must be http(s), mailto:, or relative. Any other scheme is dropped. |
imageHeight | string | no | CSS length: "120px", "4rem", "50%". |
imageWidth | string | no | CSS length. |
imageFit | string | no | "contain", "cover", "fill", "none", or "scale-down". |
imageAlign | string | no | "left", "center", or "right". Applies when the image is narrower than the cell. |
imageRadius | string | no | CSS corner radius: "8px", "50%". |
There is also a legacy link field on the cell. Use imageLink, since link is not read. The length values are sanitized, so emit plain CSS lengths.
Cell: File Upload
| Field | Type | Required | Notes |
|---|---|---|---|
fileUploadConfig | object | no | Upload limits. |
All three limits are enforced when the respondent picks files, and the refusal names the file. Omit a key to leave that limit unset.
| Field | Type | Notes |
|---|---|---|
acceptedTypes | string | HTML accept syntax: extensions, MIME types, or wildcards, such as ".pdf,.doc,.docx" or "image/*". It also filters the file picker. Omit to accept anything. |
maxFiles | integer | Maximum files per cell. A longer selection keeps the first few and drops the rest. Omit for no count limit. |
maxSizeMb | integer | Per-file size limit, capped at 7 whatever you set. Files travel as base64, which inflates them by a third against a 10MB transport limit, so a larger value is silently reduced. Omit and 7 applies. |
Empty files are always rejected. There is nothing to store, and they fail at the upload service with an unhelpful error.
Cell: Scratchpad
A freehand drawing surface with pen, eraser, colors, and undo, whose result is uploaded as a normal file and mirrored onto the bound record like any other attachment. It is not a Signature, which is a fixed one-purpose capture whose file deliberately stays on the submission.
| Field | Type | Required | Default | Notes |
|---|---|---|---|---|
fieldLabel | string | no | Drawing | The prompt above the canvas. Translatable. |
padHeight | integer | no | 260 | Canvas height in pixels, clamped to between 140 and 800. Not a CSS length: this is an interactive surface, and a bad value makes it undrawable. |
It stores no field value, so it never appears in the answers table. The drawing arrives as an attachment on the submission. A required condition is honored, satisfied once a drawing has been saved, and that survives a resumed session.
Cell: Signature, Divider, Blank
No type-specific fields. Use the common fields; a Signature normally sets fieldLabel.
Cell types
Use these exact strings. The spacing is not uniform: two are two words, and RelatedRecords is one word with no space. An unrecognized type renders as nothing.
type | Binds a field? | Type-specific fields | Usual span |
|---|---|---|---|
Question | yes, through fieldName | Question fields | 6 |
Display Text | no | richText | 12 |
Image | no | imageUrl, imageAltText, display options | 12 |
Signature | no | none; set fieldLabel | 12 |
Scratchpad | no | padHeight, fieldLabel | 12 |
File Upload | no | fileUploadConfig | 12 |
RelatedRecords | binds a child object | childConfig | 12 |
DataTable | read-only rows from a field or a query | columns, dataSource, fieldName or tableQuery | 12 |
Divider | no | none | 12 |
Blank | no | none | 12 |
Microphone is accepted but hidden from the palette, along with a partial Map. Don't emit either unless you have been asked to.
Related Records
A RelatedRecords cell collects repeatable rows, each of which becomes a child record linked back to the submitted record. Use it whenever one submission has to produce many records, such as household members, line items, or findings. It is the only way to do one-to-many.
It needs config.sobjectName set, and childObject has to have a lookup field pointing at it. Rows materialize on completion, and only for a form bound to a record.
| Field | Type | Required | Default | Notes |
|---|---|---|---|---|
childObject | string | yes | - | API name of the object each row creates, such as "Contact". Optional in display mode. |
relationshipField | string | yes | - | The lookup on childObject that points back at config.sobjectName. It is stamped with the submitted record's Id. Omit it in display mode so rows never materialize. |
source | string | no | "inline" | "inline" defines the row fields here; "template" renders rows from another form. |
childSections | array of sections | for inline | - | The row layout: a normal sections array scoped to childObject. |
nestedTemplateFamilyKey | string | for template | - | The child form's family key. |
keyFields | array of strings | no | - | Field API names on childObject forming a composite match key. Set, a re-submission updates a matching record; unset, it always inserts. |
matchScope | string | no | "perParent" | "perParent" matches only rows already under this parent; "global" matches anywhere and re-links the record to this parent. Only read when keyFields is set. |
minRows | integer | no | - | Minimum rows, enforced before the user can continue. |
maxRows | integer | no | - | Maximum rows the user can add. |
nestedTemplateName | string | no | - | Display label only. The builder sets it, and it is safe to omit. |
nestedTemplateId | string | no | - | Reserved. Don't emit it. Binding is by family key. |
Prefer source: "inline" when generating a form. It is self-contained, so the whole cell lives in this one file. "template" binds to a separate form that has to already exist in the org, matched by its family key, so use it only when you have been asked to wire up an existing child form.
Four rules govern what can go inside:
- Inline rows accept
Question,Blank, andDividercells only, and no conditions. A row that needs conditional logic has to usesource: "template". fieldNameresolves againstchildObject, notconfig.sobjectName. Everything else about the cells follows the normal rules, and IDs still need their prefixes and still have to be unique across the whole file.- Depth is one. A
childSectionslayout, or a referenced child form, must not contain anotherRelatedRecordscell. Nested ones are silently dropped. - Conditions in a template-mode child can test the parent record by prefixing the field with
Parent., such as"field": "Parent.Type__c". Without the prefix the field resolves against the row. This works only in asource: "template"child.
{
"id": "cell_members1", "type": "RelatedRecords", "span": 12, "sortOrder": 2,
"fieldLabel": "Household Members",
"conditions": { "visibility": null, "required": null, "readOnly": null },
"childConfig": {
"childObject": "Contact",
"relationshipField": "AccountId",
"source": "inline",
"keyFields": ["Email"],
"matchScope": "perParent",
"minRows": 1,
"maxRows": 10,
"childSections": [
{
"id": "sec_member01", "label": "Member", "sortOrder": 0, "active": true,
"conditions": { "visibility": null },
"cells": [
{ "id": "cell_mfirst01", "type": "Question", "span": 6, "sortOrder": 0,
"fieldName": "FirstName", "fieldLabel": "First name",
"conditions": { "visibility": null, "required": null, "readOnly": null } },
{ "id": "cell_memail01", "type": "Question", "span": 6, "sortOrder": 1,
"fieldName": "Email", "fieldLabel": "Email",
"conditions": { "visibility": null, "required": null, "readOnly": null } }
]
}
]
}
}
Rows are added and updated, never deleted: removing a row on the form never deletes the record it created. Materialization is best-effort, so a row that fails is skipped and never blocks completion.
Display mode
A Related Records cell can be made read-only, either with a readOnly condition or with isReadOnly: true on the cell. A read-only repeater hides Add and Remove, renders its rows read-only, and never blocks submission on minRows.
That turns the cell into a display table: rows you populate from elsewhere, shown on the form and printed in the PDF. Three things change:
childObjectandrelationshipFieldare optional. With nochildObject, each child cell'sfieldNameis just a key into the row's data rather than a real field, so you must type every child cell withcustomDataType. OmitrelationshipFieldand the rows never materialize into records.- A form with no bound object is the natural fit. Materialization is skipped entirely for an unbound form, so a display table cannot create stray records whatever else is set.
- Rows are supplied server-side, staged into the submission's
Data_JSON__cunderchildren, keyed by the cell's id, by whatever creates the form. There is no client-side property for child rows. The viewer loads them when it starts and the PDF reads them on completion. See Where the data lives.
{
"id": "cell_admin_vax", "type": "RelatedRecords", "span": 12,
"fieldLabel": "Administered Vaccines", "isReadOnly": true,
"conditions": { "visibility": null, "required": null, "readOnly": null },
"childConfig": {
"source": "inline", "minRows": 0,
"childSections": [
{ "id": "sec_vrow", "label": null, "conditions": { "visibility": null },
"cells": [
{ "id": "cell_vname", "type": "Question", "fieldName": "name", "fieldLabel": "Vaccine",
"customDataType": "STRING",
"conditions": { "visibility": null, "required": null, "readOnly": null } },
{ "id": "cell_vdate", "type": "Question", "fieldName": "date", "fieldLabel": "Date",
"customDataType": "DATE",
"conditions": { "visibility": null, "required": null, "readOnly": null } }
] }
]
}
}
Data Table
A read-only, data-driven table: one header row and one row per object in a collection. Use it to show and print a set of rows in a real columnar table, rather than the repeated labeled groups a Related Records cell renders. It is never submitted or written. Column headers localize through the translation overlay, keyed by column key.
The rows come from one of two places, chosen by dataSource.
| Field | Type | Required | Notes |
|---|---|---|---|
dataSource | string | no | "field" (the default when absent) or "query". |
fieldLabel | string | no | Heading shown above the table. Localizable. |
columns | array of table columns | yes | The header row, and which key fills each column. |
fieldName | string | field mode | The field or cache key holding a JSON array of row objects. On an unbound form it is staged under the data cache; on a bound form it is a Long Text field on the record. |
tableQuery | object | query mode | The query that produces the rows. |
Table column, an entry of columns:
| Field | Type | Required | Notes |
|---|---|---|---|
key | string | yes | In field mode, the property read from each row object. In query mode, a field API name on the query object, which is selected and fills the column. It is also the localization key for this column's header. |
label | string | yes | The column header text. Localizable. |
tableQuery, query mode only:
| Field | Type | Required | Notes |
|---|---|---|---|
objectName | string | yes | The object to query. Column keys and orderByField have to be fields on it. |
filter | condition group | no | A filter. Each condition compares a field on the query object to a literal, or, with valueSource: "field", to a live value from the form, in which case the table re-queries whenever that field changes. |
orderByField | string | no | A field API name to sort by. |
orderDirection | string | no | "ASC" (the default) or "DESC". |
queryLimit | integer | no | Maximum rows. The server caps this at 500 regardless. |
Query mode runs when the form loads. Object and fields are validated and every value is bound, so it is injection-safe, but on a public form the results are visible to anyone who opens it, so query only data you are comfortable exposing. Values render raw: a picklist shows its stored value, a lookup its Id, a date its ISO string.
Field mode, for a collection you populate from elsewhere:
{ "id": "cell_admin_vax", "type": "DataTable", "span": 12, "dataSource": "field",
"fieldName": "adminVaccines", "fieldLabel": "Administered Vaccines",
"columns": [
{ "key": "name", "label": "Vaccine" },
{ "key": "lot", "label": "Lot #" },
{ "key": "date", "label": "Date" }
],
"conditions": { "visibility": null, "required": null, "readOnly": null } }
with the cache holding adminVaccines as [ { "name": "MMR", "lot": "AB1234", "date": "2026-07-17" } ].
Query mode, for live records related to something chosen on this form:
{ "id": "cell_prior_visits", "type": "DataTable", "span": 12, "dataSource": "query",
"fieldLabel": "Prior Visits",
"columns": [
{ "key": "Name", "label": "Visit" },
{ "key": "Visit_Date__c", "label": "Date" }
],
"tableQuery": {
"objectName": "Visit__c",
"filter": { "logic": "AND", "conditions": [
{ "index": 1, "field": "Contact__c", "operator": "Is Equal To", "value": "ContactId", "valueSource": "field" }
] },
"orderByField": "Visit_Date__c", "orderDirection": "DESC", "queryLimit": 20
},
"conditions": { "visibility": null, "required": null, "readOnly": null } }
Merge tags
A merge tag {!Field_API_Name} is replaced with a field's value when the form is shown, and live as the user types. Use the field's API name: {!FirstName}, {!Email__c}. A tag whose field is empty resolves to nothing; it is never left on screen as literal text.
Relationship paths. A tag can traverse parent lookups from the bound record with dot notation, up to five hops. Each segment before the last is a relationship name, not a field name:
- A custom lookup
Object_B__chas the relationship nameObject_B__r, so the tag is{!Object_B__r.Some_Field__c}. - A standard lookup
AccountIdhas the relationship nameAccount, so the tag is{!Account.Name}.
Four rules bound this:
- Bound forms only. With no bound record, or a blank lookup anywhere along the path, the tag resolves to empty.
- Polymorphic lookups are not traversable.
{!Owner.*},{!What.*}, and{!Who.*}are skipped. - An invalid path resolves to empty, like any unresolved tag. It never breaks the form.
- Write Only forms never read the record, so relationship tags resolve empty there.
Where merge tags work: Display Text richText, a cell's fieldLabel and helpText, the lookup result label lookupTitleTemplate (which resolves against each result record), and config.fieldDefaults[].value (which resolves against the bound record). Relationship paths work in all of them.
{ "id": "cell_greeting", "type": "Display Text", "span": 12,
"richText": "<p>Primary contact: {!Account.Name} ({!Account.Owner.Email})</p>",
"conditions": { "visibility": null, "required": null, "readOnly": null } }
Conditions
Section conditions
| Field | Type | Notes |
|---|---|---|
visibility | condition group or null | The only condition type a section supports. |
Cell conditions
| Field | Type | Notes |
|---|---|---|
visibility | condition group or null | Show or hide the cell. |
required | condition group or null | The only way to mark a cell required. Honored on Question, Signature, Scratchpad, and File Upload cells only. For always required, emit a group with alwaysTrue: true and an empty conditions array. For sometimes required, emit real conditions. |
readOnly | condition group or null | Conditionally read-only. Question cells only. |
Condition group
| Field | Type | Required | Default | Notes |
|---|---|---|---|---|
logic | string | no | "AND" | "AND", "OR", or "CUSTOM". |
customLogic | string | when logic is "CUSTOM" | - | Such as "1 AND (2 OR 3)", referencing each condition's index. |
alwaysTrue | boolean | no | false | Shortcut: the group is always satisfied. |
alwaysFalse | boolean | no | false | Shortcut: the group is never satisfied. |
conditions | array of conditions | yes, unless always true or false | [] | The rules. |
Condition
| Field | Type | Required | Default | Notes |
|---|---|---|---|---|
index | integer | yes | - | 1-based, and referenced by customLogic. |
field | string | yes | - | API name of the field being tested. Inside a template-mode Related Records child, a Parent. prefix tests the submitted record instead. |
operator | string | yes | - | From the list below, and valid for the field's input type. |
value | string or null | depends | - | null for unary operators. Quote it when it is a number. |
valueSource | string | no | "literal" | "literal", or "field" to resolve value as the API name of a live form field. |
Operators
Use these exact strings. An operator outside this list fails closed: it silently evaluates false, so the rule never fires.
Is Equal To, Is Not Equal To, Is Greater Than, Is Less Than, Is Greater Than or Equal To, Is Less Than or Equal To, Contains, Does Not Contain, Starts With, Is Checked, Is Not Checked, Is Blank, Is Not Blank
Four are unary and take value: null: Is Checked, Is Not Checked, Is Blank, Is Not Blank. There is no Is Empty; use Is Blank.
Which operators are valid depends on the field's input type:
| Input type | Operators |
|---|---|
text, textarea, email, tel, url | Is Equal To, Is Not Equal To, Contains, Does Not Contain, Starts With, Is Blank, Is Not Blank |
number | Is Equal To, Is Not Equal To, Is Greater Than, Is Less Than, Is Greater Than or Equal To, Is Less Than or Equal To, Is Blank, Is Not Blank |
checkbox | Is Checked, Is Not Checked |
picklist, lookup | Is Equal To, Is Not Equal To, Is Blank, Is Not Blank |
multipicklist | Contains, Does Not Contain, Is Blank, Is Not Blank |
date, datetime, time | The full comparison set: Is Equal To, Is Not Equal To, Is Greater Than, Is Less Than, Is Greater Than or Equal To, Is Less Than or Equal To, Is Blank, Is Not Blank |
Field types and input types
You don't set frontEndType, since it is derived from the field. This table is here so you know which input type a field becomes, which is what decides its valid operators.
| Field data type | Input type |
|---|---|
| Text | text |
| Long text area | textarea |
email | |
| Phone | tel |
| URL | url |
| Picklist | picklist |
| Multi-select picklist | multipicklist |
| Lookup or reference | lookup |
| Number, Currency, Percent | number |
| Date | date |
| Date/Time | datetime |
| Time | time |
| Checkbox | checkbox |
Flow
flow is an array of nodes that decides which pages appear and in what order.
type | Required fields | Meaning |
|---|---|---|
"page" | pageId | Show this page. |
"decision" | id, branches, defaultNodes | Route on conditions. |
"goto" | target | Jump to a page and end that path, with no merge back. Valid only as the last node in a sequence. |
| Field | Type | Notes |
|---|---|---|
type | string | "page", "decision", or "goto". |
pageId | string | On a page node, a page's id. |
id | string | On a decision node, with the dec_ prefix. |
branches | array of branches | On a decision node, the named branches. |
defaultNodes | array of flow nodes | On a decision node, the "else" path. Use [] if there is none. |
target | string | On a goto node, a page's id. |
Branch, an entry of branches:
| Field | Type | Required | Notes |
|---|---|---|---|
label | string | yes | Branch name. |
condition | condition group | yes | When this branch is taken. |
nodes | array of flow nodes | yes | Pages, decisions, and gotos inside the branch. They nest arbitrarily. |
The config object
The form's settings, the other half of the file. Every field is optional, and omitting one takes its default. Use exactly these string values, since an unrecognized one misbehaves silently.
| Field | Type | Default | Valid values and notes |
|---|---|---|---|
sobjectName | string | "" | API name of the bound object, such as "Contact". Blank means the form isn't bound to an object, in which case every Question cell has to set saveDataCache: true and nothing is written to a record. |
readWriteStatus | string | "Read/Write" | "Read/Write", "Read Only", or "Write Only". Write Only never loads or shows existing record data; users can still submit, and static and pre-fill literals still apply. This is direction only. For frozen-after-submit use lockBehavior. |
lockBehavior | string | "never" | "never" leaves the form open, so whoever holds its link can reopen it after submitting and change what they sent. "onCompleted" freezes it: reopening the link shows the completion screen, earlier answers are not loaded back, and no further save or signature is accepted. Set "onCompleted" for consents, attestations, and anything signed. |
saveMode | string | "onSubmit" | "onSubmit" stages values on each Continue and writes the record once, at submit or on a saveOnContinue page. "progressive" writes the record on every Continue. Leave it at the default unless asked, since it avoids half-finished records. |
dedupeMatchFields | array of strings | - | Field API names on the bound object. When the form would create a record, it first looks for an existing one matching all of these and updates that instead. Skipped if any value is blank, or if the form is already bound to a record. Pairs naturally with "onSubmit". |
captureSubmissionData | boolean | false | Writes the full submitted dataset to the submission log on every successful save. It stores PII by design, so set it true only when explicitly asked. |
onFinishAction | string | "show default" | "show default" shows the completion screen; "return to start" offers a Start again button. |
generatePdf | string | omitted | Omitted or null is off. Otherwise "On First Completion" or "On Each Completion". |
pdfConfig | object | omitted | PDF page layout, meaningful only when generatePdf is set. Omit it entirely unless asked; every sub-field falls back to a package default. |
borderStyle | string | "bordered" | "bordered" or "borderless". |
hideHeader | boolean | false | Hide the page-title bar. |
hideTitle | boolean | false | Hide the title within the header. |
hideFooter | boolean | false | Hide the Back, Continue, and Submit footer. |
defaultLanguage | string | "en-us" | The form's default language. On a localized form set it to the base locale, which is layout.languages[0], so an unspecified visitor language resolves to the base. |
fieldDefaults | array of field defaults | - | Pre-fill and static seed values. |
parentRecords | array of parent-record rules | - | Find or create a related record on completion and link it. |
styleOverrides | object | - | Branding overrides. Omit entirely unless branding was requested. |
PDF page layout
pdfConfig is all optional and all in inches, measured against whichever pageSize is set. Fractional values are normal and expected. They are the content box's insets from the page edges, and the column width is derived from them.
| Field | Type | Default | Notes |
|---|---|---|---|
marginTop | number | 0.5 | Where content starts, on every page, so it is what keeps content clear of a header baked into the background. With no background, room for the built-in form title is added on top of this automatically, so you don't need to pad for it. |
marginRight | number | 0.5 | Right inset. |
marginBottom | number | 0.5 | Reserves footer space, such as a signature line in the background artwork. Raise it when the artwork has a tall footer, or content overlaps it once a page fills up. |
marginLeft | number | 0.5 | Left inset. |
overflowTop | number | omitted | Top inset on continuation pages only, overriding marginTop there. Rarely needed. With no background, later pages already drop back to marginTop because they carry no title; with a background, marginTop already applies to them. Set it only to override that deliberately. |
backgroundUrl | string | omitted | Public https:// link to a PNG, JPG, or PDF drawn behind the content on every page. It has to be reachable without authentication, so a Salesforce file URL will not work. A value that isn't http(s), such as a relative path or a data URI, is silently dropped and the PDF renders without it. Setting a background also suppresses the built-in centered form title, on the assumption the artwork carries its own. |
backgroundType | string | derived | "png", "jpg", "jpeg", or "pdf". Normally derived from the file extension on backgroundUrl, ignoring any query string. Set it only when the URL has no extension, as a signed or rewritten link often doesn't, because the render service requires it alongside the asset and fails without it. |
pageSize | string | "letter" | "letter" (8.5 by 11 inches) or "a4" (8.27 by 11.69). Only an exact "a4" selects A4; anything else reads as Letter. It sets both the page and the background template, and the background is stretched to fill the page, so A4-shaped artwork on a Letter page comes out distorted. Match this to your artwork. |
Opposing margins that leave a content box narrower than 1.5 inches or shorter than 2.5 inches make that axis revert to its defaults, so a typo degrades rather than producing an unrenderable page. It is also why millimeter-scale numbers here are not merely wrong but silently ignored: 45 is not a plausible inch margin on an 11 inch page, so the axis reverts. Always emit inches.
Field defaults
Each entry of fieldDefaults:
| Field | Type | Required | Notes |
|---|---|---|---|
fieldName | string | yes | API name on the bound object. |
mode | string | yes | "prefill" seeds an empty field at load and the user can change it. "static" force-writes on every save. |
value | string | yes | A literal, or a {!Field_API_Name} merge tag including a relationship path. |
Parent record rules
On completion, each entry of parentRecords finds or creates one related record and stamps its Id onto a lookup field of the submitted record. It is the inverse of a Related Records cell: that one creates many records pointing at this one, this one points this record at one other.
Bound forms only, and best-effort: a rule that fails is skipped and never blocks completion. Where more than one record matches, the most recently modified wins.
| Field | Type | Required | Default | Notes |
|---|---|---|---|---|
lookupField | string | yes | - | A lookup field on config.sobjectName that receives the Id. |
targetObject | string | yes | - | The object lookupField points to. It has to match, or the rule is skipped. |
createWhenNotFound | boolean | no | true | true creates the target from all mappings when nothing matches. false leaves the lookup empty. |
mappings | array of field mappings | yes | - | Which fields carry across. |
Each entry of mappings:
| Field | Type | Required | Default | Notes |
|---|---|---|---|---|
fromField | string | yes | - | Field API name on config.sobjectName. |
toField | string | yes | - | Field API name on targetObject. |
matchKey | boolean | no | false | Exactly one mapping has to set true. It is the equality test used to find the record. All mappings are written when creating one. |
"parentRecords": [
{
"lookupField": "Contact__c",
"targetObject": "Contact",
"createWhenNotFound": true,
"mappings": [
{ "fromField": "Email__c", "toField": "Email", "matchKey": true },
{ "fromField": "First_Name__c", "toField": "FirstName", "matchKey": false },
{ "fromField": "Last_Name__c", "toField": "LastName", "matchKey": false }
]
}
]
Branding
styleOverrides needs enabled: true for the color, font, and radius overrides to apply. maxWidth applies either way.
| Field | Type | Notes |
|---|---|---|
enabled | boolean | Master toggle for the overrides. Defaults to false. |
accentColor | string | CSS color for the primary accent. |
textColor | string | CSS color for body text. |
backgroundColor | string | CSS color for the form surface. |
fontFamily | string | CSS font stack. |
cornerRadius | string | Such as "0px", "8px", "14px". |
maxWidth | string | Card maximum width. Blank means no cap, which renders at 900px. |
Picklist option
Each entry of a cell's picklistOptions:
| Field | Type | Required | Default | Notes |
|---|---|---|---|---|
label | string | yes | - | Shown to the user. |
value | string | yes | - | Stored value. |
language | string | no | en | Legacy per-option tag, not used for display. Translate option labels through the translation overlay instead. |
Localization
A form can be offered in several languages within one form. Structure and logic are shared, and only display strings are translated. Two optional keys on layout do it:
languages, an array of lowercase locale codes such as["en-us", "es-es"]. The first is the base, and its strings live in the pages, sections, and cells directly, never duplicated. Omit the key entirely for a form in one language.translations, a sparse overlay keyed locale, then element id, then the translated fields. Only non-base locales appear, only the fields you have translated appear, and anything missing (a field, an element, or a whole locale) falls back to the base string.
What is translatable: a page's title, a section's label, a cell's fieldLabel, helpText, and richText, manual picklist option labels, and Data Table column headers. Everything else, including field bindings, conditions, flow, spans, and IDs, is shared across every language and must never go in the overlay.
A translation entry holds only the fields relevant to its element type:
| Field | Type | Applies to | Notes |
|---|---|---|---|
title | string | Page | Translated page title. |
label | string | Section | Translated section label. |
fieldLabel | string | Cell | Translated cell label. |
helpText | string | Cell | Translated helper text. |
richText | string | Display Text cell | Translated rich text. |
picklistOptions | object | Question cell | A map of option value to translated label, for manual options only. |
columns | object | Data Table cell | A map of column key to translated header. |
How it is shown: the form displays whichever language best matches the visitor, which is a requested language, else the submission's language, else config.defaultLanguage, falling back to the base language for anything not translated. Locale matching is case-insensitive.
{
"pages": [
{ "id": "pg_1", "title": "Welcome",
"sections": [ { "id": "sec_1", "label": "Your details",
"cells": [
{ "id": "cell_1", "type": "Question", "fieldName": "FirstName",
"fieldLabel": "First name", "helpText": "Your given name",
"conditions": { "visibility": null, "required": null, "readOnly": null } }
] } ] }
],
"flow": [ { "type": "page", "pageId": "pg_1" } ],
"dependencyMap": {},
"languages": ["en-us", "es-es"],
"translations": {
"es-es": {
"pg_1": { "title": "Bienvenido" },
"sec_1": { "label": "Sus datos" },
"cell_1": { "fieldLabel": "Nombre", "helpText": "Su nombre de pila" }
}
}
}
Validation checklist
A file is well-formed when all of these hold.
Structure
- The top level has
configandlayout, both objects, and optionally anamestring. -
layouthaspages(array),flow(array), anddependencyMapset to{}. - Every page has an
idwith thepg_prefix and at least one section; every section has anidwithsec_and acellsarray; every cell has anidwithcell_and atype. - All IDs are unique and correctly prefixed.
- Sections include
{ "visibility": null }and cells include the full{ "visibility": null, "required": null, "readOnly": null }, even when unused.
Flow
- Every
pageIdand everygototargetmatches a page that exists. - Every page you intend to show is reachable from
flow. Orphan pages never render. - Each decision node has an
idwith thedec_prefix,branches, anddefaultNodes, using[]when there are none. - Each branch has a
label, acondition, andnodes. - A
gotoappears only as the last node in its sequence.
Cells
- Every Question cell has a
fieldNamethat exists onconfig.sobjectName. - Lookup Question cells set
referencedSobjectName. -
spanis between 1 and 12. - Non-Question cells do not set
fieldName. - Every
typeis spelled exactly as listed:RelatedRecordshas no space,Display TextandFile Uploaddo.
Related Records
-
childObjectandrelationshipFieldare set, andrelationshipFieldis a real lookup onchildObjectpointing atconfig.sobjectName. -
source: "inline"setschildSections;source: "template"setsnestedTemplateFamilyKey. - Cells in
childSectionsare only Question, Blank, or Divider, and carry no conditions. - Every
fieldNameinchildSectionsexists onchildObject, not on the form's own object. - Child section and cell IDs are prefixed and unique across the whole file.
- No
RelatedRecordscell is nested insidechildSections. -
matchScopeis set only alongsidekeyFields.
Conditions
- Section conditions use
visibilityonly. Cell conditions usevisibility,required, andreadOnly, withrequiredonly on Question, Signature, Scratchpad, and File Upload, andreadOnlyonly on Question. - No
isRequiredanywhere. A required cell carries an always-trueconditions.requiredgroup instead. - Every operator is from the canonical list and valid for that field's input type.
- Unary operators have
value: null; every other operator has a value. -
logic: "CUSTOM"includescustomLogicreferencing each condition'sindex. - Each condition has a 1-based
index.
Config
-
config.sobjectNameis set. If the form is intentionally unbound, every Question cell hassaveDataCache: trueand acustomDataType, and everyPICKLISTorMULTIPICKLISTcell has its ownpicklistOptions. - Every Question
fieldName, everyfieldused in a condition, and everyfieldDefaultsentry exists onconfig.sobjectName. -
readWriteStatus,onFinishAction,generatePdf,borderStyle, andsaveModeuse the exact strings listed, or are omitted. -
fieldDefaults[].modeis"prefill"or"static", andstyleOverrides.enabledistrueif any color, font, or radius override is meant to apply. -
captureSubmissionDataistrueonly if PII retention was explicitly asked for. - Every
dedupeMatchFieldsentry exists onconfig.sobjectName. - In each
parentRecordsrule,lookupFieldis a lookup onconfig.sobjectName,targetObjectmatches what it points to, and exactly one mapping hasmatchKey: true.
If the form is localized
-
languages[0]is the base, and every other entry has an entry undertranslations, which may be partial. - Every key in
translationsis a locale listed inlanguages, and every element id exists inpages. -
translationscarries only translatable fields, and no structural ones. - Picklist translations are keyed by option
value, not by label or index. - Every translated picklist cell defines
picklistOptionson the base cell. A native field picklist has nothing to overlay, so its choices would render in the org's language whatever the locale. -
config.defaultLanguageequalslanguages[0].
The single most common mistake is a layout field that doesn't exist on the configured object. It imports, and the form is broken. Check every fieldName against config.sobjectName before you deliver the file.
A complete file
Everything above, at its smallest: one bound page with two required fields.
{
"name": "Contact form",
"config": {
"sobjectName": "Contact",
"readWriteStatus": "Read/Write",
"onFinishAction": "show default",
"borderStyle": "bordered",
"hideHeader": false,
"hideTitle": false,
"hideFooter": false,
"defaultLanguage": "en-us"
},
"layout": {
"pages": [
{
"id": "pg_main001", "title": "Contact details", "active": true,
"sections": [
{
"id": "sec_main001", "label": "About you", "sortOrder": 0, "active": true,
"conditions": { "visibility": null },
"cells": [
{
"id": "cell_first01", "type": "Question", "span": 6, "sortOrder": 0,
"fieldName": "FirstName", "fieldLabel": "First name",
"conditions": { "visibility": null, "required": { "logic": "AND", "conditions": [], "alwaysTrue": true, "alwaysFalse": false }, "readOnly": null }
},
{
"id": "cell_last001", "type": "Question", "span": 6, "sortOrder": 1,
"fieldName": "LastName", "fieldLabel": "Last name",
"conditions": { "visibility": null, "required": { "logic": "AND", "conditions": [], "alwaysTrue": true, "alwaysFalse": false }, "readOnly": null }
}
]
}
]
}
],
"flow": [ { "type": "page", "pageId": "pg_main001" } ],
"dependencyMap": {}
}
}