Skip to main content

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.

KeyTypeRequiredNotes
configobjectyesThe form's settings. See The config object.
layoutobjectyesPages, flow, and structure. See The layout object.
namestringnoThe form's display name. Omitted, it imports as "Imported Form".
nestedTemplatesarraynoChild 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:

  • config and layout are 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, and numberStep are 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.

  1. Input types are derived automatically. You can omit frontEndType and backEndType on Question cells. They come from the field you bind to, or from customDataType on a cache cell. Don't set them.
  2. dependencyMap is computed for you. Always emit {}.
  3. Explicit null values are harmless. A key set to null is treated as absent.
  4. IDs must be unique and correctly prefixed, and every reference to an ID has to match one that exists.
  5. Per-cell metadata is stamped on import, including maxLength, referencedSobjectName, and the two type keys, as long as config.sobjectName and each fieldName are correct.

IDs

Prefixed tokens, unique within the file, used consistently everywhere they are referenced.

ElementPrefixExample
Pagepg_pg_applicant1
Sectionsec_sec_contact01
Cellcell_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.

KeyTypeRequiredDefaultNotes
pagesarray of pagesyes[]All page content. Order here is not presentation order.
flowarray of flow nodesyes[]Navigation: which pages show, in what order, with branching. An empty flow shows nothing.
dependencyMapobjectyes{}Always emit {}.
languagesarray of stringsno-Enabled locale codes, base first. Omit for a form in one language. See Localization.
translationsobjectno-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

FieldTypeRequiredDefaultNotes
idstringyes-pg_ prefix.
titlestringno-Page heading, shown as the form header while on this page. Give every page one.
activebooleannotruefalse hides the page.
hideContinuebooleannofalseHide this page's Continue button.
saveOnContinuebooleannofalseSave 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".
widthstringno-Optional CSS width override.
weightstringno-Optional layout weight.
nestedTemplateIdstringno-Not implemented. Don't emit it. The schema accepts it and no runtime honors it. For child records use a Related Records cell.
sectionsarray of sectionsyes[]One or more sections.

Section

FieldTypeRequiredDefaultNotes
idstringyes-sec_ prefix.
labelstringno-Section heading.
sortOrderintegerno0Lower shows first.
activebooleannotrue-
conditionssection conditionsno{ "visibility": null }Sections support visibility only.
cellsarray of cellsyes[]-

Cell: common fields

These apply to a cell of any type.

FieldTypeRequiredDefaultNotes
idstringyes-cell_ prefix.
typestringyes-One of the cell types.
spanintegerno121 to 12 grid columns. Use 6 for inputs and 12 for Divider and Display Text. Set it explicitly.
sortOrderintegerno0Lower shows first within the section.
activebooleannotrue-
fieldLabelstringno-Display label, on Question, Image, Signature, Scratchpad, and File Upload.
helpTextstringno-Helper text under the label.
variantstringno-Multi-select picklist only: "rows" renders a checklist, omitted renders toggle buttons.
conditionscell conditionsno{ "visibility": null, "required": null, "readOnly": null }See Conditions.

Cell: Question

Only on cells whose type is Question.

FieldTypeRequiredDefaultNotes
fieldNamestringyes-API name on the bound object.
frontEndTypestringnoderivedFilled in automatically. Omit it.
backEndTypestringnoderivedFilled in automatically. Omit it.
referencedSobjectNamestringfor lookups-Target object of a lookup field.
lookupTitleTemplatestringno-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.
lookupFiltercondition groupno-Limits which records the lookup can find. Its conditions may use valueSource: "field".
maxLengthintegerno-Text length cap.
minValuestringno-Number minimum. Quote it.
maxValuestringno-Number maximum. Quote it.
numberStepstringno-Number step. Quote it.
picklistOptionsarray of picklist optionsno-Manual options, which override the field's own picklist when present. See the rules below.
isRequiredbooleannofalseDeprecated. 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.
isReadOnlybooleannofalseStatic read-only.
saveDataCachebooleannofalsetrue 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.
customDataTypestringno-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 PICKLIST or MULTIPICKLIST. 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

FieldTypeRequiredNotes
richTextstringyesHTML. 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

FieldTypeRequiredNotes
imageUrlstringyesImage source URL.
imageAltTextstringnoAlt text.
imageLinkstringnoClick-through URL, opened in a new tab. Must be http(s), mailto:, or relative. Any other scheme is dropped.
imageHeightstringnoCSS length: "120px", "4rem", "50%".
imageWidthstringnoCSS length.
imageFitstringno"contain", "cover", "fill", "none", or "scale-down".
imageAlignstringno"left", "center", or "right". Applies when the image is narrower than the cell.
imageRadiusstringnoCSS 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

FieldTypeRequiredNotes
fileUploadConfigobjectnoUpload 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.

FieldTypeNotes
acceptedTypesstringHTML accept syntax: extensions, MIME types, or wildcards, such as ".pdf,.doc,.docx" or "image/*". It also filters the file picker. Omit to accept anything.
maxFilesintegerMaximum files per cell. A longer selection keeps the first few and drops the rest. Omit for no count limit.
maxSizeMbintegerPer-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.

FieldTypeRequiredDefaultNotes
fieldLabelstringnoDrawingThe prompt above the canvas. Translatable.
padHeightintegerno260Canvas 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.

typeBinds a field?Type-specific fieldsUsual span
Questionyes, through fieldNameQuestion fields6
Display TextnorichText12
ImagenoimageUrl, imageAltText, display options12
Signaturenonone; set fieldLabel12
ScratchpadnopadHeight, fieldLabel12
File UploadnofileUploadConfig12
RelatedRecordsbinds a child objectchildConfig12
DataTableread-only rows from a field or a querycolumns, dataSource, fieldName or tableQuery12
Dividernonone12
Blanknonone12

Microphone is accepted but hidden from the palette, along with a partial Map. Don't emit either unless you have been asked to.


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.

FieldTypeRequiredDefaultNotes
childObjectstringyes-API name of the object each row creates, such as "Contact". Optional in display mode.
relationshipFieldstringyes-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.
sourcestringno"inline""inline" defines the row fields here; "template" renders rows from another form.
childSectionsarray of sectionsfor inline-The row layout: a normal sections array scoped to childObject.
nestedTemplateFamilyKeystringfor template-The child form's family key.
keyFieldsarray of stringsno-Field API names on childObject forming a composite match key. Set, a re-submission updates a matching record; unset, it always inserts.
matchScopestringno"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.
minRowsintegerno-Minimum rows, enforced before the user can continue.
maxRowsintegerno-Maximum rows the user can add.
nestedTemplateNamestringno-Display label only. The builder sets it, and it is safe to omit.
nestedTemplateIdstringno-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, and Divider cells only, and no conditions. A row that needs conditional logic has to use source: "template".
  • fieldName resolves against childObject, not config.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 childSections layout, or a referenced child form, must not contain another RelatedRecords cell. 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 a source: "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:

  • childObject and relationshipField are optional. With no childObject, each child cell's fieldName is just a key into the row's data rather than a real field, so you must type every child cell with customDataType. Omit relationshipField and 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__c under children, 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.

FieldTypeRequiredNotes
dataSourcestringno"field" (the default when absent) or "query".
fieldLabelstringnoHeading shown above the table. Localizable.
columnsarray of table columnsyesThe header row, and which key fills each column.
fieldNamestringfield modeThe 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.
tableQueryobjectquery modeThe query that produces the rows.

Table column, an entry of columns:

FieldTypeRequiredNotes
keystringyesIn 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.
labelstringyesThe column header text. Localizable.

tableQuery, query mode only:

FieldTypeRequiredNotes
objectNamestringyesThe object to query. Column keys and orderByField have to be fields on it.
filtercondition groupnoA 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.
orderByFieldstringnoA field API name to sort by.
orderDirectionstringno"ASC" (the default) or "DESC".
queryLimitintegernoMaximum 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__c has the relationship name Object_B__r, so the tag is {!Object_B__r.Some_Field__c}.
  • A standard lookup AccountId has the relationship name Account, 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

FieldTypeNotes
visibilitycondition group or nullThe only condition type a section supports.

Cell conditions

FieldTypeNotes
visibilitycondition group or nullShow or hide the cell.
requiredcondition group or nullThe 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.
readOnlycondition group or nullConditionally read-only. Question cells only.

Condition group

FieldTypeRequiredDefaultNotes
logicstringno"AND""AND", "OR", or "CUSTOM".
customLogicstringwhen logic is "CUSTOM"-Such as "1 AND (2 OR 3)", referencing each condition's index.
alwaysTruebooleannofalseShortcut: the group is always satisfied.
alwaysFalsebooleannofalseShortcut: the group is never satisfied.
conditionsarray of conditionsyes, unless always true or false[]The rules.

Condition

FieldTypeRequiredDefaultNotes
indexintegeryes-1-based, and referenced by customLogic.
fieldstringyes-API name of the field being tested. Inside a template-mode Related Records child, a Parent. prefix tests the submitted record instead.
operatorstringyes-From the list below, and valid for the field's input type.
valuestring or nulldepends-null for unary operators. Quote it when it is a number.
valueSourcestringno"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 typeOperators
text, textarea, email, tel, urlIs Equal To, Is Not Equal To, Contains, Does Not Contain, Starts With, Is Blank, Is Not Blank
numberIs 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
checkboxIs Checked, Is Not Checked
picklist, lookupIs Equal To, Is Not Equal To, Is Blank, Is Not Blank
multipicklistContains, Does Not Contain, Is Blank, Is Not Blank
date, datetime, timeThe 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 typeInput type
Texttext
Long text areatextarea
Emailemail
Phonetel
URLurl
Picklistpicklist
Multi-select picklistmultipicklist
Lookup or referencelookup
Number, Currency, Percentnumber
Datedate
Date/Timedatetime
Timetime
Checkboxcheckbox

Flow

flow is an array of nodes that decides which pages appear and in what order.

typeRequired fieldsMeaning
"page"pageIdShow this page.
"decision"id, branches, defaultNodesRoute on conditions.
"goto"targetJump to a page and end that path, with no merge back. Valid only as the last node in a sequence.
FieldTypeNotes
typestring"page", "decision", or "goto".
pageIdstringOn a page node, a page's id.
idstringOn a decision node, with the dec_ prefix.
branchesarray of branchesOn a decision node, the named branches.
defaultNodesarray of flow nodesOn a decision node, the "else" path. Use [] if there is none.
targetstringOn a goto node, a page's id.

Branch, an entry of branches:

FieldTypeRequiredNotes
labelstringyesBranch name.
conditioncondition groupyesWhen this branch is taken.
nodesarray of flow nodesyesPages, 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.

FieldTypeDefaultValid values and notes
sobjectNamestring""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.
readWriteStatusstring"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.
lockBehaviorstring"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.
saveModestring"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.
dedupeMatchFieldsarray 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".
captureSubmissionDatabooleanfalseWrites 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.
onFinishActionstring"show default""show default" shows the completion screen; "return to start" offers a Start again button.
generatePdfstringomittedOmitted or null is off. Otherwise "On First Completion" or "On Each Completion".
pdfConfigobjectomittedPDF page layout, meaningful only when generatePdf is set. Omit it entirely unless asked; every sub-field falls back to a package default.
borderStylestring"bordered""bordered" or "borderless".
hideHeaderbooleanfalseHide the page-title bar.
hideTitlebooleanfalseHide the title within the header.
hideFooterbooleanfalseHide the Back, Continue, and Submit footer.
defaultLanguagestring"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.
fieldDefaultsarray of field defaults-Pre-fill and static seed values.
parentRecordsarray of parent-record rules-Find or create a related record on completion and link it.
styleOverridesobject-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.

FieldTypeDefaultNotes
marginTopnumber0.5Where 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.
marginRightnumber0.5Right inset.
marginBottomnumber0.5Reserves 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.
marginLeftnumber0.5Left inset.
overflowTopnumberomittedTop 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.
backgroundUrlstringomittedPublic 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.
backgroundTypestringderived"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.
pageSizestring"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:

FieldTypeRequiredNotes
fieldNamestringyesAPI name on the bound object.
modestringyes"prefill" seeds an empty field at load and the user can change it. "static" force-writes on every save.
valuestringyesA 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.

FieldTypeRequiredDefaultNotes
lookupFieldstringyes-A lookup field on config.sobjectName that receives the Id.
targetObjectstringyes-The object lookupField points to. It has to match, or the rule is skipped.
createWhenNotFoundbooleannotruetrue creates the target from all mappings when nothing matches. false leaves the lookup empty.
mappingsarray of field mappingsyes-Which fields carry across.

Each entry of mappings:

FieldTypeRequiredDefaultNotes
fromFieldstringyes-Field API name on config.sobjectName.
toFieldstringyes-Field API name on targetObject.
matchKeybooleannofalseExactly 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.

FieldTypeNotes
enabledbooleanMaster toggle for the overrides. Defaults to false.
accentColorstringCSS color for the primary accent.
textColorstringCSS color for body text.
backgroundColorstringCSS color for the form surface.
fontFamilystringCSS font stack.
cornerRadiusstringSuch as "0px", "8px", "14px".
maxWidthstringCard maximum width. Blank means no cap, which renders at 900px.

Picklist option

Each entry of a cell's picklistOptions:

FieldTypeRequiredDefaultNotes
labelstringyes-Shown to the user.
valuestringyes-Stored value.
languagestringnoenLegacy 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:

FieldTypeApplies toNotes
titlestringPageTranslated page title.
labelstringSectionTranslated section label.
fieldLabelstringCellTranslated cell label.
helpTextstringCellTranslated helper text.
richTextstringDisplay Text cellTranslated rich text.
picklistOptionsobjectQuestion cellA map of option value to translated label, for manual options only.
columnsobjectData Table cellA 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 config and layout, both objects, and optionally a name string.
  • layout has pages (array), flow (array), and dependencyMap set to {}.
  • Every page has an id with the pg_ prefix and at least one section; every section has an id with sec_ and a cells array; every cell has an id with cell_ and a type.
  • 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 pageId and every goto target matches a page that exists.
  • Every page you intend to show is reachable from flow. Orphan pages never render.
  • Each decision node has an id with the dec_ prefix, branches, and defaultNodes, using [] when there are none.
  • Each branch has a label, a condition, and nodes.
  • A goto appears only as the last node in its sequence.

Cells

  • Every Question cell has a fieldName that exists on config.sobjectName.
  • Lookup Question cells set referencedSobjectName.
  • span is between 1 and 12.
  • Non-Question cells do not set fieldName.
  • Every type is spelled exactly as listed: RelatedRecords has no space, Display Text and File Upload do.

Related Records

  • childObject and relationshipField are set, and relationshipField is a real lookup on childObject pointing at config.sobjectName.
  • source: "inline" sets childSections; source: "template" sets nestedTemplateFamilyKey.
  • Cells in childSections are only Question, Blank, or Divider, and carry no conditions.
  • Every fieldName in childSections exists on childObject, not on the form's own object.
  • Child section and cell IDs are prefixed and unique across the whole file.
  • No RelatedRecords cell is nested inside childSections.
  • matchScope is set only alongside keyFields.

Conditions

  • Section conditions use visibility only. Cell conditions use visibility, required, and readOnly, with required only on Question, Signature, Scratchpad, and File Upload, and readOnly only on Question.
  • No isRequired anywhere. A required cell carries an always-true conditions.required group 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" includes customLogic referencing each condition's index.
  • Each condition has a 1-based index.

Config

  • config.sobjectName is set. If the form is intentionally unbound, every Question cell has saveDataCache: true and a customDataType, and every PICKLIST or MULTIPICKLIST cell has its own picklistOptions.
  • Every Question fieldName, every field used in a condition, and every fieldDefaults entry exists on config.sobjectName.
  • readWriteStatus, onFinishAction, generatePdf, borderStyle, and saveMode use the exact strings listed, or are omitted.
  • fieldDefaults[].mode is "prefill" or "static", and styleOverrides.enabled is true if any color, font, or radius override is meant to apply.
  • captureSubmissionData is true only if PII retention was explicitly asked for.
  • Every dedupeMatchFields entry exists on config.sobjectName.
  • In each parentRecords rule, lookupField is a lookup on config.sobjectName, targetObject matches what it points to, and exactly one mapping has matchKey: true.

If the form is localized

  • languages[0] is the base, and every other entry has an entry under translations, which may be partial.
  • Every key in translations is a locale listed in languages, and every element id exists in pages.
  • translations carries only translatable fields, and no structural ones.
  • Picklist translations are keyed by option value, not by label or index.
  • Every translated picklist cell defines picklistOptions on 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.defaultLanguage equals languages[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": {}
}
}