Skip to main content
Version: 2.4

Form Effects

A Form Effect describes one rule:

When something happens, optionally check if a condition is true, then do one or more actions.

For example: when Same as shipping changes, copy the shipping address to the billing address.

Form Effects are configured in the Form Designer. They can react to form value changes, semantic widget events, or an explicit call from another effect. Their actions can set, clear, or copy form values and execute server-side scripts.

When to Use an Effect

Choose the simplest form feature that expresses the required behavior:

RequirementRecommended solution
Display a read-only value calculated from other fieldsA JEXL default expression on that field
Hide, disable, or make a widget read-onlyA dynamic widget property
Change, clear, or copy another form controlA Form Effect
React to a button, tab, step, attachment, or another widget eventA widget-event Form Effect
Execute a server-side script as part of a form workflowA script action in a Form Effect
Dispatch page-level UI or store behavior after an operationA Success / Error Action
Effect or computed value?

Use a computed default for a pure value derived into the same control. Use an effect when the form must change another control, clear user input, copy a value, or start an external operation.

Create Your First Effect

This example copies shippingAddress to billingAddress when the user enables sameAsShipping.

  1. Open the form in the Form Designer.
  2. Open Effects using the lightning icon.
  3. Select Add effect.
  4. Give the effect the ID copy-shipping-address.
  5. Keep the Value change trigger and add $value.sameAsShipping to Listen.
  6. Set When to ${$value.sameAsShipping == true}.
  7. Add a Copy action with shippingAddress as From and billingAddress as To.
  8. Preview the form and change Same as shipping.

The saved schema contains this effect:

{
"id": "copy-shipping-address",
"listen": ["$value.sameAsShipping"],
"when": "${$value.sameAsShipping == true}",
"do": [
{
"type": "copy",
"from": "shippingAddress",
"to": "billingAddress"
}
]
}

You can edit the same effect in the visual Form Logic Editor. Both editors update the same root-level effects[] array.

The Three Parts of an Effect

1. Trigger

The trigger determines when the effect starts.

TriggerUse it when
Value changeOne or more form or context values change
Widget eventA specific widget emits an event such as success, panelOpened, or activeStepChanged
Invoked effectReusable logic should run only when called by a runEffect action

Value-change effects use listen. Widget-event and invoked effects use trigger; they do not use listen.

2. Condition

when is an optional JEXL expression. The effect runs only when the expression returns the boolean value true.

{"when": "${$value.customerType == 'company'}"}

A direct boolean field is also valid:

{"when": "${$value.sameAsShipping}"}

Values such as the string "true", the number 1, or a non-empty object are not treated as boolean true.

3. Actions

Actions run in their listed order:

ActionResult
setSet a control to a static or calculated value
clearEmpty, unset, or reset a control
copyDeep-copy one form value to another
scriptExecute a server-side script and optionally handle its result
runEffectInvoke a reusable named effect

See the Form Effects Reference for every property and the exact execution semantics.

Paths and Expressions

Form Effects use several similar-looking values with different syntax. This is the most important distinction to remember:

PropertyExpected valueExample
listenA fully prefixed watched path$value.customer.id
field, from, toA literal form-control pathcustomer.id
when, set.valueA JEXL expression${$value.customer.id != null}
scriptData, runEffect.inputsValues or objects containing JEXL expressions{"id": "${$value.customer.id}"}
effectIdA literal effect IDprocess-customer
objectPointerA JSON Pointer to a widget in the schema/layout/0/items/2

Control paths use dot notation. Do not add $value, ${...}, or JSON Pointer slashes to field, from, or to. The only dynamic segment supported in an action path is the fluent-array row placeholder $row.

// Correct
{"type": "set", "field": "customer.email", "value": "${$value.contact.email}"}

// Incorrect: field is not a JEXL expression
{"type": "set", "field": "${$value.customer.email}", "value": "${$value.contact.email}"}
listen is the trigger, not automatic dependency discovery

Reading $value.customer.id in when, set.value, or scriptData does not automatically make the effect react to that field. Add every form value whose change should rerun the effect to listen. The designer warns when it detects a $value dependency that is not covered by listen.

Common Patterns

Clear a dependent field

Clear a city whenever the country changes. empty prevents an old default from being applied again.

{
"id": "clear-city-after-country-change",
"listen": ["$value.country"],
"do": [
{"type": "clear", "field": "city", "mode": "empty"}
]
}

Set a value from multiple inputs

Every value that should trigger recalculation is listed in listen.

{
"id": "prepare-order-label",
"listen": ["$value.orderNumber", "$value.customer.name"],
"do": [
{
"type": "set",
"field": "orderLabel",
"value": "${[$value.orderNumber, $value.customer.name].filter(x => x).join(' - ')}"
}
]
}

If orderLabel is always read-only and has no workflow meaning, prefer a computed default instead.

React to a widget result

This effect runs only when the button at /layout/2 emits success:

{
"id": "customer-loaded",
"trigger": {
"type": "widgetEvent",
"objectPointer": "/layout/2",
"event": "success"
},
"do": [
{"type": "set", "field": "status", "value": "loaded"},
{"type": "clear", "field": "errorMessage", "mode": "empty"}
]
}

Use $payload in when or action expressions when the widget event contains a result payload.

Initial Values

A value-change effect normally treats the fully initialized form as its baseline and waits for a later change. Enable Run on initialization when it must run once after initial values, expression defaults, and their data sources have settled:

{
"id": "initialize-order-label",
"listen": ["$value.orderNumber"],
"runOnInitialization": true,
"do": [
{"type": "set", "field": "orderLabel", "value": "${$value.orderNumber}"}
]
}

This option applies only to value-change effects.

Nested Forms and Arrays

Control paths can cross nested objects, characteristics, and inner forms:

customer.address.street
chars.installation.code

Although effects are stored in the root effects[] array, one definition can run independently for each row of a fluent-array. Put $row at the array index in listen and JEXL expressions, and use it as a dotted segment in action paths:

{
"id": "calculate-line-total",
"listen": [
"$value.items[$row].quantity",
"$value.items[$row].unitPrice"
],
"do": [
{
"type": "set",
"field": "items.$row.total",
"value": "${($value.items[$row].quantity || 0) * ($value.items[$row].unitPrice || 0)}"
}
]
}

The path before $row identifies the array scope ($value.items in this example). The runtime compares every concrete row and runs the effect only for rows whose listened values changed. During that run, $row is the concrete row index in when, action values, script data, and invoked-effect inputs. The same index replaces $row in literal field, from, and to paths.

All $row listeners in one effect must have the same array scope. Listening to the whole array, such as $value.items, is still useful when an effect should update one control outside it. Setting, copying, clearing, or resetting a whole array also updates the rendered fluent-array rows to match the new value.

Widget events in nested arrays

A widget event identifies both its schema source and the concrete rendered occurrence. For a widget nested in orders[]lines[], an event from outer row 2 and inner row 1 provides:

$event.objectPointer // schema source, for example /layout/0/items/1/items/0
$event.dataPointer // concrete occurrence inside orders/2/lines/1
$event.indexes // [2, 1], outermost to innermost
$row // 1, the event-owning (innermost) row

The effect remains at the root and matches the stable schema objectPointer. The runtime obtains the actual rows from dataPointer; it does not guess the deepest row from the root effect's location. For example:

{
"id": "remember-saved-line",
"trigger": {
"type": "widgetEvent",
"objectPointer": "/layout/0/items/1/items/0",
"event": "saveSuccess"
},
"do": [
{
"type": "set",
"field": "lastProcessedLine",
"value": "${$value.orders[$event.indexes[0]].lines[$row].description}"
}
]
}

Use $event.indexes[0] for the outer row and $row for the innermost row in expressions. Action paths are literal paths, so field, from, and to cannot contain an expression such as $event.indexes[0].

One $row represents one array level

A path such as $value.orders[$row].lines[$row].description is ambiguous and is rejected as E_ROW_DEPTH_UNSUPPORTED. A root effect cannot use one $row to address two nested array levels. Use the event's $event.indexes when only an expression needs all nested indexes, as in the example above; otherwise split or reshape the effect so each row-scoped effect addresses one array level.

When an asynchronous script or widget result returns, the runtime follows the original row control. If rows were inserted or reordered, $row resolves to that row's current index. If the row was removed, its continuation is skipped.

Warnings and Errors

The Form Designer analyzes effect dependencies before saving:

DiagnosticSeverityWhat to do
A $value read is missing from listenWarningAdd it if its change should rerun the effect
Field dependency cycleWarningVerify that the sequence reaches a stable value or redesign it
Invalid or mismatched $row scopeErrorKeep all $row listeners in one effect on the same array
More than one $row in a pathErrorAddress only one array level per row-scoped effect
Direct or indirect runEffect recursionErrorBreak the invocation chain
Missing, duplicate, or non-invoked runEffect targetErrorGive the invoked effect one unique ID and select that ID
Control not found at runtimeRuntime warningCheck that field, from, or to uses the correct dot path

Continue Reading

  • Form Logic Editor — visually inspect and edit triggers, actions, widget events, and dependencies.
  • Form Effects Reference — complete JSON contract, script workflows, expression context, diagnostics, and runtime safeguards.
  • JEXL language reference — operators, context values, functions, transforms, and data sources.