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:
| Requirement | Recommended solution |
|---|---|
| Display a read-only value calculated from other fields | A JEXL default expression on that field |
| Hide, disable, or make a widget read-only | A dynamic widget property |
| Change, clear, or copy another form control | A Form Effect |
| React to a button, tab, step, attachment, or another widget event | A widget-event Form Effect |
| Execute a server-side script as part of a form workflow | A script action in a Form Effect |
| Dispatch page-level UI or store behavior after an operation | A Success / Error Action |
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.
- Open the form in the Form Designer.
- Open Effects using the lightning icon.
- Select Add effect.
- Give the effect the ID
copy-shipping-address. - Keep the Value change trigger and add
$value.sameAsShippingto Listen. - Set When to
${$value.sameAsShipping == true}. - Add a Copy action with
shippingAddressas From andbillingAddressas To. - 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.
| Trigger | Use it when |
|---|---|
| Value change | One or more form or context values change |
| Widget event | A specific widget emits an event such as success, panelOpened, or activeStepChanged |
| Invoked effect | Reusable 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:
| Action | Result |
|---|---|
set | Set a control to a static or calculated value |
clear | Empty, unset, or reset a control |
copy | Deep-copy one form value to another |
script | Execute a server-side script and optionally handle its result |
runEffect | Invoke 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:
| Property | Expected value | Example |
|---|---|---|
listen | A fully prefixed watched path | $value.customer.id |
field, from, to | A literal form-control path | customer.id |
when, set.value | A JEXL expression | ${$value.customer.id != null} |
scriptData, runEffect.inputs | Values or objects containing JEXL expressions | {"id": "${$value.customer.id}"} |
effectId | A literal effect ID | process-customer |
objectPointer | A 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 discoveryReading $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].
$row represents one array levelA 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:
| Diagnostic | Severity | What to do |
|---|---|---|
A $value read is missing from listen | Warning | Add it if its change should rerun the effect |
| Field dependency cycle | Warning | Verify that the sequence reaches a stable value or redesign it |
Invalid or mismatched $row scope | Error | Keep all $row listeners in one effect on the same array |
More than one $row in a path | Error | Address only one array level per row-scoped effect |
Direct or indirect runEffect recursion | Error | Break the invocation chain |
Missing, duplicate, or non-invoked runEffect target | Error | Give the invoked effect one unique ID and select that ID |
| Control not found at runtime | Runtime warning | Check 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.