Paths and Expressions
Choose the right path syntax
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.
Legacy listen entries that wrap a simple path in ${...} still work, but
new effects should use a direct prefixed path.
Expression Context
Effect expressions use the same JEXL runtime as computed form properties, with an effect-specific scope:
| Value | Meaning in a Form Effect |
|---|---|
$value | Current value of the complete root form. |
$context | Form context from the Fluent Forms store. |
$outputContext | Output context from composed widgets and flows. |
$config | Application/API configuration. |
$configUi | Reserved for UI configuration. The current Form Effects runtime supplies {}, so do not depend on it yet. |
$runtimeInfo | Current user and runtime metadata. |
$version | Application version. |
$build | Build metadata. |
$row | Concrete row index for a row-scoped value effect or array widget event; 0 when no row scope is present. |
dataSources | Registered datasource functions. |
dataSourcesValues | Settled datasource values available to the evaluator. |
$dataSourcesLoading | Whether an expression datasource is still resolving. |
$event | Complete semantic event in a widget-event effect. |
$payload | Shorthand for $event.payload. |
$input | Evaluated input object in an invoked effect. |
$response | Script 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 mode | Runtime behavior |
|---|---|
plainValue | Resolve a scalar or object value on the first emission. |
identifiedPack | Wait until the returned pack has loading !== true. Read the result from .data. |
unsupported | Reject 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.