Skip to main content
Version: 2.5

Fluent Forms Effects Engine

Overview

The effects engine lets you define reactive side-effects at the form schema root. An effect can respond to a form value change, a semantic event emitted by a specific widget, or an explicit invocation from another effect.

Main use cases:

  • Automatically clear/set field values when another field changes
  • Copy values between fields
  • Execute server-side scripts on value changes
  • React to widget events such as a button result, an opened accordion panel, or a changed wizard step
  • Reuse and chain named effects with explicit inputs
  • Run value-change logic once after form initialization when required
  • Design and inspect form logic visually, including dynamic JEXL dependencies
  • Complex form logic without writing JavaScript
Fluent Forms effects architecture

Current Capabilities at a Glance

AreaAvailable behavior
TriggersForm value change, semantic widget event, or explicit invocation from another effect
InitializationA value-change effect can optionally run once after defaults, initial values, and data sources settle
Field actionsSet, clear/reset, and deep-copy form values
Server workflowsRun a server-side script, optionally await it, and continue through success or error branches
ReuseInvoke a named effect with recursively evaluated $input values
ExpressionsUse JEXL in listen, when, action values, script data, and dynamic widget properties
Design toolsEdit the same schema in the original Effects panel or in the Form Logic diagram
DiagnosticsInspect dependencies and highlight field cycles or recursive runEffect chains before saving

Editing Effects in the Form Designer

The Form Designer provides two synchronized ways to work with the same root-level effects[] array:

ViewBest suited for
Form designer → Effects (lightning icon)Editing one effect as a linear form, reviewing all effect properties, and viewing read-only configuration effects
Form Logic (top-level tab)Understanding and editing relationships between fields, effects, widget events, actions, script branches, and dynamic properties
One schema, two views

The diagram is not a second workflow format. Both editors read and update the same FluentSchema JSON, and the original Form Designer remains fully supported. You can switch between the views at any time without migrating an existing form.

Form Logic Diagram

Open a form and select the Form Logic tab. The screen consists of three resizable areas: a palette on the left, the diagram canvas in the center, and the logic inspector on the right.

Palette

The palette contains the building blocks that are valid for the current form:

  • Triggers — Value change, Widget event, and Invoked effect
  • Actions — Set, Clear, Copy, Script, and Run effect
  • Form fields — Fields discovered from the current schema
  • Widget events — Semantic events declared by widgets present in the current layout

Drag an item to an exact position on the canvas, or activate it by click or keyboard. A trigger creates a new effect. A field, widget event, or action is initially a standalone node.

Connections are explicit

Adding a node from the palette does not automatically change an existing effect. Create the relationship deliberately by dragging from a node port to a highlighted compatible target.

The most important editable connections are:

ConnectionResult in the schema
Form field → value-change effectAdds the field path to listen
Widget event → effectCreates a widgetEvent trigger with objectPointer and event
Effect ↔ standalone actionAdds the action to the effect's do array
Set/Clear/Copy action → form fieldSelects the action target (field or to)
Run effect action → invoked effectSets effectId
Effect → invoked effectInserts and connects a new runEffect action

The diagram accepts either drawing direction for trigger connections between a field/widget event and an effect. The stored relationship still follows the logical direction shown above.

Canvas and Inspector

Select a node to edit it in the right inspector. The inspector reuses the existing Effects and Expressions editors, so advanced properties remain available without leaving the diagram. Selecting a field, action, event, expression, or effect shows only the relevant context instead of opening every effect at once.

Use the scope switch at the top of the canvas:

  • All logic shows the complete graph.
  • Selection context shows the selected node and its related subgraph. For example, selecting a field shows effects that listen to or write that field, as well as JEXL expressions that depend on it.

Without a selection, Selection context is disabled. Selecting another node recalculates the context immediately.

The toolbar provides Auto layout and Fit diagram. Dragged node positions are stored separately in settings.formLogicDiagram.nodePositions; they do not alter the form's runtime logic. Switching tabs therefore preserves the layout while schema compatibility remains unchanged.

Direct and Derived Relationships

Not every displayed edge is an independently stored relationship:

  • Trigger and runEffect edges represent directly editable schema properties.
  • Write edges are derived from action targets such as set.field, clear.field, or copy.to.
  • JEXL dependency edges are derived from expressions in conditions, action values, defaults, and dynamic widget properties.
  • Script success and error branches are derived from successDo and errorDo.

When a relationship is derived, edit the expression or action in the inspector. For a directly editable edge, selecting the edge shows its source, target, and the available delete action. Deleting an edge removes only that relationship; deleting a node removes the corresponding effect, action, or dynamic property. The Delete key is also supported for a selected deletable item.

Dynamic Widget Properties

JEXL entered in an individual widget attribute is also form logic. The diagram projects these dynamic properties as expression nodes and connects every referenced form field with a visually distinct JEXL dependency edge. Use the inspector's Expressions tab to search all expressions, see their schema location, and open the corresponding editor.

Cycle Diagnostics

The diagram analyzes executable dependencies whenever the schema changes:

  • A field dependency cycle is shown as a warning. It may be intentional and can still be saved, but the highlighted path should be reviewed.
  • A direct or indirect runEffect recursion is shown as an error and blocks saving until the recursive chain is removed.

The cycle counter and inspector panel list each detected path. Select a cycle to focus all involved nodes and edges. The original Effects panel displays the same diagnostics, so switching back to the linear editor does not hide a problem.

Read-Only Configuration Effects

Effects supplied by a base form or plugin configuration remain read-only. They are visible in the Effects editor inside the Form Logic inspector and in the original Form Designer. The editable canvas works with the form's own root effects[]; configuration effects are not copied into the schema. A form may only deactivate them as described in Configuration Effects (Read-Only).

Basic Structure

Effects are defined in the JSON schema at the root level using the effects property. This first example uses the traditional value-change trigger:

{
"type": "object",
"properties": {
"checkbox": {"type": "boolean"},
"text1": {"type": "string"},
"text2": {"type": "string"}
},
"layout": ["checkbox", "text1", "text2"],
"effects": [
{
"id": "my-effect",
"listen": ["${$value.text1}"],
"when": "${$value.checkbox == true}",
"do": [{"type": "set", "field": "text2", "value": "${$value.text1}"}]
}
]
}

Effect Object Structure

PropertyRequiredTypeDescription
idConditionalstringIdentifier for debugging and references. Required for an invoked effect and must be unique within the root schema.
listenConditionalstring[]Expressions whose output is watched. Used by value-change effects, which do not have trigger.
triggerConditionalEffectTriggerA widgetEvent or invoked trigger. It is mutually exclusive with listen.
whenNostringCondition evaluated in the trigger-specific expression context. If omitted, the effect always executes.
doYesEffectAction[]Array of actions to execute when the condition is met.
disabledNobooleanWhen true, the effect remains in the schema but is not executed.
runOnInitializationNobooleanFor a value-change effect, run once after defaults, initial values, and data sources settle. The default is false.

Trigger Types

Value change

A value-change effect has listen and no trigger. It runs whenever the evaluated result of at least one listen expression changes:

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

By default, initialization only establishes the initial listen values and does not execute the effect. Set runOnInitialization to true when the effect must also run once against the fully initialized form state:

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

This option applies only to value-change effects. The designer removes it when the trigger is changed to widgetEvent or invoked.

Widget event

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

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

The Form Designer provides a widget-source dropdown with a breadcrumb, widget selector, and pointer, for example Order › Actions › Calculate · dtl-button · /layout/0/items/4. The event dropdown then shows only the human-readable events implemented by that widget. Locate widget selects the source in the designer and scrolls it into view.

Do not hand-maintain objectPointer after structural edits. The designer remaps pointers when nodes are moved, inserted, renamed, duplicated, or deleted. Because the pointer identifies a schema position rather than a persistent ID, effects should be edited together with their source widget in the Form Designer.

The event expression context adds $event:

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

Known payload members are shown by JEXL autocomplete for the selected event. For example, dtl-fluent-steps exposes $event.payload.previousIndex and $event.payload.activeIndex for activeStepChanged.

Explicit invocation

An invoked effect is a reusable named block that does not run from a value change or widget event. It can only be called by a runEffect action:

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

Named values passed by runEffect.inputs are available as $input. An invoked effect must have a unique non-empty id; the target must be in the same root schema. The designer updates runEffect.effectId references when an effect ID is renamed.

Action Types (EffectAction)

1. CLEAR - Clear/Reset Field Value

Clears a field value. The mode controls what happens after the value is removed. If mode is not specified, empty is used.

Mode empty (default) — Simply wipes the value (sets it to null/[]/{}). The field is marked as "dirty", meaning any schema default will not be applied. Use this when you just want to blank out the field with no further behavior:

{
"type": "clear",
"field": "text1",
"mode": "empty"
}

Mode unset — Removes the value and marks the field as "pristine" (as if the user never touched it). The field is then open to receiving its default value again — but only if the default expression produces a new value in the future (e.g. when another field it depends on changes). Use this when you want to "undo" a user's input and let the form's default logic take over again passively:

{
"type": "clear",
"field": "text1",
"mode": "unset"
}

Mode reset — Same as unset, but also immediately re-applies the schema default right away, without waiting for anything to change. If the field has a default value or default expression defined, it is evaluated and set instantly. Falls back to stored control defaults if no schema default exists. Use this when you want the field to snap back to its default value on the spot:

{
"type": "clear",
"field": "text1",
"mode": "reset"
}
Key difference: unset vs reset

Both modes mark the field as pristine and allow defaults to be applied again. The difference is when:

  • unset — waits; the default is applied only if it changes later (reactive)
  • reset — acts immediately; the default is applied right now

2. SET - Set Value with Expression

Sets field value using an evaluated expression:

{
"type": "set",
"field": "text2",
"value": "${$value.text1.toUpperCase()}"
}

Properties:

  • field - field path (for example, "text1", "nested.field", "array.0.item")
  • value - JEXL expression that will be evaluated and its result set as field value

Expression Examples:

// Static value
{ "type": "set", "field": "status", "value": "\"active\"" }

// Copy value
{ "type": "set", "field": "text2", "value": "${$value.text1}" }

// Transform value
{ "type": "set", "field": "upper", "value": "${$value.text1.toUpperCase()}" }

// Conditional expression
{ "type": "set", "field": "label", "value": "${$value.active ? 'Active' : 'Inactive'}" }

// Complex calculation
{ "type": "set", "field": "total", "value": "${$value.quantity * $value.price}" }

3. COPY - Copy Value

Copies value from one field to another (including deep clone for objects/arrays):

{
"type": "copy",
"from": "sourceField",
"to": "targetField"
}

Properties:

  • from - source field path
  • to - target field path

Note: Copying creates a deep clone, so modifying the copied value does not affect the original.

4. SCRIPT - Execute Server-Side Script

Executes a server-side script with success/error action handling:

{
"type": "script",
"scriptCode": "notify_user_status_change",
"scriptData": {
"userId": "${$value.userId}",
"status": "${$value.status}"
},
"hideScriptError": false,
"successActions": [
{
"action": "SHOW_NOTIFICATION",
"actionData": {"message": "Status updated"}
}
],
"errorActions": [
{
"action": "SHOW_ERROR",
"actionData": {"message": "Update failed"}
}
]
}

Properties:

PropertyRequiredTypeDescription
scriptCodeYesstringScript code to execute
scriptDataNounknownExpression or object containing expressions, evaluated recursively before execution
hideScriptErrorNobooleanWhether to hide script errors from the user (default: false)
successActionsNoStoreAction[]NgRx store actions to dispatch on successful script completion
errorActionsNoStoreAction[]NgRx store actions to dispatch on script error
successDoNoEffectAction[]Effect actions to execute on success, with $response available in expressions
errorDoNoEffectAction[]Effect actions to execute on error, with $response available in expressions
awaitNobooleanWhether the current effect run waits for the script and its selected outcome branch (default: false)

successActions/errorActions and successDo/errorDo serve different layers. Store actions are dispatched by FormActionsService; Do actions continue the form-effects workflow and may update fields, trigger value-change cascades, or invoke another effect.

await

await controls the ordering of the current effect run. It does not block the browser UI thread.

ValueBehavior
trueWait for the script result, execute the matching successDo or errorDo branch in the same execution chain, wait for that branch to finish, and only then continue with subsequent actions in the parent effect.
false or omittedDispatch the script and continue the parent effect immediately. The matching outcome branch still runs when the response arrives, but as a detached asynchronous continuation.

Use await: true for ordered business workflows, especially when an outcome branch updates the form or uses runEffect. Use fire-and-forget behavior only for an independent side operation such as audit or telemetry.

An awaited script has a 60-second timeout. A timeout executes errorDo with this $response payload and then lets the parent effect continue:

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

successDo / errorDo

successDo and errorDo execute effect actions after a script completes, with access to the script outcome via $response. Unlike successActions/errorActions, which dispatch NgRx store actions, they continue the form-effects workflow.

{
"type": "script",
"scriptCode": "calculate_price",
"scriptData": {"productId": "${$value.productId}"},
"successDo": [
{"type": "set", "field": "price", "value": "${$response.calculatedPrice}"},
{"type": "set", "field": "discount", "value": "${$response.discount}"},
{"type": "clear", "field": "errorMessage"}
],
"errorDo": [
{"type": "clear", "field": "price"},
{"type": "set", "field": "errorMessage", "value": "${$response.message}"}
]
}

The $response variable:

  • Uses parsed response.data when present; otherwise it contains the returned failure payload. A thrown error is normalized as {error: ...}
  • Available in expressions within successDo / errorDo, including set.value and runEffect.inputs
  • All standard expression variables ($value, $context, $config, ...) remain available alongside $response

Expression examples:

${$response}                        // entire response data
${$response.price} // nested property
${$response.items[0].name} // optional chaining

Supported action types in successDo / errorDo:

TypeSupportedNote
setYes$response is available in the value expression
clearYesWorks the same as in regular do
copyYesWorks the same as in regular do
runEffectYes$response can be mapped into the target's $input
scriptNoNested SCRIPT actions are blocked; use runEffect instead

Execution order:

  1. Script is called via API
  2. successActions / errorActions (StoreAction[]) are dispatched as NgRx actions
  3. successDo / errorDo (EffectAction[]) are executed with $response in the expression context

Combining with existing properties:

successDo/errorDo and successActions/errorActions can be used together:

{
"type": "script",
"scriptCode": "validate_data",
"successActions": [{"action": "SHOW_TOAST", "actionData": {"severity": "success"}}],
"successDo": [{"type": "set", "field": "validated", "value": "${true}"}]
}

5. RUNEFFECT - Invoke Another Effect

Calls one effect declared with "trigger": {"type": "invoked"} in the same root schema:

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

effectId is a literal effect ID, not an expression. inputs is evaluated recursively, so expressions can appear inside objects and arrays. The resulting object is exposed to the invoked effect as $input.

The runtime rejects missing or ambiguous IDs, targets that are not invoked, disabled targets, and direct or indirect invocation cycles. Use runEffect for reusable workflow steps and explicit chaining; do not create artificial form fields solely to trigger the next effect.

Expressions

All expressions in effects (listen, when, do values, etc.) are standard JEXL expressions with the same evaluation context available as in other form scripting scenarios. See Frontend Scripting with JEXL for the full reference of available variables, functions, and transforms.

Three additional variables are scoped to particular workflow stages:

VariableAvailable inContents
$eventA widgetEvent effect's condition and direct actionsEvent name, payload, source objectPointer, and widget selector
$inputAn invoked effect's condition and actionsThe recursively evaluated runEffect.inputs object
$responseA script's direct successDo or errorDo actionsParsed success data or failure payload

The Form Designer's JEXL editor suggests only variables that are valid for the current effect. When a widget event declares payload metadata, autocomplete also includes known members such as $event.payload.activeIndex.

Invocation is an explicit context boundary. If an invoked effect needs widget-event or script-outcome data, map it through runEffect.inputs and read it from $input; $event and $response are not implicitly inherited by the invoked target.

Complete Examples

Example 1: Auto-clear field when checkbox changes

{
"effects": [
{
"id": "clear-text-when-disabled",
"listen": ["${$value.enableText}"],
"when": "${$value.enableText == false}",
"do": [
{
"type": "clear",
"field": "textField",
"mode": "unset"
}
]
}
]
}

Example 2: Synchronize fullName when first/last name changes

{
"effects": [
{
"id": "update-fullname",
"listen": ["${$value.firstName}", "${$value.lastName}"],
"do": [
{
"type": "set",
"field": "fullName",
"value": "${($value.firstName || '') + ' ' + ($value.lastName || '')}"
}
]
}
]
}

Example 3: Conditional address copying

{
"effects": [
{
"id": "copy-billing-address",
"listen": ["${$value.sameAsShipping}"],
"when": "${$value.sameAsShipping == true}",
"do": [
{
"type": "copy",
"from": "shippingAddress",
"to": "billingAddress"
}
]
},
{
"id": "clear-billing-when-different",
"listen": ["${$value.sameAsShipping}"],
"when": "${$value.sameAsShipping == false}",
"do": [
{
"type": "clear",
"field": "billingAddress",
"mode": "empty"
}
]
}
]
}

Example 4: Execute script on status change

{
"effects": [
{
"id": "notify-status-change",
"listen": ["${$value.status}"],
"when": "${$value.status == 'completed'}",
"do": [
{
"type": "script",
"scriptCode": "send_notification",
"scriptData": {
"userId": "${$value.userId}",
"oldStatus": "${$context.previousStatus}",
"newStatus": "${$value.status}"
},
"successActions": [
{
"action": "SHOW_NOTIFICATION",
"actionData": {"message": "Notification sent"}
}
],
"errorActions": [
{
"action": "SHOW_ERROR",
"actionData": {"message": "Failed to send notification"}
}
]
}
]
}
]
}

Example 4b: Script with successDo - update form fields from script response

{
"effects": [
{
"id": "calculate-price-from-product",
"listen": ["${$value.productId}"],
"when": "${$value.productId != null}",
"do": [
{
"type": "script",
"scriptCode": "calculate_price",
"scriptData": {"productId": "${$value.productId}"},
"successDo": [
{"type": "set", "field": "price", "value": "${$response.calculatedPrice}"},
{"type": "set", "field": "discount", "value": "${$response.discount}"},
{"type": "clear", "field": "errorMessage"}
],
"errorDo": [
{"type": "clear", "field": "price"},
{"type": "set", "field": "errorMessage", "value": "${$response.message}"}
],
"successActions": [{"action": "[Core] ShowMessage", "actionData": {"text": "Price calculated"}}]
}
]
}
]
}

Example 4c: Awaited script with explicit outcome chaining

This example waits for loadCustomer, maps the selected outcome into an invoked effect, waits for that effect to finish, and only then finishes the original run:

{
"effects": [
{
"id": "load-customer-on-selection",
"listen": ["${$value.customerId}"],
"when": "${$value.customerId != null}",
"do": [
{
"type": "script",
"scriptCode": "loadCustomer",
"scriptData": {"customerId": "${$value.customerId}"},
"await": true,
"successDo": [
{
"type": "runEffect",
"effectId": "processCustomer",
"inputs": {"customer": "${$response}"}
}
],
"errorDo": [
{
"type": "runEffect",
"effectId": "handleCustomerError",
"inputs": {"error": "${$response.error}"}
}
]
}
]
},
{
"id": "processCustomer",
"trigger": {"type": "invoked"},
"do": [
{"type": "set", "field": "customerName", "value": "${$input.customer.name}"},
{"type": "clear", "field": "errorMessage", "mode": "empty"}
]
},
{
"id": "handleCustomerError",
"trigger": {"type": "invoked"},
"do": [
{"type": "set", "field": "errorMessage", "value": "${$input.error.message}"}
]
}
]
}

Example 5: Complex calculation with multiple fields

{
"effects": [
{
"id": "calculate-total",
"listen": ["${$value.quantity}", "${$value.price}", "${$value.taxRate}"],
"do": [
{
"type": "set",
"field": "subtotal",
"value": "${($value.quantity || 0) * ($value.price || 0)}"
},
{
"type": "set",
"field": "tax",
"value": "${(($value.quantity || 0) * ($value.price || 0)) * (($value.taxRate || 0) / 100)}"
},
{
"type": "set",
"field": "total",
"value": "${(($value.quantity || 0) * ($value.price || 0)) * (1 + (($value.taxRate || 0) / 100))}"
}
]
}
]
}

Example 6: Load data from datasource

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

Advanced Features

Cycle Protection

The effects engine includes automatic protection against infinite cycles:

  • Max depth: 10 iterations
  • Warning: Console warning after 3rd iteration
  • Field change tracking: Tracks how many times each field has changed
  • Execution path logging: Logs execution path for debugging
  • Invocation stack: Rejects direct and indirect runEffect cycles

Examples of effects that cause cycles:

1. Self-loop — the simplest case, an effect listens to the same field it writes to:

{
"listen": ["${$value.text}"],
"id": "self-loop",
"do": [{"type": "set", "field": "text", "value": "${$value.text + '1'}"}]
}

text changes → effect fires → changes text → effect fires → ...

2. Ping-pong — two effects that write to each other's listened field:

[
{
"listen": ["${$value.a}"],
"id": "a-to-b",
"do": [{"type": "set", "field": "b", "value": "${$value.a + 'x'}"}]
},
{
"listen": ["${$value.b}"],
"id": "b-to-a",
"do": [{"type": "set", "field": "a", "value": "${$value.b + 'y'}"}]
}
]

a changes → sets b → sets a → sets b → ...

3. Chain cycle (A → B → C → A) — a longer chain that eventually loops back:

[
{
"listen": ["${$value.x}"],
"id": "x-to-y",
"do": [{"type": "set", "field": "y", "value": "${$value.x}"}]
},
{
"listen": ["${$value.y}"],
"id": "y-to-z",
"do": [{"type": "set", "field": "z", "value": "${$value.y}"}]
},
{
"listen": ["${$value.z}"],
"id": "z-to-x",
"do": [{"type": "set", "field": "x", "value": "${$value.z + '!'}"}]
}
]

Best Practices

1. Use meaningful IDs

{
"id": "clear-shipping-when-disabled", // Good
"id": "effect1" // Bad
}

2. Be explicit with conditions

// Good - explicit condition
"when": "${$value.checkbox == true}"

// Bad - implicit type coercion can cause issues
"when": "${$value.checkbox}"

3. Use optional chaining for safe access

"value": "${$value.user.address.street || 'N/A'}"  // Good
"value": "${$value.user.address.street}" // Can crash

4. Prefer COPY over SET for objects

// Good - deep clone
{ "type": "copy", "from": "sourceObject", "to": "targetObject" }

// Worse - shared reference
{ "type": "set", "field": "targetObject", "value": "${$value.sourceObject}" }

5. Choose the right clear mode

// Reset to schema default value immediately:
{ "type": "clear", "field": "myField", "mode": "reset" } // Re-applies default now

// Clear and allow default to be re-applied later (if default expression changes):
{ "type": "clear", "field": "myField", "mode": "unset" } // Marks pristine

// Just clear the value (blocks defaults):
{ "type": "clear", "field": "myField", "mode": "empty" } // Sets null/[]/{}

6. Choose script ordering explicitly

{
"type": "script",
"scriptCode": "long_running_operation",
"await": true,
"hideScriptError": false,
"successActions": [
// NgRx store actions such as notifications
],
"successDo": [
// Form actions with $response, including runEffect
],
"errorActions": [
// NgRx error actions
],
"errorDo": [
// Form workflow error handling
]
}

Use await: true when later actions or invoked effects depend on the outcome. Omit await only for an independent fire-and-forget operation. Use successDo/errorDo for form workflow and successActions/errorActions for store-level UI behavior such as data reloads, notifications, toasts, or navigation.

Debugging

Console Logging

Effects log to the console:

[FluentEffects] Max depth reached - stopped due to depth limit
[FluentEffects] Execution depth warning - warning at 3rd iteration
[FluentEffects] Control not found - field not found
[FluentEffects] Error evaluating expression - expression error
[FluentEffects] Script action executed successfully - script succeeded
[FluentEffects] Script action failed - script failed

Execution Path

For debugging cycles, check the execution path in the error message:

{
depth: 10,
executionPath: ['effect-1', 'effect-2', 'effect-1', 'effect-2', ...]
}

When to use effects vs computed attributes

Use CaseSolution
Calculated value (read-only)Computed attribute
Set value on changeEffect with SET action
Side effect (script, clear)Effect
Synchronous transformationComputed attribute
Asynchronous operationEffect with SCRIPT action

Widget Events at the Schema Root

Widget behavior is represented by root effects[], not by embedding effect actions into each widget's config. Every widget plugin may declare zero or more semantic events with a stable name, a human-readable label, and optional payload metadata. Only declared events are accepted by the runtime.

Representative built-in events include:

WidgetEventsKnown payload
dtl-button, dtl-create-entity-button, dtl-submit-request-buttonsuccess, errorOperation-specific response in $event.payload
dtl-fluent-inplacesaveSuccess, saveError, scriptSuccess, scriptErrorSave or script outcome in $event.payload
dtl-fluent-accordionpanelOpened, panelClosedindex, header
dtl-fluent-tabactiveTabChangedpreviousIndex, activeIndex
dtl-fluent-stepsactiveStepChangedpreviousIndex, activeIndex
dtl-fluent-accordion-arrayactiveItemChangedpreviousIndex, activeIndex
dtl-form-input-textleftIconClicked, rightIconClickedNo payload
dtl-clipboard-buttoncopySuccess, copyErrorcopyError provides reason
LOV widgets with create supportitemCreatedid, value

The Form Designer is the source of truth for the complete list: after selecting a widget source, its event dropdown contains exactly the events registered for that widget version.

Button result example

{
"type": "object",
"properties": {
"status": {"type": "string"},
"errorMessage": {"type": "string"}
},
"layout": [
"status",
"errorMessage",
{
"type": "layout",
"title": "Load customer",
"config": {
"buttonType": "request_button",
"scriptCode": "loadCustomer"
},
"widget": {"type": "dtl-button"}
}
],
"effects": [
{
"id": "customer-load-success",
"trigger": {
"type": "widgetEvent",
"objectPointer": "/layout/2",
"event": "success"
},
"do": [
{"type": "set", "field": "status", "value": "\"loaded\""},
{"type": "clear", "field": "errorMessage", "mode": "empty"}
]
},
{
"id": "customer-load-error",
"trigger": {
"type": "widgetEvent",
"objectPointer": "/layout/2",
"event": "error"
},
"do": [
{
"type": "set",
"field": "errorMessage",
"value": "${$event.payload.error.message || $event.payload.message}"
}
]
}
]
}

Legacy widget actions

Older schemas may contain successDo, errorDo, saveSuccessDo, or similar arrays inside widget config. They remain runtime-compatible. When such a schema is opened in the Form Designer, those arrays are migrated to root widget-event effects and removed from the widget config when the edited schema is saved.

During migration, expressions such as ${$response.total} become ${$event.payload.total} because the outcome is now carried by the widget event. No manual schema migration is required solely for this change.

Common Issues

Cycles

Problem: Effect A changes field B, effect B changes field A
Solution: Use when condition or combine into single effect

Field Not Found

Problem: "Control not found for set action"
Solution: Check field path - use dot notation for nested fields

Expression Does Not Evaluate Correctly

Problem: Value is not set or is undefined Solution: Check expression syntax and console for errors

Configuration Effects (Read-Only)

Configuration effects are predefined automatic actions defined in the plugin code for a given entity type (e.g., EntityCatalogSpecification). They appear in the Form Designer under the "Configuration Effects" section as read-only entries.

Configuration effects in the Form Designer

Unlike the user-defined effects described above, configuration effects:

  • Cannot be edited — they are defined by the developer in plugin code
  • Can only be deactivated (per form) by checking the checkbox in the read-only section
  • Are automatically applied to all forms of the given entity type

Each configuration effect uses the same structure as a regular effect (listen, when, do) and supports the same action types and expression context.

Example: Automatic control of the entityInstanceSpecId field

The following two configuration effects work together — the first clears the field when it is not needed, the second sets a default value when it is needed:

Effect 1 — Clear on non-instantiable specification

  • ID: instantiable-clear-entityInstanceSpecId
  • Listen: $context.form.instantiable (the "Instantiable" checkbox)
  • When: $context.form.instantiable === false
  • Action: Clear tsmControls.entityInstanceSpecId (mode: empty)

Effect 2 — Set default instance template

  • ID: instantiable-set-default-entityInstanceSpecId
  • Listen: $context.form.instantiable
  • When: $context.form.instantiable === true and entityInstanceSpecId is empty (blank or contains an empty UUID)
  • Action: Set tsmControls.entityInstanceSpecId to $context.entityInstanceSpecId (default instance template from context)

How both effects cooperate:

User checks "Instantiable" (instantiable = true)
└─ Effect 2 fires → sets the default instance template

User unchecks "Instantiable" (instantiable = false)
└─ Effect 1 fires → clears the instance template

User checks "Instantiable" but the template is already filled in
└─ Effect 2 does NOT fire (the "is empty" condition is not met)
└─ The user's existing selection is preserved

Deactivating a configuration effect

If a specific configuration effect should not run on a particular form:

  1. Open the form in the Form Designer
  2. Go to the Effects tab
  3. In the "Configuration Effects" section, expand the desired effect
  4. Check "Deactivate this effect in schema"

A deactivated effect is displayed with an orange "Deactivated" badge and will not execute at form runtime.