Skip to main content
Version: 2.4

Form Effects Reference

This page defines the persisted schema contract and runtime behavior of Form Effects. For a task-oriented introduction, start with Form Effects. For visual editing, see the Form Logic Editor.

Schema Location

Effects are stored in the root schema's effects array:

{
"type": "object",
"properties": {
"firstName": {"type": "string"},
"lastName": {"type": "string"},
"fullName": {"type": "string"}
},
"layout": ["firstName", "lastName", "fullName"],
"effects": [
{
"id": "combine-names",
"listen": ["$value.firstName", "$value.lastName"],
"when": "${$value.firstName != null || $value.lastName != null}",
"do": [
{
"type": "set",
"field": "fullName",
"value": "${[$value.firstName, $value.lastName].filter(x => x).join(' ')}"
}
]
}
]
}

Effect Object

PropertyRequiredTypeDescription
idConditionalstringIdentifier used for diagnostics and references. Required and unique for an invoked effect. Unique IDs are recommended for every effect.
nameNostringHuman-readable label used only for display and documentation.
descriptionNostringLonger human-readable explanation. It does not affect execution.
listenConditionalstring[]Paths watched by a value-change effect. Mutually exclusive with trigger.
triggerConditionalEffectTriggerA widgetEvent or invoked trigger. Mutually exclusive with listen.
whenNostringJEXL condition. The effect runs only when it evaluates to boolean true.
doYesEffectAction[]Actions executed in array order when the condition passes.
disabledNobooleanKeep the effect in the schema but skip it at runtime.
runOnInitializationNobooleanRun a value-change effect once after initialization settles. Default: false.

When when is omitted, the condition is treated as satisfied and do runs after the trigger fires.

An effect is one of three forms:

// Value change
{listen: string[], when?: string, do: EffectAction[]}

// Widget event
{trigger: {type: 'widgetEvent', objectPointer: string, event: string}, do: EffectAction[]}

// Explicit invocation
{id: string, trigger: {type: 'invoked'}, do: EffectAction[]}

Value Contracts

The schema uses three different path and value contracts:

ContractPropertiesSyntax
Watched context pathlisten[]Fully prefixed, for example $value.customer.id
Literal form-control pathfield, from, toDot notation without $value or ${...}, for example customer.id
JEXL expressionwhen, expression-backed value, nested scriptData, nested inputsWrapped in ${...}

effectId is a literal ID. objectPointer is a JSON Pointer into the schema. Neither is a JEXL expression.

Legacy listen values that wrap a simple path in ${...} remain supported, but new schemas should use direct paths.

Trigger Types

Value Change

A value-change effect has listen and no trigger. It runs when at least one watched path changes according to deep equality.

{
"id": "recalculate-total",
"listen": ["$value.quantity", "$value.price"],
"do": [
{
"type": "set",
"field": "total",
"value": "${($value.quantity || 0) * ($value.price || 0)}"
}
]
}

An empty or missing listen array never runs as a value-change effect.

Supported listen roots are:

RootWatched data
$valueCurrent root form value
$contextForm context
$outputContextOutput context
$configApplication/API configuration
$runtimeInfoCurrent user and runtime information
$versionApplication version
$buildBuild metadata

listen is not inferred from expressions. If an effect reads $value.customer.id and its change should rerun the effect, include that path explicitly.

Datasource results are resolved as part of the expressions that call them; they are not a top-level value-change trigger. Put the datasource call in when, set.value, or runEffect.inputs and listen to the form or context values passed as its parameters. Datasource calls inside scriptData are not resolved by the current Form Effects runtime.

Initialization

The first settled form snapshot normally establishes only the comparison baseline. runOnInitialization: true opts a value-change effect into exactly one run after initial values, expression defaults, and their data sources have settled.

Intermediate default or datasource emissions do not each cause a separate initialization run. The option does not apply to widget-event or invoked effects, and the designer removes it when the trigger type changes.

Widget Event

A widget-event effect listens to one semantic event from one concrete widget or layout node:

{
"id": "remember-opened-panel",
"trigger": {
"type": "widgetEvent",
"objectPointer": "/layout/0",
"event": "panelOpened"
},
"do": [
{
"type": "set",
"field": "lastOpenedPanel",
"value": "${$payload.index}"
}
]
}
Trigger propertyDescription
typeAlways widgetEvent.
objectPointerCanonical JSON Pointer of the emitting schema node.
eventStable event name declared by that widget's plugin definition.

Only events declared by the widget are accepted at runtime. The Form Designer filters the event selector accordingly and provides known payload members to JEXL autocomplete.

The source selector displays the widget breadcrumb, selector, and pointer. Use Locate widget to select that source in the designer and scroll it into view.

The event context contains:

{
name: string;
payload?: unknown;
objectPointer: string;
selector?: string;
}

Use $payload as the preferred shorthand for $event.payload. Use $event when event metadata such as name, objectPointer, or selector is required.

For tab and step transitions, prefer stable identifiers such as $payload.previousId and $payload.activeId when available. Position-based previousIndex and activeIndex remain available.

objectPointer identifies a schema position rather than a persistent widget ID. Do not maintain it by hand after structural edits. The Form Designer remaps widget-event pointers when nodes are moved, inserted, renamed, duplicated, or deleted.

Explicit Invocation

An invoked effect is a reusable named action block. It runs only through a runEffect action:

{
"id": "process-customer",
"trigger": {"type": "invoked"},
"when": "${$input.customer != null}",
"do": [
{
"type": "set",
"field": "customerName",
"value": "${$input.customer.name}"
}
]
}

Its non-empty id must identify exactly one effect in the root schema. Values passed by runEffect.inputs are available through $input.

Action Types

clear

clear changes a control according to its mode. The default mode is empty.

{"type": "clear", "field": "statusDescription", "mode": "empty"}
ModeRuntime behaviorUse it when
emptyScalars become null, arrays lose all items, and each direct child of a group becomes null. The control is dirty, which blocks default reapplication.The value should stay explicitly blank.
unsetThe value becomes undefined and the control becomes pristine. A computed default may apply later if its result changes.User input should be discarded and reactive default behavior restored.
resetThe control becomes pristine and its schema default is resolved and applied immediately. Stored control defaults are used as a fallback.The value should return to its default now.

The modes affect form state as well as the visible value. empty is therefore not interchangeable with unset.

set

set writes one static or evaluated value to a control:

{
"type": "set",
"field": "fullName",
"value": "${[$value.firstName, $value.lastName].filter(x => x).join(' ')}"
}

field is a literal control path. It is not an expression:

IncorrectCorrect
"field": "${$value.customer.email}""field": "customer.email"
"field": "$value.customer.email""field": "customer.email"
"field": "customer/email""field": "customer.email"

value can be a static string or a ${...} expression. Expression results may have any value type accepted by the target control. The runtime writes only when the resolved value differs from the current value.

{"type": "set", "field": "status", "value": "active"}
{"type": "set", "field": "price", "value": "${$value.quantity * $value.unitPrice}"}

copy

copy reads one form path and deep-copies its value to another:

{"type": "copy", "from": "shippingAddress", "to": "billingAddress"}

Both paths use the same literal dot notation as set.field. Objects and arrays are cloned so later changes to the target do not mutate the source. Form values used with copy must remain JSON-compatible.

When an exact target control is not present, the runtime can patch the deepest existing parent control at the remaining nested path.

script

script dispatches a server-side script and can continue with store actions or form actions after the result arrives:

{
"type": "script",
"scriptCode": "calculate_price",
"scriptData": {"productId": "${$value.productId}"},
"await": true,
"successDo": [
{"type": "set", "field": "price", "value": "${$response.calculatedPrice}"},
{"type": "clear", "field": "errorMessage", "mode": "empty"}
],
"errorDo": [
{"type": "clear", "field": "price", "mode": "empty"},
{"type": "set", "field": "errorMessage", "value": "${$response.error.message || $response.message}"}
]
}
PropertyRequiredTypeDescription
scriptCodeYesstringScript code to execute.
scriptDataNounknownValue or nested object containing expressions, evaluated recursively before dispatch.
hideScriptErrorNobooleanHide the standard script error presentation. Default: false.
successActionsNoStoreAction[]Store-level actions dispatched on success.
errorActionsNoStoreAction[]Store-level actions dispatched on error.
successDoNoEffectAction[]Form actions executed on success with $response.
errorDoNoEffectAction[]Form actions executed on error with $response.
awaitNobooleanWait for the selected outcome branch before continuing the parent effect. Default: false.

successActions and errorActions are handled by the application's action layer. Use them for reloads, messages, navigation, and other store-level UI behavior. See Success / Error Actions.

scriptData can use ordinary effect context expressions, but not datasource calls. Resolve datasource-backed values first with set or pass them through a separate invoked effect.

successDo and errorDo continue the form workflow. They can contain set, clear, copy, or runEffect. A nested script action is rejected; invoke a reusable effect instead.

After a response arrives, the matching successActions or errorActions are dispatched first. The matching successDo or errorDo branch then executes with $response in scope.

Script ordering

awaitBehavior
trueWait for the result, execute and await the matching Do branch in the same execution chain, then continue with later parent actions.
false or omittedDispatch the script and continue the parent effect immediately. Execute the matching Do branch later as a detached continuation.

await controls workflow ordering; it does not block the browser UI thread. Use it whenever later actions depend on the script outcome. Fire-and-forget is appropriate only for an independent side operation.

An awaited script has a 60-second timeout. A timeout runs errorDo with:

{
"error": {
"code": "SCRIPT_TIMEOUT",
"message": "Awaited script did not finish before the timeout.",
"scriptCode": "loadCustomer"
}
}

Once a script request has been sent, its success or error continuation is allowed to finish even if a newer form-state emission supersedes the original value-change run.

$response

$response contains parsed response.data when available. A returned failure payload is preserved; a thrown error is normalized as {error: ...}.

It is available only in the script's direct successDo or errorDo branch. Standard context values remain available alongside it. If an invoked effect needs the response, map the required values through runEffect.inputs.

runEffect

runEffect calls one effect declared with trigger.type === 'invoked':

{
"type": "runEffect",
"effectId": "process-customer",
"inputs": {
"customer": "${$response}",
"requestedBy": "${$runtimeInfo.userName}"
}
}

effectId is a literal ID in the same root schema. inputs is evaluated recursively, including expressions nested in objects and arrays. The resulting object becomes $input in the invoked effect.

The runtime rejects:

  • a missing or ambiguous ID;
  • a target that is not an invoked effect;
  • a disabled target;
  • direct or indirect invocation recursion.

Invocation is an explicit context boundary. $event and $response are not implicitly inherited by the target.

When an invoked effect ID is renamed in the Form Designer, local runEffect.effectId references are updated with it.

Expression Context

Effect expressions use the same JEXL runtime as computed form properties, with an effect-specific scope:

ValueMeaning in a Form Effect
$valueCurrent value of the complete root form.
$contextForm context from the Fluent Forms store.
$outputContextOutput context from composed widgets and flows.
$configApplication/API configuration.
$configUiReserved for UI configuration. The current Form Effects runtime supplies {}, so do not depend on it yet.
$runtimeInfoCurrent user and runtime metadata.
$versionApplication version.
$buildBuild metadata.
$rowAlways 0 for root Form Effects.
dataSourcesRegistered datasource functions.
dataSourcesValuesSettled datasource values available to the evaluator.
$dataSourcesLoadingWhether an expression datasource is still resolving.
$eventComplete semantic event in a widget-event effect.
$payloadShorthand for $event.payload.
$inputEvaluated input object in an invoked effect.
$responseScript result in the script's direct outcome branch.

The stage-specific values are preserved in a script outcome branch belonging to that same effect. An invoked effect receives only the values explicitly mapped into $input.

Member access

Author ordinary dot and bracket access in Form Effects expressions:

$value.customer.address.street
$response.items[0].name

The expression compiler adds safe member access. Do not add JavaScript optional access operators such as ?. to persisted Form Effects expressions. Use a fallback when the final result can be missing:

$value.customer.address.street || 'N/A'

See the JEXL language reference for operators, functions, transforms, and Public API data sources.

Datasources

Only datasources declared for effect usage can be called from an effect expression. The current runtime resolves datasource calls in when, set.value, and runEffect.inputs; it does not resolve them in scriptData:

Effect modeRuntime behavior
plainValueResolve a scalar or object value on the first emission.
identifiedPackWait until the returned pack has loading !== true. Read the result from .data.
unsupportedReject the datasource in an effect and omit it from effect autocomplete.

Example using an identifiedPack datasource:

{
"id": "load-user-name",
"listen": ["$value.userId"],
"do": [
{
"type": "set",
"field": "userName",
"value": "${evalScriptByCode('GetUserName').data}"
}
]
}

Datasource parameters are reevaluated when their dependencies change. A new value-change run supersedes an older synchronous run that has not completed.

Execution and Cascades

Within one run:

  1. matching effects are evaluated in schema order;
  2. each when must return exactly boolean true;
  3. actions execute in array order;
  4. changed fields are collected;
  5. effects listening to those fields form the next cascade iteration.

One failing effect is reported without preventing the remaining matching effects in the same run from being evaluated.

Cycle safeguards

SafeguardDefault behavior
Maximum cascade depthStop after 10 iterations.
Depth warningWarn when depth reaches 3.
Field change trackingDetect repeated changes to the same field in one chain.
Invocation stackReject direct and indirect runEffect cycles.
Burst limitStop after more than 10 top-level runs in a 2-second window.
Effects run on the browser UI thread

Synchronous expressions, functions, and transforms share the JavaScript thread with form rendering and user input. A computationally expensive or non-terminating expression can make the page unresponsive before runtime safeguards get a chance to run. Move expensive work to a server-side script and keep the effect graph acyclic.

Designer Validation and Diagnostics

DiagnosticResult
$value read not covered by listenDependency warning. Add the path if its change should rerun the effect.
Field dependency cycleWarning. Saving remains possible.
Direct or indirect runEffect recursionError; saving is blocked.
Invoked effect without an IDSchema validation error.
Duplicate invoked-effect IDSchema validation error.
Missing, ambiguous, or non-invoked runEffect targetSchema validation error.
Invalid ${...} syntaxJEXL validation error.

The diagram displays field and invocation cycles. The linear Effects editor also groups missing-listen dependency warnings.

Widget Event Catalog

Representative built-in widget events include:

WidgetEventsKnown payload
dtl-button, dtl-create-entity-button, dtl-submit-request-buttonsuccess, errorOperation-specific response
dtl-fluent-inplacesaveSuccess, saveError, scriptSuccess, scriptErrorSave or script outcome
dtl-attachments-widgetuploadSuccess, uploadError, uploadScriptSuccess, uploadScriptError, deleteAttachmentSuccess, deleteAttachmentError, deleteAttachmentScriptSuccess, deleteAttachmentScriptErrorUpload, deletion, or script response
dtl-fluent-accordionpanelOpened, panelClosedindex, header
dtl-fluent-tabactiveTabChangedpreviousIndex, activeIndex, previousId, activeId
dtl-fluent-stepsactiveStepChanged, stepsViewStateChangedTransition IDs/indexes or visited-step counts
dtl-fluent-accordion-arrayactiveItemChangedNullable previousIndex, activeIndex
dtl-form-input-textleftIconClicked, rightIconClickedNo payload
dtl-clipboard-buttoncopySuccess, copyErrorError reason when copying fails
LOV widgets with create supportitemCreatedid, value

This table is intentionally representative. The selected widget's event list in the Form Designer is the source of truth for the installed widget version.

Legacy widget actions

Older schemas can contain successDo, errorDo, saveSuccessDo, or similar arrays inside widget configuration. They remain runtime-compatible. When the schema is edited, the Form Designer migrates them to root widget-event effects and removes the legacy arrays from the widget configuration.

Legacy ${$response.total} expressions may become ${$event.payload.total}. This is equivalent to the preferred ${$payload.total} form.

Configuration Effects

Plugins and base-form configurations can supply predefined effects for an entity type. These effects:

  • use the same runtime contract as schema effects;
  • are visible but read-only in the Form Designer;
  • are applied automatically to forms using that configuration;
  • can be deactivated for one schema by ID.

The designer persists deactivation as a minimal disabled stub:

{
"id": "configuration-effect-id",
"disabled": true,
"listen": [],
"do": []
}

At runtime, configuration and schema effects are matched by id. A disabled schema stub suppresses the matching configuration effect. Other form-owned effects remain unchanged.

To deactivate a configuration effect:

  1. Open the form in the Form Designer.
  2. Open Effects.
  3. Expand the effect in Configuration Effects.
  4. Enable Deactivate this effect in schema.

Runtime Limitations

  • Effects are defined at the root schema level.
  • field, from, and to are literal paths and cannot be calculated.
  • Array targets require concrete indexes; there is no wildcard per-row action.
  • $row is 0 in effect expressions.
  • Widget-event objectPointer values identify positions and must be remapped after structural schema changes; use the Form Designer for this.
  • Nested script actions inside successDo and errorDo are blocked.

Troubleshooting

Runtime logging

Guardrail and execution failures are always reported as console warnings or errors. Detailed per-effect traces are emitted only when runtime effect logging is enabled. Representative messages include:

[FluentEffects] Max depth reached, stopping execution
[FluentEffects] Execution depth warning
[FluentEffects] Possible infinite loop detected: ...
[FluentEffects] Control not found for set action: ...
[FluentExpressionRuntimeService] evaluation error: ...
[FluentScriptEffects] Script executed successfully
[FluentScriptEffects] Script execution failed

Cycle errors include the cascade depth and executionPath, which identifies the sequence of effect IDs that led to the guardrail.

The effect does not run

  • Confirm that it is not disabled.
  • For a value-change effect, confirm that listen contains the value that actually changed.
  • For a widget event, confirm the selected widget source and event name.
  • Remember that the initial form snapshot is only a baseline unless runOnInitialization is enabled.

A field is not found

Use a literal dot path such as customer.address.street. Remove $value, ${...}, or JSON Pointer slashes from action targets.

A condition looks truthy but does not run

when must evaluate to boolean true. Inspect whether the expression returns a string, number, object, or missing value instead.

A datasource expression does not resolve

Confirm that the datasource declares a supported effect mode. For an identifiedPack, read its settled result from .data.

A script result is missing in an invoked effect

$response does not cross an invocation boundary. Map the required value into runEffect.inputs and read it from $input in the invoked effect.