Skip to main content

Developer Guide

How to wire Flourish Chronicle into custom code: Lightning pages, your own Lightning web components, Flows, and Apex.

Designing and publishing the forms themselves is entirely point-and-click. See the Form Builder guide for that.

The Form Viewer component

The runtime component is Chronicle Form Viewer (API name chron_FormViewer). Place it on App, Home, and Record pages, Experience (community) pages, and Flow screens, or embed it inside your own LWC.

Property (App Builder)LWC attributeWhat it takes
Form Keyomni-keyHow the form is resolved, see below. Required.
Record IDrecord-idOptional Salesforce record to bind the form to.
Pre-fill Values (JSON)prefill-json{"Field__c": "value"}, seeds empty fields; the user can still change them.
Static Values (JSON)static-json{"Field__c": "value"}, written on every save, overriding user input.

Pre-fill and static maps accept literal values only. Merge tags are not honored here; only builder-entered defaults are.

How a form is resolved: the Public ID rule

Form Key (omniKey) accepts exactly three things:

ValueResult
A submission's Public ID (XXXXXX-XX-XXXXXX)Reopens that exact submission, including any saved progress.
A Template IDOpens that template, finding or creating a submission.
A guest share keyOpens whichever version of that form is currently active.

A raw Salesforce record Id will never open a form. Salesforce record Ids are close enough to sequential that they could be guessed, so a submission is only ever addressable by its unguessable Public ID, Form__c.Public_Id__c, generated automatically the moment the submission is created.

If you're holding a Form__c record Id and need to open that submission, query its Public_Id__c and pass that:

String publicId = [SELECT toflourish__Public_Id__c
FROM toflourish__Form__c
WHERE Id = :formRecordId LIMIT 1].toflourish__Public_Id__c;

URL parameters

Any page hosting the Form Viewer honors these:

ParameterPurpose
c__chronfidReopen a submission by its Public ID.
c__chronskOpen a form by its guest share key (always the active version).
c__chrontidOpen a form by Template ID.
c__chronprefillJSON map of field API name to literal value (pre-fill).
c__chronstaticJSON map of field API name to literal value (static).
c__chronlangRender in a specific locale (e.g. es-es).

Each parameter also works without the c__ prefix (chronfid, chronsk, and so on) if that's easier for your host page.

A Form Key set on the component takes precedence over the URL.

Embedding the viewer in your own LWC

<toflourish-chron_form-viewer
omni-key={formPublicId}
record-id={recordId}
onfinish={handleFinish}>
</toflourish-chron_form-viewer>

The viewer fires a finish event when the user completes the form:

handleFinish(event) {
const { formId, publicId, recordId, templateId, status } = event.detail;
// refresh your data, navigate away, etc.
}

Three things to know:

  • The form key is read once, at init. Bind it before the component first renders. If you resolve the key asynchronously, guard the component (for example with lwc:if) so it mounts only once the key is available.
  • Chrome is template config, not component properties. Hiding the header, title, or footer, borderless mode, and button labels are all set on the template in its Behavior and Display panels. There are no attributes for them.
  • Don't nest viewers to build a parent and child form. If one submission needs to create many child records, that's a Related Records cell in the builder, not a viewer inside a viewer. The viewer has no property or event for nesting.

Booting a form programmatically (Flow)

Use the Boot Chronicle Form invocable action (category Chronicle) to create a submission and get everything you need to link to it:

InputNotes
Template IdRequired.
Record IdOptional, the record to bind the form to. Chronicle derives the object name from it automatically.
LanguageOptional BCP-47 locale; defaults from the template.
OutputNotes
Form IdThe created Form__c record Id.
Public IdThe public handle. Use this in links, never the record Id.
Viewer URLA ready-made link (?c__chronfid=…) against your Public Form Site, or the internal viewer tab if no site is configured.

The action is bulk-safe, so it's fine in a scheduled or record-triggered Flow that processes many records at once.

Booting a form from Apex

Insert a Form__c with the template, and optionally the record to bind. Chronicle's trigger fills in the rest: the Public ID, the bound object's name (derived from the record Id), the default language, and a readable record Name.

toflourish__Form__c f = new toflourish__Form__c(
toflourish__Form_Template__c = templateId,
toflourish__SFDC_Record_Id__c = recordId // optional
);
insert f;

// Re-query the Public ID. It's set by the trigger and isn't reliably
// readable off the just-inserted record.
String publicId = [SELECT toflourish__Public_Id__c
FROM toflourish__Form__c
WHERE Id = :f.Id LIMIT 1].toflourish__Public_Id__c;

Branding from the host page

The viewer exposes its design tokens as CSS custom properties. Override any of them on the host element to theme a specific placement. This is the code-level equivalent of the builder's Branding panel.

c-chron_form-viewer {
--chron-primary: #e11d48;
--chron-bg: #fef2f2;
--chron-font-body: 'DM Sans', sans-serif;
}

Related tokens follow the same naming, including --chron-primary-hover, --chron-primary-light, --chron-primary-text, --chron-card-bg, --chron-border, --chron-error, and --chron-font-display.

Embedding a form on a website

A form can run on any website outside Salesforce. Open the template, and in the Export & Publish section click Copy snippet. The snippet is a single script tag:

<!-- Flourish Chronicle Form -->
<script src="https://cdn.toflourish.org/chronicle/v1/embed-form.js"
data-form="YOUR_SHARE_KEY"
data-site="https://yourorg.my.site.com/form"
data-max-width="480"></script>

data-form is the form's guest share key, so the embed always renders whichever version is currently active. data-site is your Form Embed Site, configured once for the org. Adjust data-max-width to suit your layout.

The loader mounts the form in an iframe and resizes it as the form changes height, so it never scrolls internally or leaves a gap. All of the widget logic lives in the hosted script, so fixes reach embedded sites without anyone re-pasting a snippet.

Listening to the form

An iframe can't size itself or tell you what's happening inside it, so the form posts messages to the page that embeds it. Every message shares one envelope:

{ fl: 1, product: 'chronicle', instance: null, event: 'navigate', data: { step: 2, of: 4, id: 'page_x' } }

instance identifies which embed sent the message when a page holds more than one, set with the c__chroninst parameter on the site URL.

EventDataWhen
ready{ height }The form has rendered and measured itself for the first time.
resize{ height }The content height changed.
navigate{ step, of, id }The user moved to another page of the form. Use it to scroll the frame back into view, which frame height alone won't fix.
identify{ publicId }A submission now exists, from the first save onward. Opt-in, see below.
complete{ status, publicId? }The form was submitted.
error{ code, message }The form failed to load.
window.addEventListener('message', function (e) {
var msg = e.data;
if (!msg || msg.product !== 'chronicle') return;
if (msg.event === 'navigate') window.scrollTo({ top: 0, behavior: 'smooth' });
if (msg.event === 'complete') showThankYou();
});

What never crosses the frame

No Salesforce record Id is ever posted to the embedding page, not the record the form is bound to, not the submission's record Id, not the template Id, and there is no setting to allow it. A website page is outside your org and every script on it can read what's posted there.

The only handle that can cross is the submission's Public ID, and it's off by default. To receive it, add both parameters to your Form Embed Site URL:

  • c__chronpubid=1 opts this embed in.
  • c__chronhost=https://your-site.example.com names your origin.

Messages carrying the Public ID are then delivered only to that exact origin, never broadcast. Without a validated origin, they are not sent at all. Everything else (heights, step counts, a completion status) carries nothing sensitive and is broadcast normally.

Note that identify fires as soon as a submission exists, which is the first time the user continues past a page, not when they finish. Someone who abandons the form on page three has already left a real submission holding real answers.

Where the data lives

Field on Form__cContains
Public_Id__cThe public handle used to open the submission.
Status__cNew, then In Progress, Completed, and Archived.
SFDC_Record_Id__c / SFDC_SObject_Name__cThe record the submission is bound to.
Data_JSON__cCaptured values not written to the bound record, plus staged Related Records rows and any pushed Data Table arrays (see below).
Submission_JSON__cThe per-submission log: successes, failures with the underlying error, and (when Capture submission data is on) the full submitted dataset. Add the Submissions Log component to the Form record page to read it.

Repeatable child rows

A form containing a Related Records cell, which collects many child records at once, stages that cell's rows in Data_JSON__c under children, keyed by the cell's id:

{
"children": {
"cell_a1b2c3d4": [
{ "FirstName": "Ada", "Birthdate": "1815-12-10" },
{ "FirstName": "Grace", "Birthdate": "1906-12-09" }
]
}
}

This is captured input, not records. It's what lets a half-finished form resume with its rows intact. The real child records are written when the submission is completed: each row is created, or updated when the cell defines match keys, and linked back to SFDC_Record_Id__c through the lookup configured in the builder. This only happens for a form bound to a record; with nothing to link to, rows are staged but never materialize.

Rows are added and updated, never deleted. Dropping a row from a form doesn't delete a record it already created.

Display tables (Data Table cells)

A Data Table cell renders a read-only table. It has two sources, set in the builder:

  • From a field. The cell names a field (or Data_JSON cache key) whose value is a JSON array of row objects. Each column pulls one property from each object. If you're driving the form from a host wrapper, stage that array in Data_JSON__c under values, keyed by the cell's field name, the same channel as any other pushed value:

    {
    "values": {
    "adminVaccines": [
    { "name": "MMR", "lot": "AB1234", "date": "2026-07-17" },
    { "name": "Varicella", "lot": "CD5678", "date": "2026-07-17" }
    ]
    }
    }

    In a record-bound form the field can instead be a Long Text field on the record holding that JSON. Values render as stored, so push already-formatted strings if you need a particular format.

  • From a query. The cell defines an object, a filter, and a sort in the builder, and Chronicle runs the query when the form loads, so no staging is needed. The filter can compare to fixed values or to a live field on the form, and the table refreshes as that field changes. The query runs in system mode: on a public form it returns the queried records to anyone who opens the form, so only query data you're comfortable exposing.

Permissions

  • Chronicle User for people who only fill out forms. Grants the viewer and its tab.
  • Chronicle Admin for people who build forms. Grants the builder, the viewer, and full access to templates and submissions.
  • Public forms need the site's guest user to have access to the chron_FormViewer Apex class, granted on the guest profile or a permission set for that site. This is configured per org.